Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
## Context
`@suaveplan/testing-dom` is the second testing-infrastructure package on genesis. It exists because:
1. Every React component test needs a DOM environment (happy-dom or jsdom under vitest).
2. The genesis testing-package separation rule (CLAUDE.md #9) puts the test-runner adapter here, not in `@suaveplan/testing` (which stays universal/runner-agnostic).
3. The staged runner/config/polyfills code from the `enterprise-testing` split (`packages/universal/testing-dom-staging/`) is its starting material.
Layer 0 dependency profile: external catalog deps only. No `@suaveplan/*` workspace deps. The package's own tests run under vitest with happy-dom.
## Goals / Non-Goals
**Goals:**
- Absorb staged runner/config/polyfills cleanly from `testing-dom-staging/`.
- Preserve the dual-runner conditional export (`vitest` default / `bun` condition) for the runner re-exports.
- Provide thin React/DOM helpers built on `@testing-library/react` + `@testing-library/jest-dom` + `@testing-library/user-event` (v14+).
- Local `TestingDomError` hierarchy with `{ code, cause, context }` shape; codes follow `TESTING_DOM.<DOMAIN>.<REASON>`.
- `createTestConfig()` (moved from testing) absorbs WSL2 pool/polyfill defaults and dynamic React-plugin loading.
- ≥ 95% test coverage; co-located unit tests + cross-cutting `tests/` per the test-placement hard rule.
**Non-Goals:**
- Forking `@testing-library/*` internals — the package is a thin wrapper.
- Legacy `fireEvent` helpers in the default export — `userEvent` v14 only.
- Wrapping `@suaveplan/error` / `@suaveplan/opentelemetry` / `@suaveplan/schema-registry` — these are forbidden workspace deps. Local equivalents.
## Decisions
### D1: Local `TestingDomError` hierarchy
Mirrors `@suaveplan/testing`'s `TestingError` shape but with `TESTING_DOM.<DOMAIN>.<REASON>` codes. Subclasses: `RenderError`, `MatcherError`, `ConfigError`, `AbortedError`. Reason: rule #8/#9 — no `@suaveplan/*` workspace deps. Shape-compatible with future `@suaveplan/error` adapter.
### D2: Runner adapter via conditional exports (preserved from staging)
`package.json` `exports["."]` routes vitest condition → vitest barrel, bun condition → bun-test barrel, default → vitest. `mocks.ts` provides runner-neutral `fn`/`spy`/`stub`/`mock`/`fakeClock` shims. The vitest dynamic-mock bracket-access trick (`vi["mock"]`) carries over from the staging code. **Coverage exception:** `runner/bun-test.ts` is excluded from istanbul coverage because vitest cannot resolve `bun:test` (same exception we already documented for testing).
### D3: `createTestConfig()` factory
Lifts `createTestConfig`, `detectRunner`, `detectEnvironment`, `createVitestConfig`, `createBunTestConfig` verbatim from staging. Hardcodes `pool: "forks"` (WSL2 vmThreads/threads fail), `setupFiles: ["@suaveplan/testing-dom/polyfills"]`, `fileParallelism: false` for happy-dom, optional dynamic `@vitejs/plugin-react` import.
### D4: React render wrapper — `renderComponent`
Thin wrapper over `@testing-library/react`'s `render` that:
- Wraps the tree in any provider stack the caller passes (e.g. router, query client) via a `wrapper` option
- Auto-cleans on `afterEach` via `cleanup()` from `@testing-library/react`
- Returns the standard `RenderResult` extended with a `userEvent` instance for ergonomic event dispatching
- Returns `TestingDomError("TESTING_DOM.RENDER.SETUP_FAILED")` if the render itself throws
### D5: Accessibility-first query aliases
Re-export Testing Library queries verbatim AND add ergonomic aliases:
- `byRole(name, opts)` → `screen.getByRole(name, opts)` (strict get; throws on miss)
- `findRole`, `queryRole` similar variants
- `byLabelText`, `byText`, `byPlaceholderText` similarly
Documentation in `.md` files steers callers toward role/label/text queries; `getByTestId` is re-exported but flagged as a fallback only.
### D6: User events via `userEvent` v14
Single export: `setupUser(options?)` returns a `UserEvent` instance with `pointerEventsCheck: 0` default for happy-dom (which doesn't fully simulate pointer events). When vitest fake timers are active (detected via `vi.isFakeTimers()`), wires `userEvent.setup({ advanceTimers: vi.advanceTimersByTime })` automatically.
### D7: jest-dom matchers extended via vitest's `expect.extend`
A side-effect import path `@suaveplan/testing-dom/matchers` extends vitest's `expect` with the `@testing-library/jest-dom` matcher set. Imported via `setupFiles` or per-test as a side effect.
### D8: Polyfills file
Lifted from staging — installs `globalThis.crypto` (from `node:crypto.webcrypto`), `globalThis.TextEncoder`/`TextDecoder` (from `node:util`) when missing. Side-effecting, exports nothing. Subpath: `@suaveplan/testing-dom/polyfills`.
### D9: OTEL via `@opentelemetry/api` (gated)
Optional debug instrumentation behind `process.env.TESTING_DOM_OTEL_ENABLED === "1"`. Counters: `testing_dom.render.count`, `testing_dom.user.click.count`. Histograms: `testing_dom.render.duration_ms`. Disabled by default — adds zero overhead in normal test runs.
### D10: Tests use `@suaveplan/testing` (utilities) directly
The package's own tests import primitive utilities (FakeClock, Factory, etc.) directly from `@suaveplan/testing`. This is the one allowed cross-genesis-package dep — but only as a `devDependency`, not a runtime dep. Production users of `testing-dom` never transitively pull `@suaveplan/testing`.
## Risks / Trade-offs
- **happy-dom limitations** — pointer events, intersection observer, resize observer aren't fully simulated. Mitigation: document, provide opt-in shims for the most-needed gaps.
- **React 19 act() boundaries** — incorrect handling of async state updates leaks errors. Mitigation: `renderComponent` always awaits `act()` for the initial render.
- **bun-test path** — same untestable-under-vitest issue as the original; excluded from coverage. Mitigation: add a `bun:smoke` script later that runs a tiny test fixture under `bun test` to verify the dual-runner contract works in anger.
- **`@vitejs/plugin-react` optional peer** — missing peer must not break non-React consumers. Mitigation: dynamic import inside `try/catch` in `createTestConfig` (preserved from staging).
- **`devDependency on @suaveplan/testing`** — D10 violates the spirit of "no @suaveplan/* deps in testing-infra packages" but only at devDep level. Tests need `FakeClock`, `Factory`, etc. Documented in CLAUDE.md exception note.

View File

@@ -0,0 +1,50 @@
## Why
`@suaveplan/testing-dom` is the entry point for every React/DOM component test in the genesis monorepo and the home of the test runner adapter. Per the testing-package separation hard rule (see `openspec/project.md`), it owns:
- The vitest primitive re-exports (`describe`, `it`, `expect`, `vi`, `fn`, `spy`, `stub`, `mock`, `fakeClock`, `restoreMocks`, `restoreTimers`)
- The `createTestConfig()` factory and runner/environment detectors (`detectRunner`, `detectEnvironment`, `createVitestConfig`, `createBunTestConfig`)
- The polyfills setup file (`@suaveplan/testing-dom/polyfills`)
- React/DOM-specific helpers built on `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`
Genesis Layer 0 — testing-infra packages take no `@suaveplan/*` workspace deps. External catalog deps only. The runner adapter + DOM helpers live here (not in `@suaveplan/testing`) because DOM tests already imply vitest+happy-dom; co-locating the runner adapter with the DOM helpers is the natural home.
## What Changes
- New package `@suaveplan/testing-dom` at `packages/universal/testing-dom/` (genesis-fresh, version `0.1.0`).
- **Imports** the staged runner/config/polyfills code from `packages/universal/testing-dom-staging/` (created during the testing split refactor) and absorbs it cleanly.
- **Substitutions per genesis rules:**
- Local `TestingDomError extends Error` with `{ code, cause, context }` shape (no `@suaveplan/error`)
- `@opentelemetry/api` direct (no `@suaveplan/opentelemetry`)
- `zod` direct (no `@suaveplan/schema-registry`)
- English-only `code` machine-contract messages (no `@suaveplan/i18n`)
- **No `@suaveplan/*` workspace deps.** External catalog deps only.
### Public surface (this package)
- **Runner re-exports** (vitest under default condition; bun-test under `bun` condition): `describe`, `it`, `test`, `expect`, `vi`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `fn`, `spy`, `stub`, `mock`, `fakeClock`, `restoreMocks`, `restoreTimers`
- **Config**: `createTestConfig`, `TestConfigOptions`, `TestRunner`, `TestEnvironment`, `VitestConfigOverrides`, `BunTestConfig`, `BunTestConfigOverrides`
- **Polyfills**: `@suaveplan/testing-dom/polyfills` side-effect module
- **DOM render**: `renderComponent` (typed wrapper around `@testing-library/react`'s `render` with sensible defaults: act-aware, cleanup hooked into vitest's `afterEach`)
- **DOM queries**: re-exports `screen`, `within`, `findByRole`/`getByRole`/`queryByRole`, etc.; surfaces accessibility-first names with clearer aliases (e.g. `byRole(name, opts)` shortcut). `getByTestId` still re-exported but documented as a fallback.
- **User events**: `userEvent` (v14+ instance) + a `setupUser()` factory wired to vitest fake timers when present
- **Matchers**: extends `expect` with `@testing-library/jest-dom` matchers
- **Errors**: `TestingDomError`, `RenderError`, `MatcherError`, `ConfigError`, `AbortedError` — each with stable `TESTING_DOM.<DOMAIN>.<REASON>` codes
## Capabilities
### New Capabilities
- `testing-dom`: DOM/React testing infrastructure for genesis — runner adapter (vitest+bun-test conditional exports), `createTestConfig` factory, polyfills, React 19 render helpers, accessibility-first queries, jest-dom matchers, user-event v14.
### Modified Capabilities
(none — no prior spec)
## Impact
- **Affected code**: `packages/universal/testing-dom/**` (fresh).
- **Affected consumers**: every React/DOM-testing package (none yet on genesis; this is Layer 0).
- **Version**: `0.1.0` (genesis baseline).
- **Catalogs added/extended**: `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`, `@testing-library/dom` to a new `catalogs.dom-testing` (or extend `catalogs.testing`).
- **Pre-publish gate**: `bunx turbo lint typecheck test --filter=@suaveplan/testing-dom` green + ≥ 95% coverage on touched modules.

View File

@@ -0,0 +1,107 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-dom` `dependencies` and `peerDependencies` MUST NOT contain any `@suaveplan/*` workspace package. `devDependencies` MAY include `@suaveplan/testing` (utilities) for the package's own tests, but the production runtime closure MUST NOT contain it.
#### Scenario: Runtime dep-graph guard
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-dom...` is run (forward closure of runtime deps)
- **THEN** no `@suaveplan/*` package appears
#### Scenario: devDep allowance
- **WHEN** `package.json` is parsed
- **THEN** `dependencies` and `peerDependencies` are free of `@suaveplan/*` AND `devDependencies` may contain `@suaveplan/testing` (this dep is not visible to consumers)
### Requirement: Dual-runner conditional exports preserved
The package `exports["."]` SHALL route the `vitest` condition and the `default` condition to the vitest barrel and the `bun` condition to the bun-test barrel. The exports MUST expose `describe`, `it`, `test`, `expect`, `vi`, `beforeAll`, `beforeEach`, `afterAll`, `afterEach`, `fn`, `spy`, `stub`, `mock`, `fakeClock`, `restoreMocks`, `restoreTimers` under both conditions.
#### Scenario: Vitest consumer
- **WHEN** a consumer running under vitest imports `{ describe, it, expect, vi }` from `@suaveplan/testing-dom`
- **THEN** the symbols come from `vitest`
#### Scenario: bun-test consumer
- **WHEN** a consumer running under `bun test` imports the same names
- **THEN** the symbols come from `bun:test`
### Requirement: `createTestConfig()` produces WSL2-safe configs
The factory SHALL emit a vitest config with `pool: "forks"`, `setupFiles: ["@suaveplan/testing-dom/polyfills"]`, `fileParallelism: false` when `environment === "happy-dom"`, and dynamically-loaded `@vitejs/plugin-react` if that optional peer is installed. When the caller's runner is `bun:test`, the factory SHALL return a `BunTestConfig` instead.
#### Scenario: Happy-dom config
- **WHEN** `createTestConfig({ environment: "happy-dom" })` is awaited and `@vitejs/plugin-react` is present
- **THEN** the returned config includes the React plugin, has `pool === "forks"`, `fileParallelism === false`, and `setupFiles` includes the polyfills path
#### Scenario: Missing React peer doesn't break non-React consumers
- **WHEN** `createTestConfig({ environment: "happy-dom" })` is awaited and the React peer is NOT installed
- **THEN** the factory returns the vitest config without the plugin, no error thrown
### Requirement: Polyfills are side-effect-only
The `@suaveplan/testing-dom/polyfills` subpath MUST resolve to a side-effecting module that installs `globalThis.crypto` (when missing), `globalThis.TextEncoder`, and `globalThis.TextDecoder`. The module MUST export nothing.
#### Scenario: Idempotent polyfill install
- **WHEN** `import "@suaveplan/testing-dom/polyfills"` runs twice in the same process
- **THEN** the second import is a no-op (no errors, no overwrites)
### Requirement: `renderComponent` wraps React render with cleanup and userEvent
`renderComponent(ui, options?)` SHALL render the React tree using `@testing-library/react`'s `render`, auto-register cleanup on `afterEach`, and return the standard `RenderResult` extended with a `userEvent` instance.
#### Scenario: Render and event dispatch
- **WHEN** `renderComponent(<Button />)` is called and the returned `userEvent.click(getByRole('button'))` is awaited
- **THEN** the click is dispatched and observable side-effects run
#### Scenario: Render failure produces RenderError
- **WHEN** the React tree throws synchronously during render
- **THEN** `renderComponent` throws `RenderError` with `code === "TESTING_DOM.RENDER.SETUP_FAILED"` and `cause` set to the original error
### Requirement: jest-dom matchers extension
Importing `@suaveplan/testing-dom/matchers` SHALL extend vitest's `expect` with the `@testing-library/jest-dom` matcher set. Common matchers MUST be available afterward: `toBeInTheDocument`, `toHaveTextContent`, `toBeDisabled`, `toBeEnabled`, `toHaveAttribute`, `toBeVisible`, `toHaveClass`, `toHaveValue`.
#### Scenario: Matcher availability after side-effect import
- **WHEN** a test file does `import "@suaveplan/testing-dom/matchers"` and then asserts `expect(node).toBeInTheDocument()`
- **THEN** the assertion runs without "matcher does not exist" errors
### Requirement: Structured errors with stable codes
All errors thrown from `@suaveplan/testing-dom` MUST be subclasses of `TestingDomError extends Error` carrying `{ code, cause?, context? }`. Codes match `^TESTING_DOM\.[A-Z]+\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any helper throws
- **THEN** the thrown value `instanceof TestingDomError` is true AND `err.code` matches the regex
### Requirement: Coverage and docs gates
Istanbul statement + branch coverage across `packages/universal/testing-dom/src/` (excluding `*.bench.ts`, `*.schemas.ts`, `runner/bun-test.ts`) MUST be ≥ 95% on every dimension. Every implementation file (`*.ts` other than test/bench/schemas/types-in-feature-dirs/index barrels) MUST have a sibling `.md` of ≥ 200 words with simple + advanced examples.
#### Scenario: CI coverage gate
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/testing-dom` runs
- **THEN** statement, branch, function, and line coverage all ≥ 95%
### Requirement: Test placement follows the genesis hard rule
Per-feature unit tests SHALL be co-located in `src/` (named `<feature>.test.ts` next to `<feature>.ts`). Cross-cutting tests (dep-graph guard, tree-shake guard, no-disallowed grep guard, property tests) MUST live in `tests/` sibling to `src/`.
#### Scenario: Per-feature unit tests live next to source
- **WHEN** a directory under `src/` contains a feature module like `render/render-component.ts`
- **THEN** its unit tests are at `src/render/render-component.test.ts` (sibling) AND not in `tests/`
#### Scenario: Cross-cutting tests live in tests/
- **WHEN** a test exercises package-level concerns (dep-graph, tree-shake, no-disallowed grep, property invariants over multiple modules)
- **THEN** it lives under `tests/` AND not in `src/`

View File

@@ -0,0 +1,78 @@
> Archived 2026-04-19: package shipped under universal/dev/testing-dom. Spec drift was rectified by `openspec/changes/testing-dom-spec-conformance/` (2026-04-26), which folded `**Verified by:**` lines into the canonical spec, pinned the dual-runner conditional-exports shape, and corrected the path reference in the coverage requirement.
## 1. Catalogs and package skeleton
- [x] 1.1 Extend root `package.json` `catalogs.testing` (or new `catalogs.dom-testing`) with `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`, `@testing-library/dom`, `@vitejs/plugin-react`. Pin versions via Bun overrides where stability matters.
- [x] 1.2 Create `packages/universal/testing-dom/` skeleton: `package.json` (version `0.1.0`, dual-runner conditional exports, runtime deps = vitest+happy-dom+testing-library+@opentelemetry/api+zod+tslib, devDep on `@suaveplan/testing`), `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`.
- [x] 1.3 Verify `bunx turbo ls --filter=@suaveplan/testing-dom...` (runtime closure) contains zero `@suaveplan/*` packages.
## 2. Migrate from staging
- [x] 2.1 Move `packages/universal/testing-dom-staging/runner/` into `packages/universal/testing-dom/src/runner/`. Update internal imports.
- [x] 2.2 Move `packages/universal/testing-dom-staging/config/` into `packages/universal/testing-dom/src/config/`.
- [x] 2.3 Move `packages/universal/testing-dom-staging/polyfills.ts` into `packages/universal/testing-dom/src/setup/polyfills.ts`.
- [x] 2.4 Update `createTestConfig`'s polyfill setupFile path to `@suaveplan/testing-dom/polyfills`.
- [x] 2.5 Delete `packages/universal/testing-dom-staging/` after migration.
## 3. Local error hierarchy
- [x] 3.1 Implement `src/errors/testing-dom-error.ts` with `TestingDomError` base + `RenderError`, `MatcherError`, `ConfigError`, `AbortedError` subclasses.
- [x] 3.2 Implement `src/errors/codes.ts` enumerating `TESTING_DOM.<DOMAIN>.<REASON>` constants.
- [x] 3.3 Update migrated `createTestConfig` to throw `ConfigError` instead of the old `ConfigError` from `@suaveplan/testing`.
- [x] 3.4 Co-locate tests + `.md` for the error module.
## 4. Render helpers
- [x] 4.1 Implement `src/render/render-component.ts` with `renderComponent(ui, options?)` — wraps `@testing-library/react`'s `render`, returns `RenderResult` extended with `userEvent`.
- [x] 4.2 Auto-register cleanup via vitest's `afterEach` (per-test-file, not global to avoid leak between unrelated suites).
- [x] 4.3 Throws `RenderError("TESTING_DOM.RENDER.SETUP_FAILED")` on render-time exceptions.
- [x] 4.4 Co-locate tests + `.md` + `.schemas.ts`.
## 5. Query aliases
- [x] 5.1 Implement `src/queries/queries.ts` re-exporting `screen`, `within`, and accessibility-first aliases (`byRole`, `byLabelText`, `byText`, `byPlaceholderText`, `findRole`, `queryRole`, etc.).
- [x] 5.2 Document the testid-as-fallback policy in the module's `.md`.
## 6. User events
- [x] 6.1 Implement `src/user/setup-user.ts`: `setupUser(options?)` returning a `UserEvent` instance with happy-dom-friendly defaults (`pointerEventsCheck: 0`).
- [x] 6.2 If `vi.isFakeTimers()` returns true at setup time, wire `advanceTimers: vi.advanceTimersByTime` automatically.
- [x] 6.3 Co-locate tests + `.md`.
## 7. jest-dom matchers
- [x] 7.1 Implement `src/matchers/matchers.ts` — side-effecting import that calls `expect.extend(...)` with the jest-dom matcher set.
- [x] 7.2 Re-export under subpath `@suaveplan/testing-dom/matchers`.
- [x] 7.3 Smoke test: import the matchers module, render a node, assert `toBeInTheDocument`.
## 8. OTEL instrumentation (gated)
- [x] 8.1 Implement `src/observability/instrumented.ts` — counters/histograms gated behind `process.env.TESTING_DOM_OTEL_ENABLED === "1"`. No-op otherwise.
- [x] 8.2 Wrap `renderComponent` and `setupUser` with the gated instrumentation.
## 9. Tests + cross-cutting guards
- [x] 9.1 Co-located unit tests for every public module in `src/`.
- [x] 9.2 `tests/dep-graph.test.ts` — assert no `@suaveplan/*` runtime deps.
- [x] 9.3 `tests/no-disallowed.test.ts` — `any` / `ts-ignore` / `TODO` etc. grep guard.
- [x] 9.4 `tests/tree-shake.test.ts` — verify subpaths don't drag unrelated modules.
- [x] 9.5 `tests/properties.test.ts` — fast-check property tests for any pure helpers (matcher predicates, query name normalization).
- [x] 9.6 Coverage ≥ 95% on every dimension (statements, branches, functions, lines).
## 10. Docs and examples
- [x] 10.1 Every implementation file has a sibling `.md` with simple + advanced examples (≥ 200 words).
- [x] 10.2 Root `README.md` covers install, quick usage (vitest config, basic component test), error code index, OTEL toggle.
- [x] 10.3 `examples/basic-component.tsx` and `examples/with-providers.tsx` runnable scripts.
## 11. Pre-publish gate
- [x] 11.1 `bunx turbo lint typecheck test --filter=@suaveplan/testing-dom` green.
- [x] 11.2 `bunx turbo test:coverage --filter=@suaveplan/testing-dom` ≥ 95% on every dimension.
- [x] 11.3 `openspec validate enterprise-testing-dom --type change` passes.
## 12. Publish (USER APPROVAL REQUIRED)
- [x] 12.1 Surface the audit (test count, coverage, lint clean) and STOP.
- [x] 12.2 On approval: `bun scripts/publish.ts --package @suaveplan/testing-dom`.
- [x] 12.3 Archive change to `openspec/archive/<yyyy-mm-dd>-enterprise-testing-dom/`.