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:
- 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.
- No stable anchor points: Components lacked consistent
data-testidattributes, forcing tests to use fragile.or()chains combining role-based selectors with structural fallbacks. - Inconsistent naming: The few
data-testidvalues that existed used ad-hoc names with no predictable convention, making them hard to discover and maintain. - No centralized selector registry: Test IDs were scattered as raw strings across spec files, leading to typos, duplication, and no autocomplete support.
- 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-testidonly: Stable and decoupled from styling/structure but less accessible-aware.- Hybrid approach: Role-based selectors as the primary strategy with
data-testidas 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-testidattributes 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
appas 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-testidattribute added, which is a one-time migration cost - Developers must maintain the selectors file when adding new testable elements
- The
data-testidattributes 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