Writing CodeceptJS 4 Tests
A test that was never executed during authoring is unreliable. Drive the real browser via the CodeceptJS MCP server, verify every locator against the live page, commit only steps that passed.
Two modes, picked by how much of the flow you already know:
- Mode A — incremental
run_code: send one or twoI.*lines per call, read response, repeat. For extending existing tests, known flows, porting manual plans. - Mode B — scaffold-and-pause (recommended for greenfield / unknown flows): write a stub Scenario containing
I.amOnPage('/...'); pause();, run via MCPrun_test. The browser opens and yields control atpause()— drive the live page viarun_code, then replacepause()with the verified sequence.
Both share the same discovery / locator / commit steps; the difference is where the in-flight exploration happens.
Workflow
- Fundamentals first — run
codeceptjs-fundamentals. You need: active web helper, base URL, plugins (aiTrace,auth), AI provider, page objects, env vars. - Map what's already there —
check/list/dry-run(see fundamentals § Discover). Catches duplication and confirms no custom step or page-object method already covers the planned flow.- ⚠
dry-rundoes not initialize plugins:Before(({ login }) => login('admin'))raises "login is not a function" under dry-run even though a real run works. Inspect shape with--steps; ignore plugin-inject errors; verify auth with a real run.
- ⚠
- Auth — path behind login → invoke the
codeceptjs-authskill. Existing role configured:Before(({ login }) => login()). Not configured: auth skill walks adding it. Public path: skip. - Similar tests & page objects — scan Scenarios in the feature area, POs under
include, actor-file custom steps, data factories. If a PO method already encodes this area's locators, drive through it instead of rawI.clickchains. - Starting page — a real URL after any auth, not a guess; ask the user if unknown. Relative URLs only — host lives in the config (
helpers.Playwright.urletc.). - aiTrace + MCP wiring:
- Under MCP:
aiTraceis forced on by the MCP server — nothing to configure. - CLI runs: not auto-enabled — declare in config or pass
-p aiTrace, or step 11 produces nooutput/trace_*/artifacts for run-analysis. - Run headless by default (
setHeadlessWhen(CI)withCI=1exported, orshow: false). - Confirm MCP client points at
node_modules/codeceptjs/bin/mcp-server.jswithCODECEPTJS_CONFIGandCODECEPTJS_PROJECT_DIRset.
- Under MCP:
- Open a live session (pick mode):
- Mode A:
run_codescaffold —login(<role>)if needed, thenI.amOnPage(<start URL>). The response (URL, ARIA snapshot, screenshot, console) is ground truth for everything after. - Mode B: write a minimal-but-real draft in the test file:
Run via MCPBefore(({ login }) => login('admin')) // if needed Scenario('draft - feature exploration', ({ I }) => { I.amOnPage('/dashboard') pause() })run_test→{ status: 'paused', pausedAfter, page, suggestions }. The test's ownI/ browser is now driven byrun_code.
- Mode A:
- Learn the page — hand off to
codeceptjs-exploration: ARIA snapshot first, HTML fallback, enumerate and disambiguate candidates, commit a locator only after verifying it matches exactly one element viarun_code. In Mode B this happens inside the pause window. - Build the Scenario — one or two commands at a time via
run_codeinto a scratchpad. After each ask: did URL/ARIA change as expected? New console errors? Do grabbed values match expectations?- Step failed → try a different locator, add a specific
waitFor*, or reconsider the flow. - Genuinely ambiguous (two Save buttons, unclear empty state, possible feature flag) → stop and ask the user.
- Optional UI elements (cookie banners) →
await tryTo(...)instead ofif(fundamentals § Effects) — keeps scenarios linear.
- Step failed → try a different locator, add a specific
- Commit the verified sequence:
- Match existing naming; one
Featureper file. - Use page-object methods / custom steps where they fit — don't duplicate selectors.
- Translate every locator to readable form (priority below). Strict
{ css }/{ xpath }in committed code are a review red flag unless nothing else fits. - Credentials from env vars only, wrapped in
secret(...). - Mode B: replace the
pause()line with the sequence; renamedraft - ...to the real intent.
- Match existing naming; one
- Final verification:
npx codeceptjs run --grep '<scenario>' --stepswith aiTrace enabled → hand output tocodeceptjs-run-analysisto confirm the flow ran clean inoutput/trace_*/. Done only when it passes there.
Locator priority (writing time)
Always pass context — see codeceptjs-fundamentals § Locators for rationale. Top wins:
- Semantic string — visible text, label, placeholder,
name,aria-label:I.click('Save', '.toolbar') - ARIA role — text ambiguous within context ("Delete" link and button), or role part of the assertion:
I.click({ role: 'button', name: 'Sign In' }, '#login-form') $nameviacustomLocator— app exposesdata-testid/data-qabroadlylocate()builder — structural conditions; often the structural half belongs in the context:I.click('Edit', locate('tr').withText('Acme Corp'))- Strict
{ id }/{ name }/{ css }— last resorts above exhausted { xpath }— axes / text predicates the builder can't express
Writing-time specifics beyond fundamentals:
- Plain strings already match
aria-label— never write'aria-label=Save'or{ css: '[aria-label="Save"]' }. - Prefer stable contexts: landmarks (
nav,main,{ role: 'dialog' }), app-shell containers (.sidebar,.modal), rows/cards identified by their data. I.see/I.dontSeerequire a context — their first arg is plain text matched across the whole page; unscoped they can false-pass on nav/footer content.- Several matches →
step.opts({ elementIndex: N })(1-based, negative,'first'/'last') orstep.opts({ exact: true }); importstepfromcodeceptjs/steps.
Waiting while authoring
- Auto-waiting covers interactions; detect gating elements (spinner overlay, post-fetch render) from the live HTML/ARIA between MCP steps → pick the stable selector and a specific
waitFor*. I.wait(N)is acceptable during authoring to confirm a timing hypothesis — if a sleep makes the step pass, timing is the cause. Replace with the specificwaitFor*before committing.
Things to avoid
- Writing tests from imagined locators or routes — everything from the live page.
- Strict locators where semantic / ARIA /
locate()fits. - Long unscoped locators instead of short locator + context.
- Spelling out accessible names or repeating
[data-testid=...]at call sites. - Hardcoded credentials anywhere.
- Skipping the similar-test / page-object scan.
awaiton plain action steps (fundamentals await rule); speculativewaitFor*before checking auto-waiting.- Leaving
I.wait(N)orpause()in committed tests. - Using Mode A for genuinely unknown flows — slower than Mode B, easier to lose state.
- Declaring done without the end-to-end aiTrace run.
Related skills
codeceptjs-fundamentals— rules, effects, discovery (run first)codeceptjs-exploration— page inspection, WebElement APIcodeceptjs-auth— login/session reusecodeceptjs-run-analysis— trace verificationdebugging-codeceptjs-tests— when the committed test misbehaves