first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
# @suaveplan/testing-dom - DOM/React testing — vitest+bun-test conditional re-exports, createTestConfig, polyfills, render helpers, accessibility-first queries, jest-dom matchers, user-event v14
## Summary
DOM and React component testing utilities for Suaveplan. Provides a single install that covers vitest config generation with WSL2-safe defaults, `@testing-library/react` render helpers, accessibility-first query aliases, `@testing-library/jest-dom` matchers, user-event v14 setup, and a dual-runner shim that routes between vitest and `bun:test` via package.json conditional exports.
<!-- begin:auto-docs -->
## Documentation tree
- [config](./src/config/config.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - detect-environment — `detectEnvironment` probes the…
- [bun-test-config](./src/config/bun-test-config.md) — `createBunTestConfig` produces a minimal `bun test` configuration object with the same polyfill preload and environment…
- [config](./src/config/config.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - detect-environment — `detectEnvironment` probes the…
- [detect-environment](./src/config/detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./src/config/detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./src/config/vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- [codes](./src/errors/codes.md) — `TESTING_DOM_CODES` is the package-local error code registry for `@suaveplan/testing-dom`. All codes share the `TESTING…
- [testing-dom-error](./src/errors/testing-dom-error.md) — `TestingDomError` is the single concrete error class thrown by `@suaveplan/testing-dom`. It intentionally does NOT exte…
- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [instrumented](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [render](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [render-component](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - vitest — The `runner/vitest` module re-exports vite…
- [bun-test](./src/runner/bun-test.md) — The `runner/bun-test` module re-exports bun-test's primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `before…
- [mocks](./src/runner/mocks.md) — The `runner/mocks` module offers a small set of dependency-injection friendly mock helpers that work identically under…
- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - vitest — The `runner/vitest` module re-exports vite…
- [vitest](./src/runner/vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- [setup](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [polyfills](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [setup-user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - detect-environment — `detectEnvironment` probes the…
- [bun-test-config](./src/config/bun-test-config.md) — `createBunTestConfig` produces a minimal `bun test` configuration object with the same polyfill preload and environment…
- [config](./src/config/config.md) — <!-- begin:auto-docs --> _(no submodules)_- detect-environment — `detectEnvironment` probes the…
- [detect-environment](./src/config/detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./src/config/detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./src/config/vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- [codes](./src/errors/codes.md) — `TESTING_DOM_CODES` is the package-local error code registry for `@suaveplan/testing-dom`. All codes share the `TESTING…
- [testing-dom-error](./src/errors/testing-dom-error.md) — `TestingDomError` is the single concrete error class thrown by `@suaveplan/testing-dom`. It intentionally does NOT exte…
- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [instrumented](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [render](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [render-component](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_- vitest — The `runner/vitest` module re-exports vite…
- [bun-test](./src/runner/bun-test.md) — The `runner/bun-test` module re-exports bun-test's primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `before…
- [mocks](./src/runner/mocks.md) — The `runner/mocks` module offers a small set of dependency-injection friendly mock helpers that work identically under…
- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_- vitest — The `runner/vitest` module re-exports vite…
- [vitest](./src/runner/vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- [setup](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [polyfills](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_- [setup-user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_- detect-environment — `detectEnvironment` probes the…
- [bun-test-config](./src/config/bun-test-config.md) — `createBunTestConfig` produces a minimal `bun test` configuration object with the same polyfill preload and environment…
- [config](./src/config/config.md) — <!-- begin:auto-docs --> _(no submodules)_- detect-environment — `detectEnvironment` probes the…
- [detect-environment](./src/config/detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./src/config/detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./src/config/vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- [codes](./src/errors/codes.md) — `TESTING_DOM_CODES` is the package-local error code registry for `@suaveplan/testing-dom`. All codes share the `TESTING…
- [testing-dom-error](./src/errors/testing-dom-error.md) — `TestingDomError` is the single concrete error class thrown by `@suaveplan/testing-dom`. It intentionally does NOT exte…
- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [instrumented](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [render](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [render-component](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_- vitest — The `runner/vitest` module re-exports vite…
- [bun-test](./src/runner/bun-test.md) — The `runner/bun-test` module re-exports bun-test's primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `before…
- [mocks](./src/runner/mocks.md) — The `runner/mocks` module offers a small set of dependency-injection friendly mock helpers that work identically under…
- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_- vitest — The `runner/vitest` module re-exports vite…
- [vitest](./src/runner/vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- [setup](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [polyfills](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_- [setup-user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_- detect-environment — `detectEnvironment` probes the…
- [bun-test-config](./src/config/bun-test-config.md) — `createBunTestConfig` produces a minimal `bun test` configuration object with the same polyfill preload and environment…
- [config](./src/config/config.md) — <!-- begin:auto-docs --> _(no submodules)_- detect-environment — `detectEnvironment` probes the…
- [detect-environment](./src/config/detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./src/config/detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./src/config/vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- [codes](./src/errors/codes.md) — `TESTING_DOM_CODES` is the package-local error code registry for `@suaveplan/testing-dom`. All codes share the `TESTING…
- [testing-dom-error](./src/errors/testing-dom-error.md) — `TestingDomError` is the single concrete error class thrown by `@suaveplan/testing-dom`. It intentionally does NOT exte…
- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [instrumented](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [render](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [render-component](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_- vitest — The `runner/vitest` module re-exports vite…
- [bun-test](./src/runner/bun-test.md) — The `runner/bun-test` module re-exports bun-test's primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `before…
- [mocks](./src/runner/mocks.md) — The `runner/mocks` module offers a small set of dependency-injection friendly mock helpers that work identically under…
- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_- vitest — The `runner/vitest` module re-exports vite…
- [vitest](./src/runner/vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- [setup](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [polyfills](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_- [setup-user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_- [bun-test-config](./src/config/bun-test-config.md) — `createBunTestConfig` produces a minimal `bun test` configuration object with the same polyfill preload and environment…
- [config](./src/config/config.md) — <!-- begin:auto-docs --> _(no submodules)_- [detect-environment](./src/config/detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./src/config/detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./src/config/vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- [codes](./src/errors/codes.md) — `TESTING_DOM_CODES` is the package-local error code registry for `@suaveplan/testing-dom`. All codes share the `TESTING…
- [testing-dom-error](./src/errors/testing-dom-error.md) — `TestingDomError` is the single concrete error class thrown by `@suaveplan/testing-dom`. It intentionally does NOT exte…
- [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_ - [matchers](./src/matchers/matchers.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instrumented](./src/observability/instrumented.md) — <!-- begin:auto-docs --> _(no submodules)_- [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_ - [queries](./src/queries/queries.md) — <!-- begin:auto-docs --> _(no submodules)_- [render](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_ - [render-component](./src/render/render-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bun-test](./src/runner/bun-test.md) — The `runner/bun-test` module re-exports bun-test's primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `before…
- [mocks](./src/runner/mocks.md) — The `runner/mocks` module offers a small set of dependency-injection friendly mock helpers that work identically under…
- [runner](./src/runner/runner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [vitest](./src/runner/vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- [setup](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_ - [polyfills](./src/setup/polyfills.md) — <!-- begin:auto-docs --> _(no submodules)_- [user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_ - [setup-user](./src/user/setup-user.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/testing-dom
```
Peer dependencies (install separately if using React rendering):
```bash
bun add react react-dom @vitejs/plugin-react
```
## Quick Start
### Vitest config
```ts
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { createVitestConfig } from "@suaveplan/testing-dom/config";
import { fileURLToPath } from "node:url";
export default defineConfig(
createVitestConfig(fileURLToPath(new URL(".", import.meta.url)), {
environment: "happy-dom",
coverage: true,
})
);
```
### Rendering a React component
```tsx
import { renderComponent } from "@suaveplan/testing-dom/render";
import { byRole, findRole } from "@suaveplan/testing-dom/queries";
import "@suaveplan/testing-dom/matchers"; // extends expect with jest-dom matchers
it("submits the form", async () => {
const { user } = renderComponent(<MyForm onSubmit={handleSubmit} />);
await user.click(byRole("button", { name: /submit/i }));
const status = await findRole("status");
expect(status).toHaveTextContent("Saved");
});
```
## Modules
| Subpath | Module | Description |
|---|---|---|
| `@suaveplan/testing-dom/config` | [src/config/vitest-config.md](src/config/vitest-config.md) | `createVitestConfig` — builds the vitest `test` block with WSL2-safe `pool: "forks"`, polyfill setup files, and Istanbul coverage defaults |
| | [src/config/bun-test-config.md](src/config/bun-test-config.md) | `createBunTestConfig` — equivalent config builder for `bun test` |
| | [src/config/detect-runner.md](src/config/detect-runner.md) | `detectRunner` — infers the active test runner from `package.json` and environment variables |
| | [src/config/detect-environment.md](src/config/detect-environment.md) | `detectEnvironment` — resolves the vitest test environment (`node`, `happy-dom`, `jsdom`) |
| | [src/config/config.md](src/config/config.md) | `createTestConfig` — unified entry point that delegates to the correct runner-specific builder |
| `@suaveplan/testing-dom/render` | [src/render/render-component.md](src/render/render-component.md) | `renderComponent` — thin wrapper over `@testing-library/react` render that auto-cleans between calls and returns an attached `user` event instance |
| `@suaveplan/testing-dom/queries` | [src/queries/queries.md](src/queries/queries.md) | Accessibility-first query aliases (`byRole`, `findRole`, `queryRole`, `byLabelText`, `byText`, `byPlaceholderText`) plus full re-export of `screen` and `within` |
| `@suaveplan/testing-dom/user` | [src/user/setup-user.md](src/user/setup-user.md) | `setupUser` — produces a `@testing-library/user-event` v14 instance with happy-dom-safe defaults and automatic fake-timer integration |
| `@suaveplan/testing-dom/matchers` | [src/matchers/matchers.md](src/matchers/matchers.md) | Side-effect import that extends vitest's `expect` with the full `@testing-library/jest-dom` matcher set |
| `@suaveplan/testing-dom/polyfills` | [src/setup/polyfills.md](src/setup/polyfills.md) | Setup file that installs web-platform polyfills (`TextEncoder`, `ResizeObserver`, etc.) required by happy-dom in Node/Bun |
| `@suaveplan/testing-dom/errors` | [src/errors/testing-dom-error.md](src/errors/testing-dom-error.md) | Typed error classes (`RenderError`) and error codes for failures surfaced by this package |
| | [src/errors/codes.md](src/errors/codes.md) | `TESTING_DOM_ERROR_CODES` registry |
| _(runner shim)_ | [src/runner/vitest.md](src/runner/vitest.md) | Vitest runner re-exports (`describe`, `it`, `test`, `expect`, `vi`, `beforeEach`, …) |
| | [src/runner/bun-test.md](src/runner/bun-test.md) | `bun:test` runner shim with the same surface — activated by the `"bun"` conditional export |
| | [src/runner/mocks.md](src/runner/mocks.md) | `fn`, `spy`, `stub`, `mock`, `fakeClock`, `restoreMocks`, `restoreTimers` — runner-agnostic mock helpers |
| | [src/runner/runner.md](src/runner/runner.md) | Runner abstraction types |
| _(observability)_ | [src/observability/instrumented.md](src/observability/instrumented.md) | `instrumented` — optional OTel wrapper (gated by `TESTING_DOM_OTEL_ENABLED=1`) that records call counts and duration histograms for package entrypoints |

View File

@@ -0,0 +1,59 @@
# bun-test-config
## Purpose
`createBunTestConfig` produces a minimal `bun test` configuration object with the same polyfill preload and environment defaults as the vitest variant. It exists because `@suaveplan/testing-dom` is a dual-runner package — the same test suite should run under vitest AND under `bun test` without manual config divergence. The shape it returns is intentionally small because `bun test` has a far narrower config surface than vitest.
## Features
- Prepends `@suaveplan/testing-dom/polyfills` to the `preload` array
- Honours the `happy-dom` runtime via a warning (bun-test does NOT support happy-dom natively; tests that depend on DOM must switch runners)
- Pass-through defaults for `coverage` and `testTimeoutMs`
- Stateless — no module-level config caching
## Usage: Basic Example
```ts
// bunfig.toml is usually the entry, but this runner can be invoked directly:
import { createBunTestConfig } from "@suaveplan/testing-dom/config";
const cfg = createBunTestConfig(__dirname, { testTimeoutMs: 10_000 });
// cfg.preload includes "@suaveplan/testing-dom/polyfills"
```
## Usage: Advanced Example
```ts
import { createBunTestConfig } from "@suaveplan/testing-dom/config";
// Opt into coverage when running under CI.
const cfg = createBunTestConfig(__dirname, {
testTimeoutMs: 30_000,
coverageEnabled: process.env.CI === "true",
environment: "node",
preload: ["./tests/install-matchers.ts"],
});
// Hand off to a custom bun-test spawner:
await spawnBunTestRunner(cfg);
```
## API Reference
### `createBunTestConfig(callerDir, overrides): BunTestConfig`
- `callerDir` — reserved for future resolution use
- `overrides.preload` — extra preloads appended after the package polyfills
- `overrides.coverageEnabled` — default `false`
- `overrides.testTimeoutMs` — default `5000`
- `overrides.environment` — `"node"` is the only fully-supported value; `"happy-dom"` logs a warning
### `interface BunTestConfig`
- `preload: string[]`
- `coverageEnabled: boolean`
- `testTimeoutMs: number`
## Implementation Notes
The warning on `happy-dom` is logged via `console.warn` rather than thrown because some consumers intentionally use bun-test only for their Node-side suites and vitest for DOM; throwing would break the dual-runner contract. The config object is deliberately shallow — bun-test has no nested blocks, so there's no surface for the package to over-specify. Callers who need additional bun-test features (`timeout`, `coverage`, `bail`) pass them through their `bunfig.toml` directly.

View File

@@ -0,0 +1,125 @@
# createTestConfig — vitest / bun-test config factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - detect-environment — `detectEnvironment` probes the…
- [detect-environment](./detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- detect-environment — `detectEnvironment` probes the…
- [detect-environment](./detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- detect-environment — `detectEnvironment` probes the…
- [detect-environment](./detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- detect-environment — `detectEnvironment` probes the…
- [detect-environment](./detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
- [detect-environment](./detect-environment.md) — `detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"`…
- [detect-runner](./detect-runner.md) — `detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-…
- [vitest-config](./vitest-config.md) — `createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloa…
## Purpose
`createTestConfig(options?)` builds a runner-appropriate test config absorbing every WSL2-safe default and dual-runner consideration Suaveplan cares about: `pool: "forks"` (because `vmThreads` and `threads` both fail in Bun on WSL2), `setupFiles: ["@suaveplan/testing-dom/polyfills"]`, optional `@vitejs/plugin-react` loaded only when the caller targets `happy-dom` and the peer is installed, `fileParallelism: false` for happy-dom suites that would otherwise time out under 20+ concurrent forks. The factory reads the caller's `package.json` to detect runner + environment when not explicitly overridden.
This is the entry point every other package's `vitest.config.ts` should call. The `@suaveplan/testing` package itself uses an inline equivalent (it cannot import its own factory before being built).
## Features
- Auto-detects runner (`vitest` vs `bun:test`) from `BUN_TEST` env, package scripts mentioning `bun test`, or `vitest` in deps
- Auto-detects environment (`node` / `happy-dom` / `jsdom`) from `react` / `react-dom` / `jsdom` deps
- Returns a vitest `UserConfig` via `defineConfig` for the vitest path; returns `BunTestConfig` for bun-test
- WSL2 defaults: `pool: "forks"`, polyfills setupFile, `fileParallelism: false` for happy-dom
- `@vitejs/plugin-react` is loaded **only** when `environment === "happy-dom"` AND the optional peer is installed — missing peer doesn't break non-React consumers
- Coverage block `provider: "istanbul"` with 95% thresholds when `options.coverage === true`
- Throws `ConfigError("TESTING.CONFIG.UNKNOWN_RUNNER")` if asked for a runner outside the supported set
## Usage
### Basic example — Node-side test config
```ts
// packages/universal/foo/vitest.config.ts
import { createTestConfig } from "@suaveplan/testing/config";
export default await createTestConfig();
```
### React component testing with happy-dom
```ts
import { createTestConfig } from "@suaveplan/testing/config";
export default await createTestConfig({
environment: "happy-dom",
coverage: true,
});
```
### Force bun-test runner
```ts
const config = await createTestConfig({ runner: "bun:test" });
// → BunTestConfig with preload + warnings array
```
### Custom include / exclude
```ts
export default await createTestConfig({
include: ["src/**/*.spec.ts"],
exclude: ["src/__benchmarks__/**"],
testTimeout: 30_000,
});
```
## API Reference
### `createTestConfig(options?: TestConfigOptions): Promise<UserConfig | BunTestConfig>`
Async because it dynamically imports the React plugin when needed.
### `interface TestConfigOptions extends VitestConfigOverrides`
- `runner?: TestRunner` — `"vitest" | "bun:test"`; auto-detected if omitted
### `interface VitestConfigOverrides`
- `environment?: TestEnvironment` — `"node" | "happy-dom" | "jsdom"`
- `testTimeout?: number` — default `5000`
- `coverage?: boolean | Record<string, unknown>` — `true` enables defaults
- `setupFiles?: readonly string[]` — concatenated after polyfills
- `include?: readonly string[]` — concatenated after defaults
- `exclude?: readonly string[]` — concatenated after defaults
- `fileParallelism?: boolean` — defaults to `false` for happy-dom, `true` otherwise
### `type TestRunner` / `type TestEnvironment`
### `interface BunTestConfig` / `interface BunTestConfigOverrides`
- `callerDir`, `environment`, `testTimeoutMs`, `coverageEnabled`, `preload`, `warnings`
### Error codes thrown
| Code | Condition |
|---|---|
| `TESTING.CONFIG.UNKNOWN_RUNNER` | `options.runner` is set to a value other than `"vitest"` / `"bun:test"` |
## Implementation Notes
- **`pool: "forks"` is non-negotiable on WSL2.** `vmThreads` and `threads` both fail in Bun on WSL2 — see CLAUDE.md for "Vitest / WSL2 quirks". The factory hardcodes `forks`; consumers cannot override.
- **`fileParallelism: false`** for happy-dom because 20+ concurrent forks time out. Caller can override via `options.fileParallelism` if their suite is small.
- **`@vitejs/plugin-react`** is an optional peer in `package.json#peerDependenciesMeta`. Dynamic `await import` so the missing-peer case doesn't blow up at config-load time. If the import fails (no React), the factory returns the vitest config without the plugin.
- **`detectRunner`** reads `BUN_TEST` env first, then scans scripts for `bun test`, then falls back to vitest. Explicit `options.runner` always wins.
- **`detectEnvironment`** triggers `happy-dom` on `react` / `react-dom`, `jsdom` on `jsdom`, else `node`. Override via `options.environment`.
- **Coverage thresholds** default to 95% statements/branches/functions/lines, matching the quality gate. `options.coverage` as an object lets callers fully customize.
- **`callerDir` is exposed on `BunTestConfig`** so a future bun-test runner-script could resolve relative paths without re-detecting.

View File

@@ -0,0 +1,51 @@
# detect-environment
## Purpose
`detectEnvironment` probes the current runtime and returns the most-specific test environment available: `"happy-dom"` when it's importable, otherwise `"jsdom"` when that is, otherwise `"node"`. It lets config factories auto-select a DOM environment without forcing every consumer to hard-code one — the right answer depends on what devDependencies the consumer happens to have installed.
## Features
- Pure probe — returns a string, never throws
- Ordered preference: `happy-dom` → `jsdom` → `node`
- Non-throwing `require.resolve` via a safe try/catch
- Zero runtime dependencies — just reads the module graph
## Usage: Basic Example
```ts
import { detectEnvironment } from "@suaveplan/testing-dom/config";
const env = detectEnvironment();
// "happy-dom" | "jsdom" | "node"
```
## Usage: Advanced Example
```ts
import { createVitestConfig, detectEnvironment } from "@suaveplan/testing-dom/config";
import { defineConfig } from "vitest/config";
export default defineConfig(
createVitestConfig(__dirname, {
environment: detectEnvironment(),
coverage: true,
})
);
```
## API Reference
### `detectEnvironment(): TestEnvironment`
Returns one of:
- `"happy-dom"` — when `happy-dom` is resolvable from the caller
- `"jsdom"` — when `jsdom` is resolvable from the caller
- `"node"` — fallback when neither DOM library is available
### `type TestEnvironment = "happy-dom" | "jsdom" | "node"`
## Implementation Notes
Resolution uses `require.resolve` wrapped in a try/catch so missing modules don't surface as unhandled exceptions. The function runs at config-build time, not at test-run time, so the cost is paid once per vitest invocation and never per test. Consumers who need a deterministic test suite across environments should override the probe by passing an explicit `environment` to `createVitestConfig` — `detectEnvironment` is a convenience for the common case where the DOM preference follows which devDependency is installed.

View File

@@ -0,0 +1,60 @@
# detect-runner
## Purpose
`detectRunner` tells the caller whether the currently-executing process is vitest or bun-test. It's used by the runner-adapter module to pick the right implementation of `describe`/`it`/`expect` at runtime when a test file wants to be dual-runner. Since vitest and bun-test expose slightly different globals, consumers either import the detected runner's API from this package or branch on the returned string.
## Features
- Pure probe of `globalThis` / environment variables
- Three possible outcomes: `"vitest"`, `"bun-test"`, `"unknown"`
- Safe to call outside any test context — returns `"unknown"` rather than throwing
- Deterministic under a single runner — no heuristics that flip between runs
## Usage: Basic Example
```ts
import { detectRunner } from "@suaveplan/testing-dom/config";
const runner = detectRunner();
if (runner === "bun-test") {
// Use bun:test APIs
}
```
## Usage: Advanced Example
```ts
import { detectRunner } from "@suaveplan/testing-dom/config";
// A shared test-helper module that branches on runner.
export function useHelper(): unknown {
const runner = detectRunner();
switch (runner) {
case "vitest":
return loadVitestHelper();
case "bun-test":
return loadBunTestHelper();
case "unknown":
throw new Error(
"useHelper() called outside a supported test runner"
);
}
}
```
## API Reference
### `detectRunner(): TestRunner`
Returns one of:
- `"vitest"` — detected via `import.meta.vitest` or vitest-owned globals
- `"bun-test"` — detected via bun's `Bun.version` + `process.env.BUN_TEST`
- `"unknown"` — neither was detected
### `type TestRunner = "vitest" | "bun-test" | "unknown"`
## Implementation Notes
Detection reads multiple signals for each runner to remain robust against future runtime changes — relying on a single global is fragile. The function never reads from disk or spawns a child process, so it's safe to call at module-top during config import. For test files that want to statically import one runner's API, prefer importing `@suaveplan/testing-dom/runner` (which internally calls `detectRunner`) over writing the branch manually.

View File

@@ -0,0 +1,62 @@
# vitest-config
## Purpose
`createVitestConfig` builds a ready-to-use vitest test block with WSL2-safe defaults and the package's polyfills preloaded. It lives here so every package that imports `@suaveplan/testing-dom/config` gets the same `pool: "forks"`, `fileParallelism` heuristics, and setup-file wiring without copying the boilerplate into each `vitest.config.ts`.
## Features
- `pool: "forks"` — the only pool that consistently works under Bun on WSL2
- `fileParallelism` defaults to `false` for `happy-dom` / `jsdom` environments (avoids 20+ fork timeouts on WSL2)
- Prepends `@suaveplan/testing-dom/polyfills` to the user's `setupFiles`
- Accepts a `coverage` boolean (enables istanbul defaults) or a custom coverage object (pass-through)
- Appends user overrides to defaults rather than replacing them
## Usage: Basic Example
```ts
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { createVitestConfig } from "@suaveplan/testing-dom/config";
export default defineConfig(createVitestConfig(__dirname));
```
## Usage: Advanced Example
```ts
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { createVitestConfig } from "@suaveplan/testing-dom/config";
export default defineConfig(
createVitestConfig(__dirname, {
environment: "happy-dom",
coverage: true,
setupFiles: ["./tests/install-matchers.ts"],
include: ["tests/**/*.test.ts"],
fileParallelism: false,
testTimeout: 10_000,
})
);
```
## API Reference
### `createVitestConfig(callerDir, overrides): { test: Record<string, unknown> }`
- `callerDir` — the consumer package's root, passed to `root`; reserved for future alias resolution
- `overrides.environment` — `"node" | "happy-dom" | "jsdom"`; default `"node"`
- `overrides.coverage` — `true` turns on istanbul defaults with 95% thresholds; object form passes through verbatim
- `overrides.setupFiles` — appended to the polyfill import
- `overrides.include` / `overrides.exclude` — appended to defaults
- `overrides.testTimeout` — default 5000
- `overrides.fileParallelism` — default `false` for `happy-dom`, `true` otherwise
### `interface VitestConfigOverrides`
See source for the full shape; every field is optional.
## Implementation Notes
The returned shape is intentionally untyped from vitest's internals — consumers spread or merge it without tight coupling to the library's evolving generic parameters. The polyfill setup file is always prepended (not appended) so the user's own setup files can assume `globalThis.crypto` / `TextEncoder` are present. Coverage defaults live inline in this file rather than being imported from elsewhere so the produced config has zero runtime coupling to other `testing-dom` modules.

View File

@@ -0,0 +1,66 @@
# codes
## Purpose
`TESTING_DOM_CODES` is the package-local error code registry for `@suaveplan/testing-dom`. All codes share the `TESTING_DOM.` namespace so consumers can match on the prefix to spot testing-library failures in aggregated logs. The registry is hand-built here rather than going through `defineCodes` from `@suaveplan/error` because this package has zero `@suaveplan/*` deps by design.
## Features
- Frozen record of `TESTING_DOM.*` code strings
- `TestingDomCode` type alias for the discriminated union
- Stable — new codes append; removal requires a deprecation cycle
- Structural twin of the `defineCodes` output used by Layer 1 packages
## Usage: Basic Example
```ts
import { TESTING_DOM_CODES } from "@suaveplan/testing-dom";
TESTING_DOM_CODES.RENDER_FAILED; // "TESTING_DOM.RENDER.FAILED"
```
## Usage: Advanced Example
```ts
import {
TestingDomError,
TESTING_DOM_CODES,
type TestingDomCode,
} from "@suaveplan/testing-dom";
function classify(err: TestingDomError): "retry" | "fail" {
switch (err.code) {
case TESTING_DOM_CODES.USER_EVENT_INIT_FAILED:
return "retry";
case TESTING_DOM_CODES.RENDER_FAILED:
case TESTING_DOM_CODES.QUERY_FAILED:
return "fail";
}
}
function isTestingDomError(err: unknown): err is TestingDomError {
return err instanceof TestingDomError;
}
```
## API Reference
### `TESTING_DOM_CODES`
Frozen record of reason → fully-qualified code string.
### `type TestingDomCode`
Union of every `TESTING_DOM_CODES[K]` value.
## Implementation Notes
The runtime record is built by hand (`Object.freeze({ ... })`) rather than via `defineCodes` to maintain the zero-workspace-dep constraint. Adding a new code: append to the record, append to the union type, and keep existing entries unchanged. The package's own test suite verifies that every entry carries a `TESTING_DOM.` prefix and that no two entries share a value.
### Registry Layout
Codes live under the `TESTING.DOM` namespace and are produced by `defineCodes` — same pattern as every other Suaveplan package. Current codes cover render host mount failures, query helper missing-element errors, matcher setup failures, and polyfill double-install attempts. The `TestingDomCode` union is the canonical parameter type for catch blocks and error-helper signatures.
### Stability
Adding a new code is append-only: no renumbering, no migrations. Consumers that type-narrow on `code` get a compile error when a new code is added without a matching branch only if they use the `TestingDomCode` union exhaustively (e.g. in a `switch` with `never` check).

View File

@@ -0,0 +1,61 @@
# testing-dom-error
## Purpose
`TestingDomError` is the single concrete error class thrown by `@suaveplan/testing-dom`. It intentionally does NOT extend `@suaveplan/error`'s `SuaveplanError` because testing-infrastructure packages must stay free of any `@suaveplan/*` workspace dependency (otherwise they'd pull in the packages they're supposed to be testing). Its shape mirrors `SuaveplanError` structurally — `{ code, cause?, context? }` — so consumer ergonomics are identical.
## Features
- `code` narrowed to `TestingDomCode` — one of the `TESTING_DOM.*` literals
- Carries `cause` (via ES2022 `Error.cause`) and optional `context`
- Named stack trace — the `name` property is set to the subclass name
- Structural twin of `SuaveplanError` — consumers can polymorphise if they accept both shapes
## Usage: Basic Example
```ts
import { TestingDomError, TESTING_DOM_CODES } from "@suaveplan/testing-dom";
throw new TestingDomError(
TESTING_DOM_CODES.RENDER_FAILED,
"renderComponent: tree returned no roots"
);
```
## Usage: Advanced Example
```ts
import {
renderComponent,
TestingDomError,
TESTING_DOM_CODES,
} from "@suaveplan/testing-dom";
try {
renderComponent(<Broken />);
} catch (err) {
if (err instanceof TestingDomError) {
switch (err.code) {
case TESTING_DOM_CODES.RENDER_FAILED:
console.error("render failed:", err.message, err.cause);
break;
case TESTING_DOM_CODES.USER_EVENT_INIT_FAILED:
console.error("user-event init failed:", err.cause);
break;
}
}
}
```
## API Reference
### `class TestingDomError extends Error`
- `new TestingDomError(code, message, options?)`
- `options.cause?: unknown`
- `options.context?: Readonly<Record<string, unknown>>`
- `err.code: TestingDomCode` — literal union from `TESTING_DOM_CODES`
## Implementation Notes
The class does not extend `@suaveplan/error` because of rule #8 in `CLAUDE.md`: testing-infrastructure packages have ZERO `@suaveplan/*` deps to avoid the "tests depend on packages under test" circularity. Structurally, the shape matches `SuaveplanError` so if a consumer catches `TestingDomError` alongside `SuaveplanError` from application code, the two look identical at the shape level. If the project later decides this constraint is too strict, the migration is a one-line base-class swap and all callers stay unchanged.

View File

@@ -0,0 +1,60 @@
# matchers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `matchers` module is a side-effect import that extends the current test runner's `expect` with the `@testing-library/jest-dom` matchers (`toBeInTheDocument`, `toHaveAttribute`, `toHaveTextContent`, etc.). It's loaded via vitest's `setupFiles` or bun-test's `preload` rather than imported per-test so the extended matchers are available in every test file without ceremony.
## Features
- One-shot registration — calls `expect.extend(jestDomMatchers)` at import time
- No API surface of its own — it's a side-effect module
- Compatible with vitest and bun-test (both support `expect.extend`)
- Matchers include `toBeInTheDocument`, `toBeVisible`, `toHaveFocus`, `toHaveAttribute`, `toHaveClass`, and the full jest-dom set
## Usage: Basic Example
```ts
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { createVitestConfig } from "@suaveplan/testing-dom/config";
export default defineConfig(
createVitestConfig(__dirname, {
setupFiles: ["@suaveplan/testing-dom/matchers"],
})
);
```
## Usage: Advanced Example
```tsx
// Any test file, once matchers are installed via setupFiles:
import { render, screen } from "@suaveplan/testing-dom";
test("renders a labelled button", () => {
render(<button aria-label="save">save</button>);
expect(screen.getByRole("button", { name: "save" }))
.toBeInTheDocument();
expect(screen.getByRole("button", { name: "save" }))
.toHaveTextContent("save");
expect(screen.getByRole("button", { name: "save" }))
.toHaveAttribute("aria-label", "save");
});
```
## API Reference
### Side-effect import
No exports. The single act is `expect.extend(matchers)` at module top-level using matchers imported from `@testing-library/jest-dom`.
## Implementation Notes
The module intentionally has no exports to prevent consumers from trying to pass the matchers object around — side-effect imports are the only supported usage. If a setup-file chain needs multiple side-effect imports, list them in `setupFiles` in the order they should execute (polyfills first, matchers second). The import is idempotent: calling `expect.extend` twice with the same matchers is a no-op under both vitest and bun-test.
### Matcher Index
The wrapper installs every `@testing-library/jest-dom` matcher (`toBeInTheDocument`, `toHaveTextContent`, `toBeVisible`, `toHaveAttribute`, `toHaveClass`, `toBeDisabled`, `toHaveValue`, `toHaveFocus`, etc.) into both Vitest's `expect` and Bun's test runner expect surface via a single `registerMatchers()` call. Double-install is idempotent: the second call is a no-op.

View File

@@ -0,0 +1,64 @@
# instrumented
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`instrumented` wraps a named function with optional OpenTelemetry instrumentation — counters for invocations, histograms for durations, span events for failures. Instrumentation is **gated** behind the `TESTING_DOM_OTEL_ENABLED` environment variable, so production test runs pay zero overhead while CI or local profiling sessions can flip the flag on to surface hot-path data.
## Features
- Gated at import time via an environment variable check; no-op when disabled
- Uses only `@opentelemetry/api` (no SDK dependency) — consumers wire the actual exporters at bootstrap
- Preserves the wrapped function's signature and return type
- Handles both sync and async functions, including rejection paths
- Zero allocation overhead when gated off (returns the original function)
## Usage: Basic Example
```ts
import { instrumented } from "@suaveplan/testing-dom/observability";
const render = instrumented("testing_dom.render", (tree: unknown) => {
return reactRender(tree);
});
render(<App />); // counter incremented, histogram timed — if enabled
```
## Usage: Advanced Example
```ts
import { instrumented } from "@suaveplan/testing-dom/observability";
class TestingHarness {
public setup = instrumented("testing_dom.harness.setup", async () => {
await this.loadFixtures();
await this.mountApp();
});
public teardown = instrumented("testing_dom.harness.teardown", async () => {
await this.unmountApp();
});
}
// Flag-off: `const harness = new TestingHarness(); await harness.setup();`
// runs at the same cost as the unwrapped method.
// Flag-on: every call emits a counter and a histogram observation.
```
## API Reference
### `instrumented<TFn>(name, fn): TFn`
- `name` — metric name suffix; the full metric becomes `testing_dom.<name>.count` / `duration_ms` / `errors`
- `fn` — any function (sync or async)
- Returns `TFn` — a function with the same signature, instrumented when enabled
### Environment gating
`TESTING_DOM_OTEL_ENABLED` must equal `"1"` / `"true"` at module-import time for instrumentation to activate.
## Implementation Notes
The gate is evaluated once, at module import — flipping the env var mid-process has no effect. This avoids per-call environment reads in the hot path. When the gate is off, `instrumented` returns the original function reference unchanged, so V8 inlines as if the wrapper was never there. The `@opentelemetry/api` import is a marker package — it provides no-op tracers/meters when no SDK is registered, so even with the gate on, unconfigured environments pay only the cost of a no-op counter increment.

View File

@@ -0,0 +1,64 @@
# queries
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `queries` module re-exports the `@testing-library/react` query primitives — `screen`, `within`, `byRole`, `byLabelText`, `byPlaceholderText`, `byText`, `byDisplayValue`, `byAltText`, `byTitle`, `byTestId` — under one canonical import path so every package in the project reaches for the same API without individually depending on `@testing-library/react`. Keeping the re-export surface in one module also means upgrades to `@testing-library/react` touch a single file.
## Features
- Curated re-exports of every stable `@testing-library/react` query
- `screen` and `within` helpers included for scoped queries
- Types preserved — TS jumps straight to `@testing-library/react`'s definitions
- Zero runtime overhead — pure re-export
## Usage: Basic Example
```tsx
import { render, screen } from "@suaveplan/testing-dom";
render(<button>save</button>);
expect(screen.getByRole("button", { name: "save" })).toBeInTheDocument();
```
## Usage: Advanced Example
```tsx
import { render, screen, within } from "@suaveplan/testing-dom";
render(
<form aria-label="login">
<input aria-label="email" />
<input aria-label="password" type="password" />
<button type="submit">sign in</button>
</form>
);
const form = screen.getByRole("form", { name: "login" });
const scoped = within(form);
expect(scoped.getByLabelText("email")).toBeInTheDocument();
expect(scoped.getByRole("button", { name: "sign in" })).toBeEnabled();
```
## API Reference
### Re-exported symbols
- `screen` — global query root
- `within(container)` — scoped queries
- `getByRole` / `queryByRole` / `findByRole` — role-based
- `getByLabelText` / `queryByLabelText` / `findByLabelText`
- `getByPlaceholderText` / …
- `getByText` / …
- `getByDisplayValue` / …
- `getByAltText` / …
- `getByTitle` / …
- `getByTestId` / …
All retain their upstream type signatures.
## Implementation Notes
Rule #4 in `CLAUDE.md` forbids re-exporting external dependencies "for convenience" — the exception here is that this module serves as a project-wide swap point if we ever replace testing-library with another library. Every import path consumers use is `@suaveplan/testing-dom`, so a swap is a one-file change. The curated surface excludes `fireEvent` because userEvent (in `setup-user.ts`) is the preferred ergonomic wrapper — `fireEvent` is available via direct `@testing-library/react` import for callers who need the lower level.

View File

@@ -0,0 +1,60 @@
# render-component
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`renderComponent` wraps `@testing-library/react`'s `render` with two project-wide defaults: (1) it calls `cleanup()` before mounting so orphaned trees from a previous test never pollute the current test's query space, and (2) it throws `TestingDomError("TESTING_DOM.RENDER.FAILED")` on any underlying failure so callers always see a typed error with structured context.
## Features
- Automatic pre-mount `cleanup()` — no stray DOM from prior tests
- Typed failure mode via `TestingDomError`
- Thin wrapper — returns the same `RenderResult` as upstream
- Accepts the full `RenderOptions` surface (`container`, `baseElement`, `hydrate`, `wrapper`)
## Usage: Basic Example
```tsx
import { renderComponent } from "@suaveplan/testing-dom";
const { getByRole } = renderComponent(<button>save</button>);
expect(getByRole("button", { name: "save" })).toBeInTheDocument();
```
## Usage: Advanced Example
```tsx
import { renderComponent, TestingDomError, TESTING_DOM_CODES } from "@suaveplan/testing-dom";
import { MyProvider } from "./MyProvider";
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<MyProvider>{children}</MyProvider>
);
try {
const { container, rerender } = renderComponent(<App />, { wrapper: Wrapper });
expect(container.firstChild).toHaveTextContent("Hello");
rerender(<App locale="fr" />);
expect(container.firstChild).toHaveTextContent("Bonjour");
} catch (err) {
if (err instanceof TestingDomError && err.code === TESTING_DOM_CODES.RENDER_FAILED) {
console.error("render failed:", err.cause);
}
throw err;
}
```
## API Reference
### `renderComponent(ui, options?): RenderResult`
- `ui` — React element or functional component
- `options` — passed through to upstream `render` (full `RenderOptions` surface)
- Returns the upstream `RenderResult` unchanged
- Throws `TestingDomError("TESTING_DOM.RENDER.FAILED")` on any underlying failure, preserving `cause`
## Implementation Notes
Calling `cleanup()` BEFORE mount (rather than after) is deliberate — it means a test file that forgot to register `afterEach(cleanup)` still gets isolated trees between tests. It also avoids the subtle afterEach-ordering issue where a cleanup hook might fire before the test's expectations if another `afterEach` throws. Catching and rethrowing as `TestingDomError` normalises the error surface for downstream code; the underlying testing-library error lives on `err.cause` for debugging.

View File

@@ -0,0 +1,62 @@
# runner/bun-test
## Purpose
The `runner/bun-test` module re-exports bun-test's primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `mock`) under the same stable alias the `runner/vitest` module uses. Together they make `@suaveplan/testing-dom/runner` a dual-runner entry point — consumer test files that import from here run unchanged under either vitest (via `bunx vitest`) or bun-test (via `bun test`).
## Features
- Re-exports the bun-test assertion + lifecycle API
- Uses `bun:test` specifier — resolved at runtime by Bun, excluded from vitest builds
- Conditional export — package.json's `exports` picks this module under the `bun` condition
## Usage: Basic Example
```ts
import { describe, it, expect } from "@suaveplan/testing-dom/runner";
describe("same test file, either runner", () => {
it("asserts", () => {
expect(1 + 1).toBe(2);
});
});
```
## Usage: Advanced Example
```ts
import {
describe,
it,
expect,
beforeAll,
afterAll,
mock,
} from "@suaveplan/testing-dom/runner";
describe("bun-test specific mocks", () => {
let server: { close: () => void };
beforeAll(() => {
server = startHttpStub(3000);
});
afterAll(() => server.close());
it("mocks a module method", () => {
const fetchStub = mock<typeof fetch>(() => Promise.resolve(new Response("ok")));
// ... use fetchStub
expect(fetchStub).toHaveBeenCalled();
});
});
```
## API Reference
Re-exports: `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `mock`.
## Implementation Notes
The module imports from `"bun:test"`, which resolves only in Bun; under vitest it's excluded from the bundle via vite's external-module configuration AND via the package's coverage-exclude list (since the file is never executed under vitest). Mocking APIs differ between the two runners: bun-test's `mock` returns a different shape than vitest's `vi.fn`. Consumers who need both runners' test files to type-check should stick to the lowest-common-denominator API — `describe`, `it`, `expect`, and lifecycle hooks — and branch on `detectRunner()` for anything beyond that.
### Integration
The Bun-test runner shim exposes `describe` / `it` / `beforeEach` / `afterEach` / `expect` bindings that mirror Vitest's API so test files can be written once and run in either runner. The shim installs the jest-dom matchers via `registerMatchers` on module load and sets up happy-dom as the default DOM host when the test file imports from `@suaveplan/testing-dom/runner/bun-test`.

View File

@@ -0,0 +1,57 @@
# runner/mocks
## Purpose
The `runner/mocks` module offers a small set of dependency-injection friendly mock helpers that work identically under vitest and bun-test. Because `vi.mock` / `mock.module` semantics differ sharply between the two runners (and often fail unpredictably in Bun's ESM resolver), this package prefers DI-style helpers — `makeFn`, `makeClass`, `makePromise` — that let a test substitute values at call-site rather than patching the module graph.
## Features
- Runner-agnostic helpers — no reliance on `vi.mock` / `mock.module`
- `makeFn` returns a call-tracking function with `.calls`, `.mock`, and `.mockReturnValueOnce`
- `makeClass` constructs a no-op subclass with overridable methods
- `makePromise` returns `{ promise, resolve, reject }` for deferred resolution in tests
- Works inside both runners without configuration
## Usage: Basic Example
```ts
import { makeFn } from "@suaveplan/testing-dom/runner";
const greet = makeFn<(name: string) => string>(() => "hi");
greet("alice");
expect(greet.calls).toEqual([["alice"]]);
```
## Usage: Advanced Example
```ts
import { makeFn, makePromise } from "@suaveplan/testing-dom/runner";
import { describe, it, expect } from "@suaveplan/testing-dom/runner";
describe("flaky dependency via DI", () => {
it("coordinates two async callers via a deferred", async () => {
const deferred = makePromise<{ status: 200 }>();
const fetchStub = makeFn<() => Promise<{ status: 200 }>>(
() => deferred.promise
);
const results = [fetchStub(), fetchStub()];
deferred.resolve({ status: 200 });
expect(fetchStub.calls.length).toBe(2);
expect((await Promise.all(results)).every((r) => r.status === 200)).toBe(true);
});
});
```
## API Reference
### `makeFn<TFn>(impl?): TFn & { calls: Parameters<TFn>[]; mockReturnValueOnce(v: ReturnType<TFn>): void }`
### `makeClass<T>(overrides?): new () => T`
### `makePromise<T>(): { promise: Promise<T>; resolve: (v: T) => void; reject: (e: unknown) => void }`
## Implementation Notes
These helpers intentionally duplicate some of what `vi.fn` offers because the project standard (captured in the `bun-vitest-module-mocking` feedback memory) is to avoid module-level mocks entirely in favour of DI. Module mocks break under Bun's ESM resolver in unpredictable ways, and even under vitest they create implicit coupling between the test and the subject's internal imports. `makeFn` records call history but does not provide `toHaveBeenCalled` — consumers use `fn.calls.length` directly so the assertion works under any runner.

View File

@@ -0,0 +1,100 @@
# Runner — vitest / bun-test conditional re-exports
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - vitest — The `runner/vitest` module re-exports vite…
- [vitest](./vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- vitest — The `runner/vitest` module re-exports vite…
- [vitest](./vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- vitest — The `runner/vitest` module re-exports vite…
- [vitest](./vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- vitest — The `runner/vitest` module re-exports vite…
- [vitest](./vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
- [vitest](./vitest.md) — The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll…
## Purpose
The runner barrel re-exports the test primitives (`describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `vi`) plus a runner-neutral mock surface (`fn`, `spy`, `stub`, `mock`, `fakeClock`, `restoreMocks`, `restoreTimers`). Two backends are supported: vitest (the default) and bun's built-in `bun:test`. Which backend resolves at import time is driven by `package.json` conditional exports — under the `bun` condition Node resolves `./dist/runner/bun-test.js`; under `vitest` or `default` it resolves `./dist/index.js` which barrels in `vitest.ts`.
The mock helpers exist so test code can stay runner-agnostic. A package that uses `fn()` / `spy()` / `stub()` instead of importing `vi` directly will run unchanged when its consumers swap test runners — no test-source rewrites required. This is the pattern the original `2026-04-08-testing-libraries` change established and we preserve it here.
## Features
- Re-exports of `vitest` primitives under the default / vitest conditions
- `bun:test` re-exports under the bun condition with API-shaped equivalents (`vi` is aliased to bun's `jest` namespace)
- Runner-neutral mock helpers: `fn`, `spy`, `stub`, `mock(modulePath)`, `fakeClock`, `restoreMocks`, `restoreTimers`
- `mock(modulePath, factory?)` returns a `{ restore() }` handle for symmetry across runners
- `fakeClock()` exists for legacy compatibility — new code should prefer the deterministic `FakeClock` class from `@suaveplan/testing/clock/fake-clock`
## Usage
### Basic example — runner-neutral test
```ts
import {
describe,
it,
expect,
fn,
spy,
} from "@suaveplan/testing";
describe("user service", () => {
it("calls the dependency exactly once", { timeout: 5000 }, () => {
const dep = { fetch: () => "real" };
const handle = spy(dep, "fetch");
const callback = fn();
callback("hello");
expect(callback).toHaveBeenCalledWith("hello");
expect(handle).toHaveBeenCalledTimes(0);
});
});
```
### Advanced example — module mocking with restore
```ts
import { mock, restoreMocks } from "@suaveplan/testing";
const handle = mock("./network.js", () => ({
fetchUser: () => ({ id: 42 }),
}));
// run tests against the mocked module ...
handle.restore();
restoreMocks();
```
### Switching to bun-test
In a consumer's `package.json`, set the `bun` condition (or run via `bun test`). Resolution picks `@suaveplan/testing/dist/runner/bun-test.js` automatically; no test-source changes are needed.
## API Reference
### Test primitives
`describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `vi` — re-exported from the active runner.
### Mock helpers
- `fn<TArgs, TReturn>(implementation?): Mock`
- `spy<T, K>(obj: T, method: K): SpyHandle`
- `stub<T, K>(obj: T, method: K, implementation?): SpyHandle`
- `mock(modulePath: string, factory?: () => unknown): { restore() }`
- `fakeClock(): { advance(ms), restore() }`
- `restoreMocks(): void`
- `restoreTimers(): void`
## Implementation Notes
- **`vi` on the bun path is aliased to `jest`.** Bun-test's mock surface lives under `jest` for API compatibility; the alias keeps consumer code identical across runners.
- **`fakeClock` is legacy.** Prefer the deterministic `FakeClock` class from the clock module — it's runner-independent, supports `Clock` injection across the package, and gives you `snapshot()` / `restore()` for fine control.
- **`mock()` on bun has a no-op `restore()`** because `bun:test` does not expose `unmock`. Call `restoreMocks()` (which delegates to `jest.restoreAllMocks()`) to reset state.
- **The conditional-exports routing** is load-bearing — see `package.json#exports.".".bun` and `.vitest`. Removing either branch breaks the dual-runner contract documented in the spec.
- **Type safety on cross-runner mocks** is approximate. The wrappers use `as never` casts internally because the spy/stub generics differ between the two libraries; consumer code sees a stable shape.

View File

@@ -0,0 +1,71 @@
# runner/vitest
## Purpose
The `runner/vitest` module re-exports vitest's `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, and `vi` under a stable alias. It's part of the dual-runner support in `@suaveplan/testing-dom` — consumers who want their test files to run under both vitest and bun-test can import from `@suaveplan/testing-dom/runner` instead of picking one directly. At type-check time this module statically resolves to vitest's types.
## Features
- Re-exports the vitest assertion + lifecycle API
- Preserves upstream types — TS jumps to vitest's definitions
- Conditional export — package.json's `exports` picks this module under the `vitest` condition
## Usage: Basic Example
```ts
import { describe, it, expect } from "@suaveplan/testing-dom/runner";
describe("my suite", () => {
it("asserts", { timeout: 5000 }, () => {
expect(1 + 1).toBe(2);
});
});
```
## Usage: Advanced Example
```ts
import { describe, it, expect, beforeEach, vi } from "@suaveplan/testing-dom/runner";
describe("mockable dependency", () => {
let clock = 0;
beforeEach(() => {
clock = 0;
});
it("uses fake timers", { timeout: 5000 }, () => {
vi.useFakeTimers();
try {
const inner = vi.fn((n: number) => ++clock);
inner(1);
inner(1);
expect(inner).toHaveBeenCalledTimes(2);
expect(clock).toBe(2);
} finally {
vi.useRealTimers();
}
});
});
```
## API Reference
Re-exports: `describe`, `it`, `test`, `expect`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `vi`.
## Implementation Notes
The re-export is unconditional because the module is only reachable via the `vitest` condition in `package.json#exports`. If the condition machinery is bypassed (e.g. the import is resolved manually), vitest must still be available as a peer of the consumer — this module does not install vitest or provide its own assertion library.
### Integration
The Vitest runner shim re-exports `describe` / `it` / `beforeEach` / `afterEach` / `expect` from `vitest` and installs the jest-dom matcher set via `registerMatchers` on module load. Importing this module is sufficient for a Vitest file to use the full DOM-matcher surface — no manual matcher wiring needed.
## Further Usage: Basic Example
```ts
import { it, expect, render } from "@suaveplan/testing-dom/runner/vitest";
it("renders hello", { timeout: 5000 }, () => {
const { getByText } = render(<h1>Hello</h1>);
expect(getByText("Hello")).toBeInTheDocument();
});
```

View File

@@ -0,0 +1,61 @@
# setup/polyfills
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `setup/polyfills` module is a side-effect import that installs the globals test code expects to be present — `crypto`, `TextEncoder`, `TextDecoder` — when the host runtime (bare Node, happy-dom, jsdom, or Bun) doesn't already provide them. It's idempotent and safe to import multiple times.
## Features
- Installs `globalThis.crypto` from Node's `webcrypto` when missing
- Installs `TextEncoder` / `TextDecoder` from `node:util` when missing
- Guards every assignment with an existence check — never overwrites a populated global
- Exports nothing — side effects only
## Usage: Basic Example
```ts
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { createVitestConfig } from "@suaveplan/testing-dom/config";
export default defineConfig(
createVitestConfig(__dirname, {
setupFiles: ["@suaveplan/testing-dom/polyfills"],
})
);
```
## Usage: Advanced Example
```ts
// bunfig.toml (bun-test)
// [test]
// preload = ["@suaveplan/testing-dom/polyfills", "./tests/install-matchers.ts"]
// Or programmatically via createBunTestConfig:
import { createBunTestConfig } from "@suaveplan/testing-dom/config";
const cfg = createBunTestConfig(__dirname, {
preload: ["./tests/install-matchers.ts"],
// createBunTestConfig adds the polyfills entry automatically.
});
```
## API Reference
No exports. Side effects:
- `globalThis.crypto ??= webcrypto` _(from `node:crypto`)_
- `globalThis.TextEncoder ??= NodeTextEncoder`
- `globalThis.TextDecoder ??= NodeTextDecoder`
## Implementation Notes
Modern Node (≥19) and Bun both ship `crypto` globally, so the `??=` short-circuits on those hosts. The module remains in the package for older Node versions and for happy-dom sandboxes that don't initialise `crypto`. Imports use `node:` specifiers so the code remains bundler-safe — Rollup/esbuild skip it when externalising node built-ins. Because the module exports nothing, consumers who mistakenly try `import { x } from "@suaveplan/testing-dom/polyfills"` get a useful TypeScript error rather than silent misbehaviour.
### Coverage
The polyfill bundle installs happy-dom's `DOMParser`, `XMLSerializer`, `fetch`, `Request`, `Response`, `Headers`, `URL`, `URLSearchParams`, and `crypto.getRandomValues` into the Node / Bun global scope when they are missing. Double-install is idempotent — the module tracks a `Symbol.for("suaveplan.testing-dom.polyfilled")` marker on `globalThis` so repeated imports are cheap.

View File

@@ -0,0 +1,63 @@
# setup-user
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`setupUser` returns a configured `@testing-library/user-event` v14 instance with project-wide defaults appropriate for happy-dom. The wrapper exists so every test that simulates user input gets the same `delay`, `pointerEventsCheck`, and timer handling — consumers don't have to remember the boilerplate and upgrades of `user-event` touch a single file.
## Features
- Defaults tuned for happy-dom (no `delay`, no pointer-event checks — happy-dom treats every node as clickable)
- Forwards caller overrides so individual tests can opt-in to stricter pointer checks
- Respects vitest's fake-timers — temporarily swaps them for real timers while user-event internals await
- Returns the fully-typed `UserEvent` instance from `@testing-library/user-event`
## Usage: Basic Example
```tsx
import { renderComponent, setupUser, screen } from "@suaveplan/testing-dom";
test("typing", async () => {
renderComponent(<input aria-label="name" />);
const user = setupUser();
await user.type(screen.getByLabelText("name"), "alice");
expect(screen.getByLabelText("name")).toHaveValue("alice");
});
```
## Usage: Advanced Example
```tsx
import { renderComponent, setupUser, screen } from "@suaveplan/testing-dom";
import { vi } from "@suaveplan/testing-dom/runner";
test("debounced save uses fake timers", async () => {
vi.useFakeTimers();
try {
renderComponent(<DebouncedForm />);
const user = setupUser({ advanceTimers: vi.advanceTimersByTime });
await user.type(screen.getByLabelText("title"), "new title");
vi.advanceTimersByTime(500);
expect(screen.getByText("saved")).toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});
```
## API Reference
### `setupUser(options?): UserEvent`
- `options` — passed through to upstream `userEvent.setup(options)`
- Returns a `UserEvent` instance (upstream type preserved)
## Implementation Notes
The wrapper supplies `{ delay: null, pointerEventsCheck: 0 }` as defaults because happy-dom's layout engine is minimal — simulating realistic pointer-event checks wastes test time without surfacing additional bugs. Tests that need the full browser-like semantics should run under Playwright (the `testing-e2e` package) rather than trying to coerce happy-dom into matching. The fake-timers integration uses try/finally around `vi.useFakeTimers()` so a failed test leaves the runtime in a known-good real-timer state for subsequent tests.
### User Events
`setupUser()` wraps `@testing-library/user-event`'s `userEvent.setup()` with sensible Suaveplan defaults: `pointerEventsCheck: 0` (off, because happy-dom doesn't model pointer-events reliably), `delay: null` for deterministic tests, and automatic advance of Vitest fake timers when they're active. The returned object has the full `click` / `type` / `hover` / `keyboard` / `upload` API surface.