E2E Authoring Rules

SkillFiles & storage

E2E testing with Cypress and Cucumber, feature files, step definitions, page objects, fluent interface, assertions, data-app-busy / waitUntilAppIsNotBusy pairing, test execution. Use when writing Cypress/Cucumber E2E tests.

Use E2E Authoring Rules in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add E2E Authoring Rules and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use the E2E Authoring Rules skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

E2E Authoring RulesStart free

What this skill tells your AI

The instructions your AI receives, as published by nerds-odd-e/doughnut in .agents/skills/e2e-authoring/SKILL.md and read by Ahel’s review.

Use this skill when writing or modifying E2E tests, Cypress/Cucumber feature files, step definitions, page objects, or E2E assertions. For OCR-specific canvas assertions, use the e2e-ocr skill.

Environment Assumption

The pnpm cy:run wrapper owns its stack and compiles current backend source once per batch, without a continuous backend compiler. Local batches retain frontend HMR; backend edits need a new invocation. Use pnpm cy:open for an interactive session with backend reload and frontend HMR. Required mocks are owned by the selected invocation.

If services are not running, suggest running pnpm cy:run --spec <feature> in a separate terminal.

Cypress Origin

Primary unconfigured checkouts and CI use http://localhost:5173 as the Cypress app origin through the local load balancer. A linked worktree (or a primary with .worktree.local.json) uses that checkout's isolated origin http://127.0.0.1:<lbListenPort> — see docs/worktree-browser-tests.md. Isolated Cypress accepts one or more selections from the assessed active feature set declared in scripts/isolated-cypress-spec-selection.mjs (application-only active specs plus the active CLI, MCP, OpenAI-mock, and Wikidata-mock focused specs) per run; a new feature file must be added to that set (e.g. APPLICATION_ONLY_ACTIVE_SPECS in scripts/isolated-cypress-active-specs.mjs) before an isolated run accepts it. The CLI command is scoped to the assessed active CLI workflows. The MCP command is scoped to that one search/graph feature. OpenAI-mock specs start a runner-owned private OpenAI Mountebank; Wikidata-mock specs start a runner-owned private Wikidata Mountebank; application-only, CLI, and MCP specs do not require Mountebank. Each isolated checkout needs its own healthy E2E allocation. Paired CLI-vs-browser reset proof:

CURSOR_DEV=true nix develop -c node scripts/worktree-reset-isolation-harness.mjs --mode cli --peer <cli-checkout> --resetter <browser-checkout>

CI vs pnpm cy:run, wait-on, NO_PROXY, and env vars are documented in docs/gcp/prod_env.md, Local dev / Cypress under section 6.

Run E2E Tests

Agents should default to pnpm cypress run --spec with one or more .feature paths tied to the changed capability. Add adjacent specs only when regression risk warrants it. Do not run bare pnpm cypress run, pnpm verify, or the whole suite unless the user explicitly asks for a full E2E run or the instructions require reproducing CI.

CURSOR_DEV=true nix develop -c pnpm cypress run --spec e2e_test/features/ai_generated_recall_questions/question_contest.feature

Important notes:

  • Do not use cypress run -- --spec; the empty -- causes the --spec parameter to be ignored.
  • The default tag filter is expose.tags in e2e_test/config/ci.ts, for example not @ignore. Override per raw pnpm cypress run with --expose tags='...' (-x; the pnpm cy:run wrapper drops it), not --env tags=... (ignored by @badeball/cypress-cucumber-preprocessor v27 on Cypress >= 15.17).
  • @skipOptimizationDueToKnownNecessarySlowness is Donut's profile-only exclusion for known-necessary slowness. dough-test-optimization profiles exclude it as described under "Test-optimization profiling" below. Normal CI/dev runs still execute these scenarios. Do not add this tag without developer review, except when an explicit dough-test-optimization --resolve pass makes an evident exclusion under the public skill's candidate-resolution rules.
  • @focus and @only are local debugging aids only. Do not commit them; CI runs ./scripts/check_focus_tags.sh on .feature files to block @focus, and @only should be treated the same.
  • If any node in a feature file is tagged @focus or @only, @badeball/cypress-cucumber-preprocessor filters scenarios in that file to @focus or @only only.

Test-optimization profiling

For per-scenario timing, run the selected feature scope through the owning wrapper and tee its output to a local file:

