Initial Commit
This commit is contained in:
152
openspec/archive/2026-04-19-enterprise-testing/surface.md
Normal file
152
openspec/archive/2026-04-19-enterprise-testing/surface.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# `@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.
|
||||
Reference in New Issue
Block a user