frontend-testing · git:20260916.b334104 · 2026-09-16 · sha256 9ffac160afed856d

frontend-testing git:20260916.b334104A

Immutable. This exact content is served forever at /api/v1/blob/9ffac160afed856d.

---
name: frontend-testing
description: Frontend Vitest browser-mode tests, Testing Library queries, mockSdkService, makeMe builders, deterministic Vue component testing. Use when writing or changing frontend Vitest tests.
paths:
  - "frontend/tests/**/*.ts"
  - "frontend/src/**/*.spec.ts"
---
# Frontend Testing Rules

**Style ("small test" practice — stable boundary, data over mocks, focused assertions, concise makeMe):** the `unit-testing` skill — follow that first.

## Test Commands

From the repo root:

```bash
CURSOR_DEV=true nix develop -c pnpm frontend:test
```

Run tests with browser rendering UI:

```bash
CURSOR_DEV=true nix develop -c pnpm frontend:test:ui
```

Run tests in browser-rendered watch mode:

```bash
CURSOR_DEV=true nix develop -c pnpm frontend:test:watch
```

Run a single test file from the repo root:

```bash
CURSOR_DEV=true nix develop -c pnpm frontend:test tests/path/to/TestFile.spec.ts
```

Or only the frontend package:

```bash
CURSOR_DEV=true nix develop -c pnpm -C frontend test tests/path/to/TestFile.spec.ts
```

Run a specific test case:

```bash
CURSOR_DEV=true nix develop -c pnpm -C frontend test -t "test name pattern"
```

The test file path is relative to `frontend/`. Do not use `pnpm ... test -- tests/...`; the `--` is forwarded to Vitest and file filtering is skipped.

In most situations, run all unit tests instead of a selected file only. Use a single test file when actively debugging or iterating on a specific component.

For `dough-test-optimization`, `frontend:test` remains the ordinary local
feedback measurement because it runs Chromium browser mode. When per-test JSON
durations are needed for investigation, use
`CURSOR_DEV=true nix develop -c pnpm -C frontend exec vitest run --reporter=json`.
That plain Vitest run is profiling evidence only; verify retained changes with
the normal browser-mode command and do not compare its wall time directly with
`frontend:test`.

## Component Behavior

- Drive the mounted component / page ("small test" style per `unit-testing` skill); cover lower layers with realistic `makeMe` props/state, not by testing internal helpers in isolation.
- Test through user interactions; assert observable DOM outcomes.
- Use `data-testid` for test selectors.
- Use Vitest browser mode and prefer real browser rendering over mocking sibling components or internal modules; stop using jsdom.

## Routing assertions

Navigation assertions use named locations (or helpers that return them).
Rendered-href assertions use `noteShowHref`. Path strings belong only in
`routes.spec.ts` (matching / redirects) and inbound URL classifiers. Test
routers that resolve named screen locations use production `routes` or
`dummyRouteRecordsFromMetadata` (the `routeMetadata` table with dummy
components; no page imports). Catch-all `/` or `/:pathMatch(.*)*` routers
and `useRoute` stubs with `path: "/"` are not a second screen dialect
(ADR 0005).

## Avoid Role Queries

- Do not use `getByRole`, `findByRole`, `queryByRole`, `getAllByRole`, and similar queries; they are slow due to expensive visibility checks.
- Testing Library recommends role queries for accessibility, but this project prioritizes test performance.
- Use faster alternatives: `getByText`, `getByLabelText`, `getByTitle`, or `querySelector` / `querySelectorAll`.

## Mock SDK Services

The backend HTTP API is an **external** dependency from the frontend’s perspective (`unit-testing` skill mocking exception).

- Use `mockSdkService` from `@tests/helpers` for type-safe mocking: pass the generated **controller class** and the **method name** (same static methods as `@generated/donut-backend-api/sdk.gen`).
- It automatically wraps responses in the standard format `{ data, error, request, response }`.
- It returns a spy that can be reconfigured in tests.
- Use `wrapSdkResponse` when updating mock return values.
- Use `mockSdkServiceWithImplementation` only for custom async logic based on options.
- Build response payloads with `makeMe`; do not mock in-process collaborators to “reach” coverage.

```typescript
import { NoteController } from "@generated/donut-backend-api/sdk.gen"
import { mockSdkService } from "@tests/helpers"

beforeEach(() => {
  mockSdkService(NoteController, "getRecentNotes", [])
  mockSdkService(NoteController, "showNote", makeMe.aNoteRealm.please())
})
```

```typescript
import { NoteController } from "@generated/donut-backend-api/sdk.gen"
import { mockSdkService, wrapSdkResponse } from "@tests/helpers"

const spy = mockSdkService(NoteController, "showNote", makeMe.aNoteRealm.please())
spy.mockResolvedValue(wrapSdkResponse(differentNote))
```

```typescript
import { TextContentController } from "@generated/donut-backend-api/sdk.gen"
import { mockSdkServiceWithImplementation } from "@tests/helpers"

mockSdkServiceWithImplementation(TextContentController, "updateNoteContent", async (options) => {
  return await someAsyncOperation(options)
})
```

## Component Props

- Use `helper.component(ComponentName).withStorageProps` if the component requires a `storageAccessor` prop.
- Use `withProps` instead of `withStorageProps` if the component does not require `storageAccessor`.
- Test prop changes and their effects.

```typescript
const wrapper = helper
  .component(Component)
  .withStorageProps({ value: initialValue })
  .mount()

await wrapper.setProps({ value: newValue })
```

## Data Builders

- Use `makeMe` for API-shaped test data; implementation lives in `packages/donut-test-fixtures`.
- Import `donut-test-fixtures/makeMe` only. Do not import the bare package name or deep paths into `src/`.
- Concise setup, defaults, and extending builders: `unit-testing` skill.

```typescript
const note = makeMe.aNoteRealm.title("Dummy Title").content("Description").please()
const answered = makeMe.anAnsweredQuestion
  .accidentalMatch("alias", [note.note.noteTopology])
  .please()
```

## Browser Mode Mounting

- Use `render()` from `@testing-library/vue` for most tests.
- `render()` encourages user-perspective tests and returns fast queries such as `getByText`, `getByLabelText`, and `data-testid`.
- Use `mount()` from `@vue/test-utils` only when you need direct access to Vue internals, emitted events, slots, or provide/inject edge cases.
- Query the DOM, not Vue components.

Prefer:

```typescript
page.getByText(/submit/i)
screen.getByText("Loading...")
```

Avoid:

```typescript
wrapper.findComponent(MyButton)
wrapper.find("[data-testid='my-component']")
```

## Deterministic Tests

- Tests must always execute the same way.
- Use assertions instead of if-conditions so failures are clear.
- Use sequential async operations instead of loops where possible.

Avoid:

```typescript
if (vm.searchResults) {
  const selected = vm.searchResults.find((result) => result.id === wikidataId)
  if (selected) {
    // do something
  }
}
```

Prefer:

```typescript
expect(vm.searchResults).toBeDefined()
expect(vm.searchResults.length).toBeGreaterThan(0)
const selected = vm.searchResults.find((result) => result.id === wikidataId)
expect(selected).toBeDefined()
```