CURSOR_DEV=true nix develop -c pnpm cy:run --spec '<feature,feature,...>' 2>&1 | tee /tmp/donut-e2e-profile.log
  • The wrapper forwards only --spec (and --browser) to Cypress (scripts/e2e-cypress-process.mjs); --reporter and --expose tags=... are dropped. Apply the profile-only exclusion by leaving @skipOptimizationDueToKnownNecessarySlowness features out of the spec list, and subtract excluded scenarios inside a selected feature from the profile.
  • Per-scenario times are the spec reporter's ✓ <scenario> (<N>ms) lines; associate them with the preceding Running: <feature> line.
  • An isolated checkout (a linked worktree, or a primary with .worktree.local.json) refuses glob spec selections; list the features comma-separated. If mirroring CI, also leave out @wip features.

This exclusion is profiling-only and must not narrow normal verification.

Debugging

Use the canonical log-tail command for backend E2E log inspection:

CURSOR_DEV=true nix develop -c pnpm logs:tail backend-e2e

Frontend Vite checker terminal errors still matter even though the browser overlay is disabled for E2E stability. For canonical lint and format commands, use the linting_formating skill.

Technology And Structure

  • Cypress for E2E testing.
  • Cucumber plugin for BDD-style tests.
  • TypeScript for type safety.
  • Mountebank mocks external services.
  • e2e_test/features contains BDD-style tests.
  • e2e_test/step_definitions contains step definitions.
  • e2e_test/start/ contains page objects.
  • e2e_test/start/pageObjects/cli/ contains CLI page objects with the cli.xxx prefix.
  • e2e_test/support/ contains support files.

Focus on Behavior

Behavior (external behavior) has 3 parts: pre-condition, trigger, and post-condition. Each part is:

  • externally observable
  • of user value
  • typically involves state change.

Avoid e2e test that is only about the presentation of a state and having no state change after setup / given.

Gherkin

  • Write features in Gherkin syntax focused on behaviors that has business value.
  • Group related features in domain-specific folders.
  • Name feature files and titles by domain/capability, such as note_creation.feature or spaced_repetition.feature. Never name them by planning sequence number or delivery order.
  • Use tags, such as @usingMockedOpenAiService or @mockBrowserTime, to control test execution.
  • Keep scenarios focused and concise.
  • Prefer Scenario Outline with an Examples table when the path and assertion focus are the same and only data varies. Do not copy-paste near-identical Scenarios for different inputs. Keep separate Scenarios when the observable behavior differs (see Explicit Conditions and step-definition guidance on variants).

Step Definitions

  • Keep step definitions lightweight.
  • Delegate implementation details to page objects, preferably as fluent chains.
  • When user-visible behavior differs, use different steps and thin implementations. Do not hide variants behind one parameterized step.
  • Use TypeScript and reuse steps where practical.
  • Use parameter types for complex objects.
When("I start a conversation about the note {string}", (noteTopology: string) => {
  start.jumpToNotePage(noteTopology).startAConversationAboutNote()
})

Page Objects

Use page objects with a fluent interface: methods perform an action and return the next page object, or this when staying on the same screen. This keeps step definitions short and navigation explicit in the types.

  • Navigation (ADR 0005). E2E navigation goes through e2e_test/start/router.ts (visitNamed / named push). Compile hrefs with namedLocationHref / noteShowHref. Page objects and steps do not call cy.visit with SPA path strings (scripts/check_e2e_spa_visit_gate.sh in CI).

  • Signing in only establishes the session; it does not open the app. The first router().push opens it, page objects that start from the main menu use withinMainMenuItem (which calls router().openApp()), and a re-login reloads an app the scenario already opened. A scenario that never touches the web UI pays no page load.

    IntentMechanism
    Unique trigger is in-app navigationUI
    Given-shaped shortcut (including Gherkin When I visit … when the unique behavior is on that screen)Named router.push after first load
    First SPA load, inbound URL, or explicit remountcy.visit of href compiled from the named table
  • Recall: visitRecallPage is remount (visitNamed('recall')); navigateToRecallPage is sidebar UI. Gherkin When I visit recall stays remount — do not convert it to sidebar.

  • Centralize UI interactions in page objects.

  • Prefer fluent chaining over one-off helpers that do not return a navigable object.

  • Keep page objects focused on a single responsibility.

  • Use meaningful method names that reflect user actions.

