Tool · Template

AI test automation standards.

Hand a coding agent a test suite with no guidance and it writes tests its own way: brittle selectors, fixed waits, empty test IDs, every convention you have ignored. This is the file that makes it write them your way. Pick your framework, copy it, and drop it at your repo root as CLAUDE.md or AGENTS.md.

Each tab is a real, complete standards file for that runner, not a generic template with the commands swapped. Claude Code reads CLAUDE.md; Cursor, Codex, and Copilot read AGENTS.md. It is one file with two names, so download whichever your tools expect. Drop it at your repo root, edit the bracketed parts for your app, and commit it, so the whole team and every agent share one set of conventions.

CLAUDE.md · Cypress
# Cypress End-to-End Test Suite: standards for AI agents

This file is for a Cypress end-to-end suite. Drop it at your repo root as CLAUDE.md
(Claude Code reads it) or AGENTS.md (Cursor, Codex, and Copilot read it). It is what
makes a coding agent write Cypress tests your way instead of its own. Edit the
[bracketed] parts for your app.

## Architecture
- Driver pattern: all Cypress interactions go through driver utility functions, never
  raw cy commands in specs.
- Page Object Model (POM): all selectors, text, and URL params live in page object files.
- Role-based organisation: tests organised by feature > role > spec.
- Path alias: ~/ maps to cypress/ (configured in tsconfig.json).

## Critical rules

### 1. Driver-only interactions
ALL Cypress interactions MUST use driver utilities. Never use raw cy.get(), cy.contains(),
cy.url(), cy.click() etc. in spec files.
Allowed exceptions:
- cy.get("@alias") for Cypress aliases (e.g. cy.get("@userName"))
- cy.get("body").then(($body) => ...) for conditional DOM checks (synchronous jQuery)
- cy.window(), cy.session() inside custom commands only
If no driver exists for what you need, create one in the appropriate driver file.

### 2. No inline values in specs
All text strings, regex, URL fragments, and selectors live in page object files. Nothing
hardcoded inline in a spec.

### 3. No inline comments
Test titles and driver names are self-documenting. Do not add comments inside spec files.

### 4. No cy.wait()
Never use cy.wait(ms) for arbitrary delays. Instead:
- waitOnAlias() after API calls
- elementShouldExist() / validateElementIsVisible() for DOM readiness

### 5. Test IDs (HARD RULE)
Every it() must start with a non-empty ID in square brackets. An empty [] never ships.
- Carry over an existing case ID from your test-case manager (e.g. [C1234]) when the test
  covers a mapped scenario, including when porting it into a new spec.
- New scenario with no mapping: a short per-feature prefix plus a zero-padded number, e.g.
  [chk-001], [chk-002]. Keep the prefix consistent per feature; never renumber existing IDs.

### 6. Stable selectors
Use attributes from your component library: data-* slots (e.g. [data-slot="table-body"]),
role (e.g. [role="dialog"]), aria-label, or id with a partial match ([id*="trigger-"]).
Avoid class names, DOM hierarchy, and nth-child selectors.

### 7. Reusable helpers in driver files
Multi-step workflow actions (select group + wait, open drawer, dismiss dialog) belong in
drivers_{feature}.ts files, not inline in specs.

### 8. Explicit beforeEach
Keep login, intercept setup, navigation, and dismissals explicit in each spec's beforeEach,
so you can read what each test starts with. Do not hide navigation inside drivers.

### 9. Reusable helpers in the POM
Multi-step actions shared between specs of the same feature go in that feature's page object
file (when tightly coupled to its selectors/text) or a drivers_{feature}.ts file (when they
orchestrate multiple drivers or are shared across features).

## Driver design rules
Drivers represent USER ACTIONS, not Cypress primitives.
- Good: selectGroupAndWaitForData(), openSavedView(name), openFirstRowDrawer(), dismissWelcomeDialog()
- Bad: clickButton(selector), typeIntoInput(selector) (too generic, exposes implementation)
Drivers should encapsulate selectors (callers never pass raw selectors), encapsulate waits
(intercept + action + waitOnAlias inside the driver), and represent real workflows. When a
feature exceeds ~5 helpers, give it a dedicated drivers_{feature}.ts file.

