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.
Next: CI/CD Pipeline Recipe →