Back to Docs
Recipes

E2E Test Patterns

Battle-tested patterns for writing reliable end-to-end tests that survive refactors, network flakiness, and CI pipeline drift.

The Page Object Model

Wrap every page in a plain class that exposes semantic actions — never scatter selectors across test files. When the UI changes, update one file.

class LoginPage {
  visit() { cy.visit('/login'); }
  fillEmail(v: string) { cy.get('[data-testid="email"]').type(v); }
  submit() { cy.get('[data-testid="submit"]').click(); }
}

Data-Testid Anchors

Use data-testid attributes instead of CSS classes or XPath. They survive styling churn and communicate intent to the next engineer.

Network-First Assertions

Intercept the API response and assert on status + body shape before checking the DOM. This catches backend regressions that the UI silently masks with stale state.

cy.intercept('POST', '/api/auth').as('login');
cy.get('[data-testid="submit"]').click();
cy.wait('@login').its('response.statusCode').should('eq', 200);

Retry-ability & Flake Guards

Cypress and Playwright auto-retry DOM queries. Lean into it — chain assertions off .should() instead of manual cy.wait() calls. For flaky third-party scripts, stub them at the network layer.

CI Resilience

Run tests in headless Chromium with a fixed viewport. Seed the database before each spec. Use a dedicated test tenant so parallel runs never collide.