graphdb-workbench

E2E testing conventions (Cypress)

Specs describe what the user does and sees. Every selector lives in a steps class, every cy.intercept in a stubs class. A spec contains only step calls, assertions, and the Given / When / Then narrative.

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.

Contents

  1. Principles: the six rules everything else follows; no selectors in specs is the most important.
  2. Running the tests: the backend, the dev server, and running one spec.
  3. Directory structure: the suites and the shared steps, stubs, fixtures, support folders.
  4. Naming: file, class, fixture, alias, data-test, and describe/it title patterns.
  5. Steps classes: page objects with static getters, actions, and verifyX; BaseSteps, composite steps.
  6. Selectors: data-test first, then ARIA; PrimeNG overlays outside the page root; translated text.
  7. Stubbing & network control: stubs classes (stubX/spyX), waiting on aliases, fixtures.
  8. Custom commands: for REST seeding, imports, and auth.
  9. Writing a spec: structure, state, Given / When / Then, assertions, an example.
  10. Do / Don’t and Checklist.

1. Principles

  1. Behaviour, not implementation. A spec reads as user intentions and expected outcomes. How the DOM is queried is hidden behind steps.
  2. No selectors in specs. All DOM access lives in steps classes. This is the most important rule.
  3. Structure mirrors features. Specs, steps, stubs, and fixtures are grouped by feature, so related files sit together.
  4. Small, targeted tests. One scenario per test. Long scenarios only when the feature genuinely requires an ordered sequence.
  5. Readable narrative. Multi-step tests spell out Given / When / Then as comments (see §9.2).
  6. Deterministic and isolated. A test passes on its own and in any order. It creates the state it needs.

2. Running the tests

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.


3. Directory structure

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.


4. Naming

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

5. Steps classes (page objects)

We use the page-object pattern through Steps classes, so selectors live in one place and a DOM change never touches a spec.

5.1 Rules

5.2 BaseSteps

Generic 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.

5.3 Composite steps

A composite step does a bigger job, like checking every row of a table against expected data.


6. Selectors

6.1 Use data-test

Select 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');
}

6.2 PrimeNG overlays render outside the page

Several PrimeNG pieces are not inside the page component’s DOM:

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.

6.3 Text and translations

Tests run in English.


7. Stubbing & network control (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.

7.1 Stubs classes

All cy.intercept calls go in a stubs class. Two kinds:

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');
    }
}

7.2 Rules


8. Custom commands

Custom commands live in support/*-commands.js. Add them only for setup that isn’t DOM interaction:

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.


9. Writing a spec

9.1 Structure

9.2 Given / When / Then

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.

9.3 Assertions

9.4 Example

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');
    });
});

10. Do / Don’t

Do

Don’t


11. Checklist

Before calling a test done: