90 lines
5.8 KiB
Markdown
90 lines
5.8 KiB
Markdown
# Capability Spec Delta: testing-dom (testing-dom-runner-and-matchers-hardening)
|
|
|
|
**Package**: `@suaveplan/testing-dom`
|
|
**Status**: PROPOSED
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: Runner-neutral `RunnerMock<TArgs, TReturn>` type alias
|
|
|
|
The package SHALL export a `RunnerMock<TArgs extends unknown[], TReturn>` type alias from both runner-conditional barrels (`./runner/vitest.js` and `./runner/bun-test.js`) AND from the package barrel (`./index.js`). Both runner paths SHALL implement `fn<TArgs, TReturn>(impl?)` returning `RunnerMock<TArgs, TReturn>` so consumer code typed as `const m = fn<[string], number>()` resolves to a callable `(arg: string) => number` regardless of which runner is active.
|
|
|
|
#### Scenario: vitest path preserves call-signature typing
|
|
|
|
- **WHEN** a consumer running under vitest declares `const m = fn<[string], number>()`
|
|
- **THEN** `m("hello")` type-checks AND `ReturnType<typeof m>` equals `number`
|
|
|
|
#### Scenario: bun-test path mirrors the vitest typing surface
|
|
|
|
- **WHEN** the same declaration runs under the `bun` conditional export
|
|
- **THEN** the imported `fn` returns a `RunnerMock<TArgs, TReturn>` carrying the same call signature
|
|
|
|
#### Scenario: SpyHandle type alias re-exported
|
|
|
|
- **WHEN** a consumer imports `SpyHandle` from `@suaveplan/testing-dom`
|
|
- **THEN** the type matches `ReturnType<typeof spy>` on the active runner path
|
|
|
|
**Verified by:** `src/runner/runner.spec-conformance.test.ts::"fn<[string], number>() preserves call-signature typing"` AND `src/runner/runner.spec-conformance.test.ts::"fn<[number, number], number>(impl) preserves call-signature typing"` AND `src/runner/runner.spec-conformance.test.ts::"BunRunnerMock<TArgs, TReturn> shape mirrors the vitest path"` AND `src/runner/runner.spec-conformance.test.ts::"BunSpyHandle is exported and PkgRunnerMock matches both paths"` AND `src/runner/runner.spec-conformance.test.ts::"spy returns a SpyHandle assignable to the package's exported type"` AND `src/runner/runner.spec-conformance.test.ts::"stub returns a SpyHandle assignable to the package's exported type"`
|
|
|
|
---
|
|
|
|
### Requirement: jest-dom matchers ship ambient type augmentation
|
|
|
|
The `@suaveplan/testing-dom/matchers` side-effect import SHALL bring the `@testing-library/jest-dom` ambient module declaration into the consumer's compilation unit, so `expect(el).toBeInTheDocument()` and the other common jest-dom matchers type-check without the consumer adding `@testing-library/jest-dom` to their own `tsconfig.json#compilerOptions.types`.
|
|
|
|
#### Scenario: matcher names are typed as functions
|
|
|
|
- **WHEN** a test file does `import "@suaveplan/testing-dom/matchers"` and writes `expect(node).toBeInTheDocument`
|
|
- **THEN** the expression has type `() => void` (a callable matcher) under `tsc --noEmit`
|
|
|
|
**Verified by:** `src/matchers/matchers.spec-conformance.test.tsx::"expect(element).toBeInTheDocument is typed as a function"` AND `src/matchers/matchers.spec-conformance.test.tsx::"every common matcher is typed as a function on expect(...)"`
|
|
|
|
---
|
|
|
|
### Requirement: `setupUser` resolves `vi` through the runner adapter
|
|
|
|
`src/user/setup-user.ts` SHALL import `vi` from the runner adapter (`./runner/index.js`) rather than directly from `vitest`. This preserves the dual-runner contract: when the `bun` conditional export is active, the runner adapter resolves to the bun-test path's `vi` alias backed by `bun:test`'s `jest` namespace.
|
|
|
|
#### Scenario: source-level guard against vitest hard-import
|
|
|
|
- **WHEN** `src/user/setup-user.ts` is read
|
|
- **THEN** the file contains no `from "vitest"` import AND it imports from `../runner/index.js`
|
|
|
|
**Verified by:** `src/user/setup-user.test.ts::"imports vi via the runner adapter (no direct vitest hard-import)"`
|
|
|
|
---
|
|
|
|
### Requirement: `instrumented()` ends spans only after async fns settle
|
|
|
|
`instrumented(name, fn)` SHALL detect when the wrapped function returns a PromiseLike value and SHALL defer `span.end()` and the duration histogram record until the returned promise settles. On rejection, the span SHALL `recordException` (wrapping non-Error rejections in `new Error(String(reason))`) before ending. On resolution, the duration recorded SHALL reflect the wall-clock interval between invocation and settlement.
|
|
|
|
#### Scenario: async resolve
|
|
|
|
- **WHEN** the wrapped fn awaits ≥10ms before returning
|
|
- **THEN** the awaited result equals the inner return value AND the elapsed wall-clock time is ≥10ms (proving the wrapper did not synchronously close the span)
|
|
|
|
#### Scenario: async reject
|
|
|
|
- **WHEN** the wrapped async fn throws an `Error`
|
|
- **THEN** the returned promise rejects with the same error (no swallowing)
|
|
|
|
#### Scenario: async reject with non-Error reason
|
|
|
|
- **WHEN** the wrapped async fn rejects with a string
|
|
- **THEN** the returned promise rejects with the same string AND `recordException` is invoked with a wrapped `Error`
|
|
|
|
**Verified by:** `src/observability/instrumented.test.ts::"ends span only after async fn settles (resolved)"` AND `src/observability/instrumented.test.ts::"ends span only after async fn settles (rejected)"` AND `src/observability/instrumented.test.ts::"ends span only after async fn settles (rejected with non-Error)"`
|
|
|
|
## MODIFIED Requirements
|
|
|
|
### Requirement: Coverage and docs gates
|
|
|
|
Istanbul statement + branch + function + line coverage across `packages/dev/testing-dom/src/` (excluding `*.bench.ts`, `*.schemas.ts`, `runner/bun-test.ts`) MUST be 100% on every dimension. Every implementation file (`*.ts` other than test/bench/schemas/types-in-feature-dirs/index barrels) MUST have a sibling `.md` of ≥ 200 words with simple + advanced examples.
|
|
|
|
#### Scenario: CI coverage gate
|
|
|
|
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/testing-dom` runs
|
|
- **THEN** statement, branch, function, and line coverage all = 100%
|
|
|
|
**Verified by:** CI gate (`bunx turbo test:coverage --filter=@suaveplan/testing-dom` exits non-zero on regression)
|