Now with AI-powered page building via MCP Server

Testing a cmssy app

The editor is the path your build cannot check. One smoke test covers it.

A site whose editor is dead still compiles, still serves, and still passes your unit tests. Nothing in a normal build touches the edit path, so nothing in a normal build tells you when it breaks.

checkCmssyEditMode is the check that does.

The smoke test

import { checkCmssyEditMode } from "@cmssy/next/testing";

const result = await checkCmssyEditMode({
  baseUrl: "http://localhost:3000",
  secret: process.env.CMSSY_DRAFT_SECRET!,
  path: "/",
  // Only if your URLs carry the language. The check reads <html lang> - a
  // contract - rather than hunting for a word from your copy, which an editor
  // can rewrite at any time.
  localizedPath: "/pl",
});

expect(result.failures).toEqual([]);

Run it against a started production build (next build && next start), not the dev server. Static rendering is the thing that made the edit route necessary in the first place, so the dev server tests the wrong app.

One more option exists for sites whose URL prefix is not the locale code. localizedLocale names the language localizedPath must render; it defaults to the first path segment, which is the language on a prefixed site. Set it explicitly when your prefix and your locale differ - /en-gb serving locale en, say - or the check compares against a language that does not exist and fails for the wrong reason.

What it asserts

  1. The public page returns 200 without the editor, with header and footer rendered server-side.
  2. A bare ?cmssyEdit=1 with no secret does not enter edit mode. An unverified request must never open the door.
  3. A verified cmssyEdit=1 plus cmssySecret renders the editor and moves header and footer onto the edit bridge.
  4. Optionally: the localized preview declares the language its URL asks for, via <html lang>.

Why "no header in the SSR" means success

This is the assertion that reads backwards until you see why.

In edit mode the header and footer mount through the edit bridge, which renders on the client. So a header still present in the server-rendered HTML is a header the editor can select but cannot edit - the difference between an editable block and plain markup.

When it fails, the message names the cause rather than the symptom:

edit /shop: the header and footer are still server-rendered - they will be
selectable but have no fields (is CMSSY_EDIT_HEADER set on the rewrite?)

In CI

- run: pnpm build
- run: |
    pnpm start &
    npx wait-on http://localhost:3000 --timeout 60000
- run: pnpm smoke:edit

Skip the job when the workspace secrets are absent. A green check that verifies nothing is worse than no check - it converts an unknown into a false assurance.

Wiring it as a script

The reference app keeps it as a standalone script rather than a test-runner case, so it can run in CI without a framework:

{
  "scripts": {
    "smoke:edit": "node smoke-edit.mjs"
  }
}
// smoke-edit.mjs
import { checkCmssyEditMode } from "@cmssy/next/testing";

const result = await checkCmssyEditMode({
  baseUrl: "http://localhost:3000",
  secret: process.env.CMSSY_DRAFT_SECRET,
  localizedPath: "/pl",
});

console.log(result.ok ? "EDITOR OK" : "EDITOR BROKEN");
for (const f of result.failures) console.log("  -", f);
process.exit(result.ok ? 0 : 1);

What else to test

Blocks are ordinary React components, so test them the ordinary way. Two cmssy-specific habits are worth keeping:

  • Render each block with data: undefined - that is what the editor passes, and a block that throws without its loader result is a block that breaks the editor.
  • Render with a missing locale - content may not be translated yet, and the fallback path is the one your visitors will hit first in a new language.

Next steps