3.9 KiB
3.9 KiB
testing-dom: runner generics + matchers ambient + async-span hardening
Package: @suaveplan/testing-dom
Status: PROPOSED
Type: bug-fix
Why
Three latent defects in @suaveplan/testing-dom block correct downstream typing and produce wrong observability data:
- Runner-generic erasure on the bun-test path.
src/runner/bun-test.ts#fn<TArgs,TReturn>returnsReturnType<typeof bunMock>— an opaque type. The vitest path returnsMockand (after this change)RunnerMock<TArgs, TReturn>. A consumer writingconst m = fn<[string], number>()gets a typed callable under vitest and an opaque mock under bun-test, breaking the dual-runner contract. - jest-dom matchers have no ambient type augmentation.
src/matchers/matchers.tscallsexpect.extend(matchers)at runtime but consumers seeProperty 'toBeInTheDocument' does not existbecause the file never pulls in the@testing-library/jest-domambient module declaration. instrumented()ends spans synchronously even for async fns. Async-wrapped functions get a span duration of ~0 ms regardless of how long they actually run, corrupting every histogram. PlussetupUserhard-importsvitest, defeating the bun-test conditional path.
What changes
- ADDED:
RunnerMock<TArgs, TReturn>type alias — exported from./runner/index.js,./runner/vitest.js,./runner/bun-test.js, and the package barrel./index.js. Mirrors on both runner paths. - ADDED:
SpyHandlere-export at the package barrel. - MODIFIED:
fn<TArgs,TReturn>()(both runner paths) — now returnsRunnerMock<TArgs, TReturn>carrying the precise call signature. - MODIFIED:
spy<T,K>()andstub<T,K>()on the bun-test path — returnSpyHandle(alias forReturnType<typeof bunSpyOn>). - MODIFIED:
src/matchers/matchers.ts— addsimport type {} from "@testing-library/jest-dom"so the ambient module declaration ships with the side-effect import. - MODIFIED:
src/user/setup-user.ts— routesvithrough../runner/index.jsinstead of hard-importingvitest. - MODIFIED:
src/observability/instrumented.ts— detects PromiseLike return values and ends the OTel span (and records the histogram) only after the promise settles. - MODIFIED:
openspec/specs/testing-dom/spec.md— coverage gate raised from ≥95% to 100% to matchCLAUDE.md's pre-publish gate. Path reference updated frompackages/universal/testing-dom/topackages/dev/testing-dom/.
Out of scope
- Re-organising the runner barrel — the existing dual-runner conditional-exports shape is preserved.
- Refactoring
instrumented()away fromtracer.startActiveSpan— keeping the same OTel surface, only fixing span lifetime. - Introducing a runner-detection helper inside
setupUser— the runner adapter already exists.
Verification approach
src/runner/runner.spec-conformance.test.ts— pinsfn<TArgs,TReturn>()typing on the vitest path AND symmetric typing on the bun-test path viaimport type; pins theRunnerMock/SpyHandleexports at both runner barrels and the package barrel. UsesexpectTypeOf.src/matchers/matchers.spec-conformance.test.tsx— pinsexpectTypeOf(expect(el).toBeInTheDocument).toBeFunction()etc., proving the ambient augmentation reached the consumer.src/user/setup-user.test.ts::"imports vi via the runner adapter (no direct vitest hard-import)"— source-level guard against regression.src/observability/instrumented.test.ts::"ends span only after async fn settles (resolved)"— observable timing assertion (elapsed >= 10ms).src/observability/instrumented.test.ts::"ends span only after async fn settles (rejected)"and"… (rejected with non-Error)"— pin the rejection branch includingrecordExceptionfor non-Error throws.
Migration impact
None — additive types and bug-fix-only behavioral change. Consumers that previously typed mocks with ReturnType<typeof fn> continue to compile.