const notePage = {
  startAConversationAboutNote() {
    this.toolbarButton("Start a conversation").click()
    return conversationPage()
  },
}

CLI steps use cli.xxx page objects, not start.xxx. For the install feature, use cli.backend(), cli.installation(), and cli.nonInteractiveOutput() for spawned version / update stdout. The @interactiveCLI-tagged CLI files (cli_access_token.feature, cli_interactive_mode.feature, cli_recall.feature) and cli_gmail.feature are @ignore in CI; extend Vitest under cli/tests/interactive/ for interactive TTY coverage instead of adding a second Cypress PTY harness.

Then("I should see {string} in the non-interactive output", (expected: string) => {
  cli.nonInteractiveOutput().expectContains(expected)
})

Waiting until the app is not busy

Product loading UI that means unfinished work marks itself with data-app-busy (LoadingThinBar from apiCallWithLoading, ContentLoader for pending page data, LoadingModal for blockUi). See .agents/skills/frontend/SKILL.md.

E2E waits with waitUntilAppIsNotBusy() from e2e_test/start/pageBase.ts (also start.waitUntilAppIsNotBusy()):

import { waitUntilAppIsNotBusy } from '../pageBase'

cy.findByRole('button', { name: 'Submit' }).click()
waitUntilAppIsNotBusy()

Rules of thumb:

  • Call waitUntilAppIsNotBusy() in the page object on the action that starts loading, before the next step that needs that work’s result.
  • Prefer asserting a user-visible success outcome when the product provides one; use the waiter for “busy cleared,” not as a substitute for success.
  • Do not use hardcoded waits (cy.wait(ms)) for this.
  • Do not skip the wait and rely on a later navigation’s loading check alone — that races when the next step does not go through busy UI.
  • Network cy.intercept / cy.wait('@…') is optional; it is not the documented default stand-in for this product/E2E pair.

Test Data And Service Mocking

Follow test data and service mocking when writing Given setup or external-service fixtures.

Assertions

Error messages must be immediately actionable:

  1. Assert with clear domain meaning, not just technical selectors.
  2. Show expected and actual values when they differ.
  3. Include relevant context, such as page content or API response, when helpful.
  4. Given, When, and Then steps should be assertive and fail early.
  5. Put assertions inside page objects where that keeps step definitions simple.
expectQuizScore(expectedScore?: string) {
  if (expectedScore) {
    cy.get("[data-test='quiz-score']").should(($score) => {
      const actualScore = $score.text().trim()
      expect(
        actualScore,
        `Expected quiz score to be ${expectedScore}, but found ${actualScore}`
      ).to.equal(expectedScore)
    })
  }
  return this
}

Explicit Conditions

A scenario describes one fixed path. Prefer dumb automation: the path is visible in Gherkin and in small, purpose-named page-object methods, not inferred at runtime from a string or flag.

  • Avoid large if / switch trees, parameter sniffing, and mode flags in steps and page objects except when the branch exists only to assert or fail fast with a clear error.
  • Distinguish variants at the caller with different steps and/or page-object methods, not one smart API.
  • State the condition in step wording when behavior differs, for example slash command vs plain line vs Enter alone vs ESC.
  • For CLI TTY sequences, add or extend Vitest under cli/tests/interactive/ instead of adding a second Cypress PTY stack.

Before hook order

@badeball/cypress-cucumber-preprocessor defaults Before hooks to order 10000. The shared DB reset Before uses order: 0 so it runs before tagged setup. Hooks that start long-lived side effects (e.g. @interactiveCLI PTY at order 2) must stay after that reset; otherwise truncate can block on MySQL locks held by the PTY’s HTTP traffic.

Pitfalls

  • Do not put complex logic in step definitions.
  • Do not create long scenarios with many steps.
  • Avoid using UI language like "click a button" in Gerhkin. Use intention and domain language.
  • Do not rely on test order.
  • Do not use hardcoded waits.
  • Do not fire a user action that starts app-busy loading and then assert domain state without waitUntilAppIsNotBusy() (or a clearer user-visible success signal) first.
  • Failure handling in steps and page objects: ADR 0006.
  • Do not mix different levels of abstraction in scenarios.

Signals

GitHub stars
49
Forks
72
Last commit
Oct 2026
Advanced
Item type
skill
Key
e2e-authoring
Source
github.com/nerds-odd-e/doughnut