Skip to content

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, polyfills matchMedia, ResizeObserver, scrollIntoView, and pointer capture so shadcn primitives work under happy-dom.
  • tests/utils/render-with-theme.tsx — RTL render wrapped 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:

  1. Renders without crashing.
  2. Has the expected accessible name and/or role.
  3. Key interactive behaviour works (click / keyboard / state change).
  4. 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:coverage from apps/lms (or npx turbo run test:coverage --filter=@open-learning-hub/lms from the repo root).
  • Reports land in apps/lms/coverage/ (coverage-summary.json for baselines, index.html for drill-down). CI uploads the same tree as the vitest-coverage artifact. Thresholds are enforced in apps/lms/vitest.config.ts via global and path-specific gates.

Server mutation tests (T-005)

  • Layout: tests/unit/app/** mirrors src/app/** server actions; tests/unit/auth/actions/** covers src/auth/actions/*; API route handlers use co-located route.test.ts under src/app/api/**.
  • Harness: tests/unit/mutations/_harness.ts and assertions.tsdefaultMockSessionUser / buildMockSession (includes NextAuth id + userId), memory rate-limit driver setup (useMemoryRateLimitDriver), assertActionFailure / expectApiError / assertNotNull aligned with T-003 ERROR_CODES. Assign fixed-arity mocks (e.g. mintProof) directly as vi.fn, not unknown[] spread wrappers.
  • Nullable fixtures: prefer module-level tenantId from insertTenant in beforeEach; use assertNotNull for nullable query rows — avoid ! (Biome noNonNullAssertion).
  • 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-limit in tests that assert RATE_LIMITED — use useMemoryRateLimitDriver() 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 ConfirmDialog test IDs with a stable base and generated -cancel / -confirm suffixes.

Reference implementation and policy: apps/cms/docs/reference/menu_actions.md.