Testing conventions¶
Layout¶
tests/unit/**/*.test.{ts,tsx}— non-component unit tests (lib helpers, schemas, db queries).tests/e2e/**— Playwright (Phase 3+).tests/setup.ts— global setup; loaded by Vitest. Adds@testing-library/jest-dom, polyfillsmatchMedia,ResizeObserver,scrollIntoView, and pointer capture so shadcn primitives work under happy-dom.tests/utils/render-with-theme.tsx— RTLrenderwrapped with<NextIntlClientProvider>and<ThemeProvider>. Import via@tests/utils/render-with-theme. Accepts{ theme: 'light' | 'dark' | 'system', locale, messages }.
Per-component tests¶
Per the implementation plan's guiding principle, every custom component under src/components/** (excluding primitives imported from @open-learning-hub/ui, which are upstream-tested in the shared package) ships with a co-located test beside its source file. Filename pairing follows 031-file-naming.mdc: kebab-case.tsx with kebab-case.test.tsx. Minimum assertions:
- Renders without crashing.
- Has the expected accessible name and/or role.
- Key interactive behaviour works (click / keyboard / state change).
- Renders in both light and dark themes via
renderWithTheme(ui, { theme: 'dark' })for at least one assertion.
Run all component tests with npm run test:components.
Coverage¶
- Full suite with coverage:
npm run test:coveragefromapps/lms(ornpx turbo run test:coverage --filter=@open-learning-hub/lmsfrom the repo root). - Reports land in
apps/lms/coverage/(coverage-summary.jsonfor baselines,index.htmlfor drill-down). CI uploads the same tree as thevitest-coverageartifact. Thresholds are enforced inapps/lms/vitest.config.tsvia global and path-specific gates.
Server mutation tests (T-005)¶
- Layout:
tests/unit/app/**mirrorssrc/app/**server actions;tests/unit/auth/actions/**coverssrc/auth/actions/*; API route handlers use co-locatedroute.test.tsundersrc/app/api/**. - Harness:
tests/unit/mutations/_harness.tsandassertions.ts—defaultMockSessionUser/buildMockSession(includes NextAuthid+userId), memory rate-limit driver setup (useMemoryRateLimitDriver),assertActionFailure/expectApiError/assertNotNullaligned with T-003ERROR_CODES. Assign fixed-arity mocks (e.g.mintProof) directly asvi.fn, notunknown[]spread wrappers. - Nullable fixtures: prefer module-level
tenantIdfrominsertTenantinbeforeEach; useassertNotNullfor nullable query rows — avoid!(BiomenoNonNullAssertion). - DB:
createTestDb+tests/unit/db/fixtures.ts(insertTenant,insertUser,insertEnrollment,insertInvitation, …). - Mocks per file:
vi.mock("@/db/client")with test DB;vi.mock("@/auth/guards")for session/role;vi.mock("@/lib/email/client")for outbound mail;vi.mock("next/cache")when actions revalidate. Do not mock@/lib/rate-limitin tests that assertRATE_LIMITED— useuseMemoryRateLimitDriver()and exhaust the key instead. - Route exceptions (documented in T-005): public site asset proxies (no auth denial);
test/oauth/google(non-production); NextAuth handler (export smoke only).
data-testid rule¶
Canonical policy: 059-data-testid-policy.mdc. Also LMS AGENTS.md.
Every custom component and every interactive sub-element gets a data-testid so Playwright (and component tests) can target it unambiguously. Primitives from @open-learning-hub/ui are exempt — they expose shadcn's data-slot.
Naming: kebab-case, scope from coarsest to finest. Examples: site-header, main-nav-link-courses, theme-option-dark, locale-option-es.
Co-located component tests: when the component (or its default root) exposes a stable data-testid, assert expect(screen.getByTestId("…")).toBeInTheDocument() in at least one test (in addition to role/name checks). Override ids via props remain testable with the override value.
Inventory: from repo root, npm run audit:testids (report-only; see 059).
Admin row-action test IDs¶
For admin table row action menus, align with the canonical menu-action pattern used across CMS and LMS.
- Trigger button:
admin-{resource}-row-actions-{id} - Menu item:
admin-{resource}-row-{action}-{id} - Keep
{resource}singular where possible (user,tenant,course,student,course-admin). - Keep
{action}explicit (manage,settings,suspend,unsuspend,remove,cancel, etc.). - Destructive confirmations should use
ConfirmDialogtest IDs with a stable base and generated-cancel/-confirmsuffixes.
Reference implementation and policy: apps/cms/docs/reference/menu_actions.md.