Specs describe what the user does and sees. Every selector lives in a steps class, every
cy.interceptin a stubs class. A spec contains only step calls, assertions, and theGiven / When / Thennarrative.
This is the guide for the Cypress suites in e2e-tests/. Treat the rules as
rules: where one genuinely doesn’t fit, deviate on purpose and leave a comment
saying why. Much of the existing suite predates these rules (class selectors,
cy.get in specs); write new and touched specs to them.
Unit tests (Jest) follow testing.md instead.
steps, stubs, fixtures, support folders.data-test, and describe/it title patterns.verifyX; BaseSteps, composite steps.data-test first, then ARIA; PrimeNG overlays outside the page root; translated text.stubX/spyX), waiting on aliases, fixtures.Given / When / Then, assertions, an example.Given / When / Then as
comments (see §9.2).The specs run against the real backend: a GraphDB at http://localhost:7200,
with the workbench dev server (npm run start from the repo root) on
http://localhost:9000 proxying to it. Every config sets baseUrl to
http://localhost:9000.
Run one spec, or a part of one — never the whole suite locally. From
e2e-tests/:
npx cypress run --config-file cypress-legacy.config.js --spec e2e-legacy/repository/repositories.spec.js --browser chrome
npx cypress open --config-file cypress-legacy.config.js # interactive
The cy:* scripts in e2e-tests/package.json run whole suites and are for CI.
e2e-tests/
├── e2e-legacy/ # Main suite (cypress-legacy.config.js) — one folder per feature
│ └── repository/
│ ├── repositories.spec.js
│ └── repository-picker-tooltip.spec.js
├── e2e-security/ # Security suite (cypress-security.config.js)
├── e2e-flaky/ # Quarantined flaky specs (cypress-flaky.config.js)
├── steps/ # Steps classes — all DOM access lives here
│ ├── base-steps.js # BaseSteps — generic helpers
│ ├── application-steps.js
│ ├── header-steps.js # One cohesive shared component per file
│ └── repositories/ # Feature folders for multi-file features
├── stubs/ # cy.intercept stub/spy classes
│ └── repositories/
│ └── repositories-stubs.js
├── fixtures/ # JSON response payloads, per feature
│ └── repositories/
│ └── get-repositories.json
└── support/ # Custom commands (*-commands.js) and the support file
The security suite has its own guide: security testing.
Don’t run or fix e2e-flaky/ specs as part of verifying a change.
Split specs by capability. Don’t put every test for a feature in one file:
repositories.spec.js, rename-repository-restrictions.spec.js,
repository-picker-tooltip.spec.js, and so on.
| Asset | Convention | Example |
|---|---|---|
| Spec file | e2e-legacy/<feature>/<capability>.spec.js |
repository/repository-picker-tooltip.spec.js |
| Steps class | <Feature><Page>Steps |
RepositoryPickerListSteps |
| Steps file | <feature>-<page>-steps.js |
repository-picker-list-steps.js |
| Stubs class | <Feature>Stubs |
RepositoriesStubs |
| Stubs file | <feature>-stubs.js |
repositories-stubs.js |
| Fixture file | fixtures/<feature>/<verb>-<noun>[-<variant>].json |
fixtures/repositories/get-repositories.json |
| Intercept alias | camelCase verb + noun | getRepositories, runningRepositorySize |
data-test |
kebab-case, prefixed with the component/page | graph-navigator-settings-btn |
describe title: the human-readable feature/page name — 'Repository picker repository information'.it title: lowercase 'should <expected outcome>' —
'should show the repository id and the statement counts of a running repository'.We use the page-object pattern through Steps classes, so selectors live in one
place and a DOM change never touches a spec.
static. Steps are stateless namespaces, not instances.getView() /
getPage()), and scope children from it. Exception: overlays rendered outside
the page (see §6.2).getX) locate and return a chainable. They don’t assert. The spec
decides what to check.openX, focusRepository, submitForm) perform a user
interaction, named after the user’s intent.verifyX) are the one place where steps assert. Use them for
rich, repeated structures (see §5.3).cy.get in the spec.BaseStepsGeneric helpers shared by several steps classes live in steps/base-steps.js;
extend BaseSteps. Keep it small and truly generic. Feature logic belongs in the
feature’s steps.
A composite step does a bigger job, like checking every row of a table against expected data.
describe.verifyX method.data-testSelect by the data-test attribute, through the getByTestId command
(support/commands.js), which builds [data-test="..."]:
<button data-test="graph-navigator-settings-btn">
static getSettingsButton() {
return this.getView().getByTestId('graph-navigator-settings-btn');
}
data-test, add one to the template in the
same change. Don’t fall back to a fragile selector.[role="dialog"], [aria-label="…"]),
when the element is inherently identified by its accessibility role..p-button), deep descendant
chains, and position-based CSS (:nth-child). They change for visual reasons..eq(i) on a list of data-test matches rather than position selectors.Several PrimeNG pieces are not inside the page component’s DOM:
appendTo="body" (selects, menus, popovers) is moved to the
end of <body>.So a page-root query will never find them. Query these from
ApplicationSteps (or a dedicated overlay steps class) starting at cy.get('body')
or at the overlay’s own data-test/role.
Tests run in English.
data-test.Given, and must not leave
anything behind for the next test.cy.intercept)The specs run against the real backend for true end-to-end coverage. Stub only when you must force a state the real backend can’t easily produce: errors, empty states, edge-case data, enterprise-only responses, slow responses.
All cy.intercept calls go in a stubs class. Two kinds:
stubX) return a canned response to control what the app sees.spyX) let a real request through and give it an alias, so a test can
cy.wait('@alias').export class EnvironmentStubs {
static spyProductInfo() {
cy.intercept('/rest/info/version?local=1').as('productInfo');
}
static stubProductInfo(productVersion) {
cy.intercept('GET', '/rest/info/version?local=1', {
statusCode: 200,
body: {productType: 'free', productVersion}
}).as('productInfo');
}
}
cy.wait('@productInfo')), never on fixed times
(cy.wait(500)).cy.wait doesn’t
care which one is active.{statusCode: 500})
can stay inline in the stubs class.stubXError, stubXEmpty),
never as inline objects in a spec.Custom commands live in support/*-commands.js. Add them only for setup that
isn’t DOM interaction:
cy.createRepository(...),
cy.deleteRepository(...));cy.importServerFile(...));cy.switchOnSecurity(), cy.loginAs(...)).Never add a custom command for clicking, typing, or finding elements. That is
what steps are for. getByTestId is the one generic DOM helper, and it is used
inside steps, not in specs. Some existing commands (cy.pasteQuery,
cy.verifyResultsMessage, …) predate this rule; don’t add more like them.
A command returns its Cypress chain so callers can chain on it.
describe per feature/page. Use nested context blocks for tests that share
a precondition.beforeEach. Give created resources unique names
('picker-tooltip-running-' + Date.now()) so runs never collide.afterEach (e.g. cy.deleteRepository) is fine, but no test
may depend on it: afterEach doesn’t run if the run is interrupted. Each test
creates the state it needs instead of assuming a clean server.Document the scenario with comments. Given is setup (stubs, seeded data), When
is the user’s action, Then is the observable outcome. Use And / But for
continuations.
// TODO:.verifyX steps (§5.3).should('have.text', repositoryId)) over bare
existence checks when the value matters.From e2e-legacy/repository/repository-picker-tooltip.spec.js:
describe('Repository picker repository information', () => {
let runningRepositoryId;
beforeEach(() => {
runningRepositoryId = 'picker-tooltip-running-' + Date.now();
cy.createRepository({id: runningRepositoryId});
// A new repository is not running. Requesting its size starts it.
cy.initializeRepository(runningRepositoryId);
RepositoriesStubs.spyGetRepositorySize(runningRepositoryId, 'runningRepositorySize');
GraphNavigatorSteps.visit();
});
afterEach(() => {
cy.deleteRepository(runningRepositoryId);
});
it('should show the repository id and the statement counts of a running repository', () => {
// When I move the keyboard focus to a running repository in the picker
RepositoryPickerListSteps.focusRepository(runningRepositoryId);
// Then I expect the repository information to show the repository id
RepositoryPickerListSteps.getRepositoryInfoPart('title').should('have.text', runningRepositoryId);
// And the statement counts, loaded from the server
cy.wait('@runningRepositorySize');
RepositoryPickerListSteps.getRepositoryInfoPart('total').should('contain.text', 'Total statements');
});
});
Do
data-test. Add one to the template when it’s missing.ApplicationSteps, not the page root.Given / When / Then for multi-step tests.Don’t
cy.get(...) or cy.intercept(...) in a spec.cy.wait(<ms>).afterEach for a clean starting state.process) in Cypress config; add them to languageOptions.globals..then() when you don’t use what it yields — it opts out of the retry mechanism.{ force: true } — it clicks through overlays and hides real bugs. Close or wait out what covers the element.Before calling a test done:
cy.intercept in stubs.data-test (added to the template if missing).Given / When / Then comments that match the code.describe/it names follow §4.afterEach.cy.wait(ms). Sync on aliases or UI state.