## Test isolation
Each test is independent and self-contained:
- Every it() works regardless of execution order.
- Never depend on state from a previous test.
- beforeEach sets up all required state (login, navigation, data prerequisites).
- cy.session() caches login for speed, but each test navigates fresh.
Sharing state between tests (e.g. skipping login because a previous test logged in) is forbidden.

## Data management (live environments)
Tests run against live environments with real data:
- Be resilient to data variations: no exact-count or specific-name assertions.
- Use regex for dynamic values (numericPattern: /\d+/) and "at least" assertions
  (validateElementLengthIsAtLeast(selector, 1)).
- When data may not exist, use cy.get("body").then() for a synchronous check instead of a
  retrying assertion that will time out.
- Never create or delete production data. Tests are read-only observers.

## Assertions philosophy
Validate what the USER sees, not implementation details:
- validateContainsElementIsVisible() for text on screen
- validateURLContainsString() for navigation state
- validateElementLengthIsAtLeast() rather than exact counts
- validateContainsElementParentTextMatches() with regex for dynamic numbers
Prefer positive assertions. Use negative ones (elementShouldNotExist) only for access-denied
and permission tests.

## Retry and flakiness
Cypress retries assertions (defaultCommandTimeout). Use it correctly. Synchronisation, in
order of preference:
1. Wait on an API alias: waitOnAlias("groups") after actions that trigger calls.
2. Validate a UI state change: validateElementIsVisible() / elementShouldExist().
3. Validate element content: validateContainsElementIsVisible("expected text").
Prevention:
- Never cy.wait(ms) for timing; wait on a real signal.
- For elements that may not exist, use cy.get("body").then() synchronous checks.
- Intercept a new API call BEFORE the click that triggers it, then wait on it.
- Fix flaky tests rather than leaning on run-mode retries.
Config: defaultCommandTimeout 20000, pageLoadTimeout 60000, retries { runMode: 2, openMode: 0 }.

## Test readability (the shape every spec follows)
describe("Admin: Feature name", { tags: ["@feature", "@regression"] }, () => {
  beforeEach(() => {
    cy.validLogin("admin");
    interceptAndAssignAlias("GET", "**/api/**someEndpoint**", "alias");
    navigateTo(PAGENAMES_URLS["Page name"]);
    waitOnAlias("alias");
    dismissWelcomeDialog(); // if applicable
  });

  it("[chk-001] Verify admin can perform the action", () => {
    selectGroupAndWaitForData();
    validateContainsElementIsVisible(featurePO.staticText.expectedText);
  });
});
Tests are short and focused (one behaviour each). Action first, then validation. The title
says what the user can do, not how.

## API alias naming
Aliases describe the endpoint purpose (groups, userDetails, savedViews, dashboard). Avoid
generic names (api1, data, response). Suffix repeats with a number (dashboard2).

## API intercept pattern
Always set up the intercept BEFORE the action that triggers it:
  interceptAndAssignAlias("GET", "**/api/**groups.list**", "groups");
  navigateTo(PAGENAMES_URLS["Dashboard"]);
  waitOnAlias("groups");
For calls triggered mid-test, intercept right before the action, then wait on it.

## What AI should NEVER generate
- cy.wait(5000) or any cy.wait(ms): use waitOnAlias() or element checks
- cy.get("selector").click() in a spec: use a driver
- inline selectors or inline text: they go in page objects
- conditional logic in a spec (except cy.get("body").then() for empty-state checks)
- hardcoded timeouts: use configured defaults or waitOnAlias
- comments in specs: code is self-documenting
- cy.contains("text"): use clickElementContainingText() or validateContainsElementIsVisible()
- generic driver names like clickButton(): drivers represent user actions
- tests that depend on another test's state
- exact-count assertions on live data: use "at least" or pattern matching
- it("[] ..."): an empty test ID is forbidden
When a required driver does not exist, create it rather than using a raw command.

## File structure
cypress/
  e2e/{feature}/{role}/            specs organised by feature then role
  pages/
    common_navigation_constants.ts URL map: PAGENAMES_URLS
    page_objects/{feature}_page.ts page objects per feature
  support/
    commands.ts                    custom commands (validLogin, etc.)
    e2e.ts                         global hooks
  utils/
    drivers.ts                     core interactions: click, type, navigate, get, clear
    drivers_validation.ts          assertions: visibility, text, URL, existence
    drivers_requestHandler.ts      intercepts: interceptAndAssignAlias, waitOnAlias
    drivers_tableUtils.ts          table lookups
    drivers_{feature}.ts           feature-specific multi-step workflows
