Files
2026-05-27 23:29:53 +01:00

153 lines
8.0 KiB
Markdown

# `@suaveplan/testing` — Surface Inventory
Preservation contract. Every name below is reproduced unchanged in shape by the genesis rewrite. Signatures gain the optional `{ clock?, rng?, signal? }` options parameter where appropriate (purely additive — zero-config callers are unaffected).
Source: `/mnt/s/web/suaveplan/packages/universal/testing/src/` at branch `main` head `ebff1a14d`.
## Root barrel (`@suaveplan/testing`)
### Runner primitives (via conditional exports)
| Name | Kind | Notes |
|---|---|---|
| `afterAll`, `afterEach`, `beforeAll`, `beforeEach` | function | Lifecycle hooks |
| `describe` | function | Group tests |
| `it`, `test` | function | Declare a test |
| `expect` | function | Assertion entry |
| `vi` | namespace | Vitest mock namespace (vitest path only) |
| `fn` | `<TArgs extends unknown[], TReturn>(implementation?: (...args: TArgs) => TReturn) => Mock` | Runner-agnostic mock fn |
| `mock` | `(modulePath: string, factory?: () => unknown) => { restore(): void }` | Module mock (vitest path only) |
| `spy` | `<T, M extends keyof T>(obj: T, method: M & string)` | Method spy |
| `stub` | `<T, M extends keyof T>(obj: T, method: M & string, impl?: (...args: unknown[]) => unknown)` | Method stub with default implementation |
| `fakeClock` | `() => { advance(ms: number): void; restore(): void }` | Vitest fake-timer wrapper |
| `restoreMocks` | `() => void` | |
| `restoreTimers` | `() => void` | |
Routing: `package.json` conditional exports pick vitest (`default`), bun-test (`bun` condition), or the vitest barrel (`vitest` condition) per the archived `2026-04-08-testing-libraries` change.
### Builders
| Name | Kind |
|---|---|
| `RequestBuilder` | class |
| `ResponseBuilder` | class |
| `get(url: string): RequestBuilder` | function (preset) |
| `post(url: string, body?: unknown): RequestBuilder` | function (preset) |
| `put(url: string, body?: unknown): RequestBuilder` | function (preset) |
| `patch(url: string, body?: unknown): RequestBuilder` | function (preset) |
| `del(url: string): RequestBuilder` | function (preset, `delete` keyword avoidance) |
| `ok(body?: unknown): ResponseBuilder` | function (preset 200) |
| `created(body?: unknown): ResponseBuilder` | function (preset 201) |
| `noContent(): ResponseBuilder` | function (preset 204) |
| `badRequest(body?: unknown): ResponseBuilder` | function (preset 400) |
| `unauthorized(body?: unknown): ResponseBuilder` | function (preset 401) |
| `notFound(body?: unknown): ResponseBuilder` | function (preset 404) |
| `serverError(body?: unknown): ResponseBuilder` | function (preset 500) |
### Clock
| Name | Kind |
|---|---|
| `FakeClock` | class |
### Factories
| Name | Kind |
|---|---|
| `defineFactory<T>(definition: FactoryDefinition<T>): Factory<T>` | function |
| `Factory<T>` | class |
### Fixtures
| Name | Kind |
|---|---|
| `FixtureManager` | class |
### Storage
| Name | Kind |
|---|---|
| `MemoryStore` | class |
### Helpers
| Name | Kind |
|---|---|
| `assertThrows(fn: () => void, matcher?: string \| RegExp \| ErrorConstructor): Error` | function |
| `assertThrowsAsync(fn: () => Promise<unknown>, matcher?: string \| RegExp \| ErrorConstructor): Promise<Error>` | function |
| `createCollector<T>(): { push(value: T): void; items(): readonly T[]; clear(): void }` | function |
| `deferred<T>(): { promise: Promise<T>; resolve(value: T): void; reject(reason: unknown): void }` | function |
| `measureTime<T>(fn: () => T \| Promise<T>): Promise<{ result: T; elapsedMs: number }>` | function |
| `resetTestIdCounter(): void` | function |
| `sleep(ms: number): Promise<void>` | function |
| `uniqueTestId(prefix?: string): string` | function |
| `waitFor(predicate: () => boolean \| Promise<boolean>, options?: WaitForOptions): Promise<void>` | function |
### Constants
| Name | Kind | Source |
|---|---|---|
| `DEFAULT_CLOCK_OPTIONS` | `Required<FakeClockOptions>` | `src/constants/` |
| `DEFAULT_HTTP_STATUS` | `number` (200) | |
| `DEFAULT_RESPONSE_LATENCY_MS` | `number` (0) | |
| `DEFAULT_STORE_OPTIONS` | `Required<MemoryStoreOptions>` | |
| `DEFAULT_WAIT_OPTIONS` | `Required<WaitForOptions>` | |
| `MAX_FIXTURES` | `number` (100) | |
| `MAX_STORE_ENTRIES` | `number` (1,000,000) | |
| `MAX_TIMERS` | `number` (10,000) | |
### Public types
| Name | Kind | Definition location |
|---|---|---|
| `HttpMethod` | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE" \| "HEAD" \| "OPTIONS"` | `src/types/testing.types.ts` |
| `HttpHeaders` | `Record<string, string>` | |
| `QueryParams` | `Record<string, string \| string[]>` | |
| `TestRequest` | interface: `{ method, url, headers, query, body, params }` | |
| `TestResponse` | interface: `{ status, headers, body, latencyMs }` | |
| `FakeClockOptions` | interface: `{ startTime?, autoAdvance?, autoAdvanceMs? }` | |
| `ScheduledTimer` | interface: `{ id, callback, fireAt, interval, type }` | |
| `MemoryStoreOptions` | interface: `{ maxEntries?, defaultTtlMs? }` | |
| `StoreEntry<T>` | interface: `{ value: T, createdAt, expiresAt }` | |
| `FactoryDefinition<T>` | interface: `{ defaults, traits?, afterCreate? }` | |
| `FactoryDefaults<T>` | mapped type | |
| `FactoryBuildOptions<T>` | interface: `{ overrides?, traits?, index? }` | |
| `FixtureDefinition<T>` | interface: `{ name, setup, teardown?, dependencies? }` | |
| `FixtureState<T>` | interface: `{ name, value, isSetup, setupCount, teardownCount }` | |
| `WaitForOptions` | interface: `{ timeoutMs?, intervalMs? }` | |
| `ClockSnapshot` | feature-local | `src/clock/fake-clock/fake-clock.types.ts` |
| `RequestBuilderPreset`, `RequestBuilderState` | feature-local | `src/builders/request-builder/` |
| `ResponseBuilderPreset`, `ResponseBuilderState` | feature-local | `src/builders/response-builder/` |
| `BatchBuildResult` | feature-local | `src/factories/factory/` |
| `SetupAllResult`, `TeardownAllResult` | feature-local | `src/fixtures/fixture-manager/` |
| `MemoryStoreEvent`, `MemoryStoreStats` | feature-local | `src/storage/memory-store/` |
## Subpath `./config` (`@suaveplan/testing/config`)
| Name | Kind |
|---|---|
| `createTestConfig(options?: TestConfigOptions): Promise<VitestUserConfig \| BunTestConfig>` | async factory |
| `TestConfigOptions` | interface extending `VitestConfigOverrides` with `runner?: TestRunner` |
| `TestRunner` | `"vitest" \| "bun:test"` |
| `TestEnvironment` | `"node" \| "happy-dom" \| "jsdom"` |
| `VitestConfigOverrides` | interface (passes through to vitest's config) |
| `BunTestConfig` | interface (minimal bun-test config shape) |
| `BunTestConfigOverrides` | interface (partial of the above) |
## Subpath `./utils` (`@suaveplan/testing/utils`)
Re-exports every non-runner symbol above for single-import convenience (`import { FakeClock, Factory, … } from "@suaveplan/testing/utils"`).
## Subpath `./polyfills` (`@suaveplan/testing/polyfills`)
Side-effecting module (`import "@suaveplan/testing/polyfills";`) loaded by `setupFiles` in consumers' vitest configs. Installs happy-dom-needed globals (e.g. `crypto.getRandomValues` polyfill, whatever else surfaces as needed on WSL2).
## Non-obvious constraints (from the archived 2026-04-08-testing-libraries change)
- **Dual-runner exports are load-bearing.** Swapping a package between vitest and bun-test is a single condition change with no test-source rewrite. The vitest path and the bun-test path expose the same names.
- **`@vitejs/plugin-react` is an optional peer dep.** `createTestConfig` dynamically imports it only when the caller requests `environment: "happy-dom"` AND the peer is installed. Missing peer must NOT break non-React consumers.
- **`pool: "forks"` everywhere.** `vmThreads` and `threads` both fail in Bun on WSL2. Both the vitest config factory and any direct config consumers must force `forks`.
- **`fileParallelism: false`** required for the happy-dom path (20+ concurrent forks time out on WSL2).
- **Polyfills import is a side-effect module**, loaded via `setupFiles: ["@suaveplan/testing/polyfills"]`. Must export nothing; merely installing globals.
- **`mocks.ts` uses vitest's `vi` directly.** On the bun-test path, the `mocks.ts` shim is replaced by bun-test equivalents. Callers see the same function names either way.