# `@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` | `(implementation?: (...args: TArgs) => TReturn) => Mock` | Runner-agnostic mock fn | | `mock` | `(modulePath: string, factory?: () => unknown) => { restore(): void }` | Module mock (vitest path only) | | `spy` | `(obj: T, method: M & string)` | Method spy | | `stub` | `(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(definition: FactoryDefinition): Factory` | function | | `Factory` | 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, matcher?: string \| RegExp \| ErrorConstructor): Promise` | function | | `createCollector(): { push(value: T): void; items(): readonly T[]; clear(): void }` | function | | `deferred(): { promise: Promise; resolve(value: T): void; reject(reason: unknown): void }` | function | | `measureTime(fn: () => T \| Promise): Promise<{ result: T; elapsedMs: number }>` | function | | `resetTestIdCounter(): void` | function | | `sleep(ms: number): Promise` | function | | `uniqueTestId(prefix?: string): string` | function | | `waitFor(predicate: () => boolean \| Promise, options?: WaitForOptions): Promise` | function | ### Constants | Name | Kind | Source | |---|---|---| | `DEFAULT_CLOCK_OPTIONS` | `Required` | `src/constants/` | | `DEFAULT_HTTP_STATUS` | `number` (200) | | | `DEFAULT_RESPONSE_LATENCY_MS` | `number` (0) | | | `DEFAULT_STORE_OPTIONS` | `Required` | | | `DEFAULT_WAIT_OPTIONS` | `Required` | | | `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` | | | `QueryParams` | `Record` | | | `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` | interface: `{ value: T, createdAt, expiresAt }` | | | `FactoryDefinition` | interface: `{ defaults, traits?, afterCreate? }` | | | `FactoryDefaults` | mapped type | | | `FactoryBuildOptions` | interface: `{ overrides?, traits?, index? }` | | | `FixtureDefinition` | interface: `{ name, setup, teardown?, dependencies? }` | | | `FixtureState` | 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` | 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.