Spec naming: {feature}_{role}_spec.cy.ts (e.g. dashboard_admin_spec.cy.ts).

## Page object pattern
Each page object file exports named objects with a consistent prefix:
  export const dashUrlParams = { tabOpen: "tab=open", groupId: "groupId=" };
  export const dashStaticText = { title: "Dashboard", numericPattern: /\d+/ };
  export const dashSelectors = { tableBody: '[data-slot="table-body"]', drawer: '[role="dialog"]' };
Import as a namespace: import * as dashPO from "~/pages/page_objects/dashboard_page";

## Import order
constants > page objects > drivers > drivers_validation > drivers_requestHandler > feature drivers

## Environment variables and login
Specs never read credentials directly. Log in by ROLE with cy.validLogin("admin"); the command
resolves the matching username plus the shared password internally. Define one env var per role
(e.g. CYPRESS_ADMIN_USERNAME, CYPRESS_EDITOR_USERNAME, CYPRESS_VIEWER_USERNAME) plus CYPRESS_PASSWORD.

## Tags
Apply tags on describe blocks, not individual tests:
  describe("Feature", { tags: ["@feature", "@regression"] }, () => { ... });
Run filtered: cypress run --env grepTags=@feature,grepFilterSpecs=true

## Common patterns
Empty tables (do not let a retrying query time out):
  cy.get("body").then(($body) => {
    if ($body.find(`${dashPO.dashSelectors.tableBody} tr`).length > 0) { /* validate rows */ }
  });
Dismiss a popup only if present:
  cy.get("body").then(($body) => {
    if ($body.find(`:contains("${dashPO.dashStaticText.notNow}")`).length > 0) {
      clickElementContainingText("button", dashPO.dashStaticText.notNow);
    }
  });

## PR review checklist
- Specs use only driver utilities (zero raw cy commands except the allowed exceptions)
- All text and selectors live in page objects (zero inline values in specs)
- No inline comments, no cy.wait(ms)
- Each test is independent (reorder and they still pass)
- Imports follow the standard order; feature helpers in drivers_{feature}.ts
- beforeEach is explicit and readable; every it() has a real ID

What an agent should never produce

The fast tell that an agent ignored the file, in any framework. If you see one of these in a generated spec, send it back.

A fixed-time wait: cy.wait(ms), page.waitForTimeout, or browser.pause
A raw framework command in a spec (wrap it in a page object or helper)
Inline selectors or text in a spec (they belong in page objects)
A brittle selector: XPath, nth-child, or a deep CSS chain
Hardcoded test data that rots when the environment resets
A test that depends on another test or on run order
An exact-count assertion against live data
A missing or empty test ID

You do not write the file by hand. You build it with the agent.

Do not type these rules from a blank page. Let the agent draft and prove the file while you bring the judgment. Run these three prompts in order: mine what you already correct, have the agent read your suite and draft the file, then make it prove the file with real tests. The full walkthrough is here.

1Mine your corrections

Based on everything you know about how I work and every correction I have given you, list the things I correct you on the most. Group them so I can turn each into a checkable rule.

2Investigate the suite, then draft

Read this test suite before you write anything. Open the specs, the page objects, the drivers, the fixtures, and the config, and work out the conventions already in use: how locators are written, how interactions are wrapped, where selectors and test data live, how tests are named and isolated, and where the suite contradicts itself. Then draft a CLAUDE.md at the repo root that turns those conventions into rules an agent can check itself against, each with a one-line example of what passes and what fails. Keep it under 200 lines. If a rule is really a multi-step procedure, leave it out and tell me to make it a skill; if it only matters in one folder, tell me to make it a path-scoped rule. Show me the draft and the biggest inconsistencies you found, and do not change a test yet.

3Prove the file, then tune from what breaks

Using only the rules in that CLAUDE.md, write three tests and run them: one simple case, one hard case with real setup, and one edge case I keep meaning to cover. Every test must assert what the user actually sees, not just that the page loaded. Assert each thing that matters on its own, and let each test fail for one clear reason; if a test checks two behaviors, split it into two. Run them until they pass, and show me the output. Then I will tell you what to fix, and you fold those fixes back into the CLAUDE.md as new rules, not just patch the tests.