Documentation

adrs/056-e2e-test-infrastructure-conventions.md

ADR 056: E2E Test Infrastructure Conventions

Status

Accepted

Context

As the AssetTrak UI grows in complexity, we need reliable end-to-end (E2E) tests that can validate page-level behavior without being tightly coupled to implementation details. Our initial E2E test suite exposed several recurring problems:

  1. Brittle selectors: Tests relied on CSS class names, text content, or DOM structure that changed frequently with UI updates, causing cascading test failures unrelated to actual regressions.
  2. No stable anchor points: Components lacked consistent data-testid attributes, forcing tests to use fragile .or() chains combining role-based selectors with structural fallbacks.
  3. Inconsistent naming: The few data-testid values that existed used ad-hoc names with no predictable convention, making them hard to discover and maintain.
  4. No centralized selector registry: Test IDs were scattered as raw strings across spec files, leading to typos, duplication, and no autocomplete support.
  5. Missing test data strategy: Tests assumed pre-existing database state, causing failures in clean environments and coupling tests to manual setup.

We evaluated several approaches:

  • CSS class selectors: Fragile; classes serve styling purposes and change with design updates.
  • Role-based selectors only: Ideal for accessibility-focused testing but insufficient for custom components, data containers, and state indicators that lack semantic ARIA roles.
  • data-testid only: Stable and decoupled from styling/structure but less accessible-aware.
  • Hybrid approach: Role-based selectors as the primary strategy with data-testid as stable fallback for elements that lack semantic roles.

Decision

We adopt a layered E2E test infrastructure with four conventions:

1. Selector Strategy: Role-First with TestID Fallback

  • Primary: Use Playwright's role-based locators (getByRole, getByLabel, getByText) for elements with clear ARIA semantics (buttons, links, headings, inputs, tabs).
  • Fallback: Use data-testid attributes for elements that lack semantic roles or need stable anchors independent of content (tables, lists, cards, state indicators, custom containers).
  • Never: Select by CSS class name, inline style, or DOM structure position.

2. data-testid Naming Convention

All data-testid values follow a dot-notation hierarchy:

{page}.{section}-{element}
  • page: The route segment name, lowercase (e.g., workflows, assets, item-types).
  • section: The logical area of the page, lowercase with hyphens (e.g., table, form, header, filters).
  • element: The specific interactive or data element, lowercase with hyphens (e.g., search-input, create-button, name-column).

Examples:

workflows.table-container      -- the main data table wrapper
workflows.header-create-button -- the "New Workflow" button
assets.form-serial-input       -- serial number input on asset form
locations.tree-container       -- the location tree wrapper
locations.tree-node            -- individual tree nodes
item-types.filters-category    -- category filter dropdown

Rules:

  • Page prefix matches the route path segment exactly
  • Use hyphens within section and element names, dots only to separate page from section
  • Shared/layout components use app as the page prefix (e.g., app.nav-sidebar, app.header-user-menu)
  • Modal/dialog containers include the triggering context (e.g., workflows.delete-confirm-dialog)

3. Centralized Selectors File

All data-testid string constants live in a single TypeScript file:

test/e2e/selectors.ts

Structure:

export const selectors = {
  workflows: {
    table: {
      container: '[data-testid="workflows.table-container"]',
      row: '[data-testid="workflows.table-row"]',
      nameColumn: '[data-testid="workflows.table-name-column"]',
    },
    header: {
      createButton: '[data-testid="workflows.header-create-button"]',
    },
  },
  assets: {
    // ...
  },
} as const;

Benefits:

  • TypeScript autocomplete prevents typos
  • Single source of truth for all test IDs
  • Easy to audit coverage
  • Refactoring a test ID updates all references

4. Test Data Strategy

Tests use a layered fixture approach built on Playwright's fixture system:

  • Worker-scoped fixtures: API clients (Kiota-generated) initialized once per worker, used to seed and tear down test data via backend APIs.
  • Test-scoped fixtures: Individual test data created before each test and cleaned up after, ensuring test isolation.
  • Data-aware helpers: Utility functions that probe for existing data and skip tests gracefully when required data is missing (e.g., waitForAssetsList).
  • Console error filtering: Shared ignore list for known acceptable noise (next-auth session checks, ResizeObserver, favicon).

Test data is seeded through the same Kiota API clients that the UI consumes, ensuring data validity and exercising the full API path.

Consequences

Positive

  • Tests are decoupled from CSS and DOM structure changes
  • Consistent naming makes test IDs predictable and discoverable
  • Centralized selectors file prevents typos and enables refactoring
  • Role-first approach maintains accessibility awareness
  • Test data fixtures enable running tests in clean environments
  • New pages get test coverage faster by following established patterns

Negative

  • Every interactive UI element needs a data-testid attribute added, which is a one-time migration cost
  • Developers must maintain the selectors file when adding new testable elements
  • The data-testid attributes add small amounts of markup to production HTML (negligible impact, can be stripped in production builds if needed)

Neutral

  • Existing tests that use .or() fallback patterns will continue to work during migration
  • The convention is compatible with future component library extraction