data-testid Policy (Canonical)¶
This is the single normative rule for data-testid usage in the Open Learning Hub monorepo. App-specific docs link here instead of restating policy.
Purpose¶
Stable data-testid values let Playwright and Testing Library target components and containers without brittle CSS or text selectors. User-visible copy stays in i18n; never use translation strings as test hooks.
Scope¶
- LMS (
apps/lms) and CMS (apps/cms) custom UI undersrc/components/**andsrc/app/**. - Shared package
@open-learning-hub/ui: primitives use shadcndata-slot; composites that accepttestId/dataTestIdare documented below. - Widget renderers (
packages/widget-renderers):widget-{type}-*prefixes per 130-assessment-widgets.mdc.
Does not replace 055-data-listing-patterns.mdc (table/search/pagination hooks) or 058-search-input-ux-policy.mdc (search aria-label / placeholder).
Three layers (every user-facing surface)¶
| Layer | Required ID | Example |
|---|---|---|
| Page / section shell | {area}-page or documented alias |
admin-tenants-page, learn-dashboard |
| Primary container | {area}-{feature} |
admin-users-table, workarea-preview |
| Interactive control | {area}-{control} or row/menu patterns |
sign-in-submit, admin-user-row-actions-{id} |
Thin page.tsx files may omit a shell ID when a child component owns the full contract (e.g. TenantsManager → admin-tenants-page).
Naming¶
- kebab-case, coarse → fine:
site-header,main-nav-link-courses,theme-option-dark. - Dynamic suffix for lists:
admin-user-row-actions-{id},learn-dashboard-enrollment-{slug}. - Area prefixes (CMS authoring; LMS may use the same where helpful):
| Prefix | Use |
|---|---|
shared- |
Cross-app chrome (header, theme toggle) |
auth-{form}- |
Auth forms (auth-login-form-email) |
workarea- / sidebar- / editor- |
CMS workarea |
admin-{entity}-{form\|list\|row}- |
Admin CRUD |
widget-{type}- |
Widget internals |
Row menus: apps/cms/docs/reference/menu_actions.md — LMS uses the same trigger/item pattern via AdminRowActionsMenu.
Search inputs: apps/cms/docs/reference/searching.md ({surface}-search, {surface}-search-clear).
What must have a test id¶
- All interactive controls:
button/Button,a/Link,input/Input/Textarea,Switch,Select+ trigger/items,DropdownMenuItem, dialog confirm/cancel. - Exported custom component roots get one stable id or documented delegation to a child that owns the contract.
- ConfirmDialog (
@open-learning-hub/ui/confirm-dialog): passtestId; cancel/confirm become${testId}-cancel/${testId}-confirm.
Exemptions (do not flag in audit)¶
| Category | Examples |
|---|---|
@open-learning-hub/ui / app components/ui/* primitives |
Use wrapper testids or data-slot |
| Providers / adapters | ThemeProvider, session-provider, widget-host-adapter |
| Pure presentational icons | GateStateIcon, decorative SVGs |
| Thin server wrappers | MainNav → MainNavView (leaf owns IDs) |
| DnD-only drop zones | Unless e2e targets them |
Prop patterns¶
data-testidon DOM nodes.dataTestId,testId,dataTestIdPrefix,testIdPrefix,rootTestId,triggerTestIdon shared components (forward to DOM or children).
Audit tooling¶
Use npm run audit:testids from the repo root (report-only). Canonical script usage and flags (--json, --write-registry) are documented in Root Scripts Reference.
Not part of blocking npm run check until a future enforcement phase.
Testing¶
- Co-located component tests: when a component exposes a root
data-testid, assertgetByTestIdin apps/lms/.cursor/rules/030-testing.mdc. - E2E: prefer
page.getByTestId(...)for stable hooks.
Future work (out of current scope)¶
- LMS Playwright specs for learner flows (
learn-dashboard, catalog, enroll, course entry). - Promote
audit:testidsto blocking CI innpm run check. - Vitest catalogue for
widget-*parity between CMS editor andpackages/widget-renderers.