@jterrazz/test 16.0.0 → 16.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/dist/appium.adapter.js +59 -4
  2. package/dist/browser/index.js +203 -42
  3. package/dist/checker.js +305 -63
  4. package/dist/index.d.ts +35 -4
  5. package/dist/index.js +47 -24
  6. package/dist/oxlint.cjs +123 -28
  7. package/dist/oxlint.js +123 -28
  8. package/dist/playwright.adapter.js +88 -50
  9. package/dist/substring-warning.js +282 -0
  10. package/dist/vitest.js +91 -12
  11. package/docs/01-architecture.md +95 -0
  12. package/docs/02-developing.md +351 -0
  13. package/docs/03-testing.md +495 -0
  14. package/docs/04-operating.md +101 -0
  15. package/docs/05-module-tests.md +152 -0
  16. package/docs/06-integration.md +180 -0
  17. package/docs/07-component.md +246 -0
  18. package/docs/08-website.md +404 -0
  19. package/docs/09-mobile.md +270 -0
  20. package/docs/10-api.md +264 -0
  21. package/docs/11-jobs.md +157 -0
  22. package/docs/12-cli.md +677 -0
  23. package/docs/13-elements.md +248 -0
  24. package/docs/14-assertions.md +314 -0
  25. package/docs/15-tokens.md +253 -0
  26. package/docs/16-contracts.md +438 -0
  27. package/docs/17-services.md +188 -0
  28. package/docs/18-conventions.md +332 -0
  29. package/docs/19-linting.md +512 -0
  30. package/docs/README.md +36 -0
  31. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  32. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  33. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  34. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  35. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  36. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  37. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  38. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  39. package/docs/decisions/_template.md +22 -0
  40. package/docs/reference/classes/Matcher.md +120 -0
  41. package/docs/reference/classes/ProcessHandle.md +368 -0
  42. package/docs/reference/functions/defineContract.md +39 -0
  43. package/docs/reference/functions/defineContracts.md +36 -0
  44. package/docs/reference/functions/disabled.md +28 -0
  45. package/docs/reference/functions/enabled.md +20 -0
  46. package/docs/reference/functions/focused.md +26 -0
  47. package/docs/reference/functions/mockOf.md +79 -0
  48. package/docs/reference/functions/postgres.md +31 -0
  49. package/docs/reference/functions/process.md +33 -0
  50. package/docs/reference/functions/redis.md +28 -0
  51. package/docs/reference/functions/required.md +41 -0
  52. package/docs/reference/functions/selected.md +26 -0
  53. package/docs/reference/functions/sqlite.md +33 -0
  54. package/docs/reference/functions/testId.md +20 -0
  55. package/docs/reference/functions/text.md +40 -0
  56. package/docs/reference/functions/valued.md +28 -0
  57. package/docs/reference/functions/waitUntil.md +46 -0
  58. package/docs/reference/functions/within.md +29 -0
  59. package/docs/reference/index.md +196 -0
  60. package/docs/reference/interfaces/BaseResult.md +87 -0
  61. package/docs/reference/interfaces/CallResult.md +160 -0
  62. package/docs/reference/interfaces/CaptureScope.md +82 -0
  63. package/docs/reference/interfaces/CliResult.md +253 -0
  64. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  65. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  66. package/docs/reference/interfaces/FetchResult.md +180 -0
  67. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  68. package/docs/reference/interfaces/HttpResult.md +126 -0
  69. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  70. package/docs/reference/interfaces/PageResult.md +356 -0
  71. package/docs/reference/interfaces/RenderResult.md +72 -0
  72. package/docs/reference/interfaces/RenderedText.md +110 -0
  73. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  74. package/docs/reference/interfaces/ScreenResult.md +128 -0
  75. package/docs/reference/interfaces/TableAccessor.md +26 -0
  76. package/docs/reference/interfaces/TextAccessor.md +96 -0
  77. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  78. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  79. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  80. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  81. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  82. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  83. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  84. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  85. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  86. package/docs/reference/type-aliases/CliEnv.md +11 -0
  87. package/docs/reference/type-aliases/CliHandle.md +80 -0
  88. package/docs/reference/type-aliases/CliInput.md +36 -0
  89. package/docs/reference/type-aliases/CliOutput.md +46 -0
  90. package/docs/reference/type-aliases/CliPort.md +58 -0
  91. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  92. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  93. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  94. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  95. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  96. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  97. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  98. package/docs/reference/type-aliases/Contract.md +68 -0
  99. package/docs/reference/type-aliases/ContractInput.md +13 -0
  100. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  101. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  102. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  103. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  104. package/docs/reference/type-aliases/Contracts.md +48 -0
  105. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  106. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  107. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  108. package/docs/reference/type-aliases/DevicePort.md +51 -0
  109. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  110. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  111. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  112. package/docs/reference/type-aliases/DomMount.md +21 -0
  113. package/docs/reference/type-aliases/ElementKind.md +27 -0
  114. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  115. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  116. package/docs/reference/type-aliases/ElementRef.md +128 -0
  117. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  118. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  119. package/docs/reference/type-aliases/HonoApp.md +31 -0
  120. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  121. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  122. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  123. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  124. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  125. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  126. package/docs/reference/type-aliases/Intercept.md +19 -0
  127. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  128. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  129. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  130. package/docs/reference/type-aliases/JobHandle.md +34 -0
  131. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  132. package/docs/reference/type-aliases/JobsResult.md +16 -0
  133. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  134. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  135. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  136. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  137. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  138. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  139. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  140. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  141. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  142. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  143. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  144. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  145. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  146. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  147. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  148. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  149. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  150. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  151. package/docs/reference/type-aliases/MockPort.md +10 -0
  152. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  153. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  154. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  155. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  156. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  157. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  158. package/docs/reference/type-aliases/ServerPort.md +36 -0
  159. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  160. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  161. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  162. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  163. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  164. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  165. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  166. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  167. package/docs/reference/type-aliases/SpecKind.md +8 -0
  168. package/docs/reference/type-aliases/SpecRun.md +108 -0
  169. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  170. package/docs/reference/type-aliases/SpecStream.md +32 -0
  171. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  172. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  173. package/docs/reference/type-aliases/SseEvent.md +58 -0
  174. package/docs/reference/type-aliases/StreamBody.md +61 -0
  175. package/docs/reference/type-aliases/TextFilter.md +22 -0
  176. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  177. package/docs/reference/type-aliases/Visitor.md +215 -0
  178. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  179. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  180. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  181. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  182. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  183. package/docs/reference/variables/anthropic.md +19 -0
  184. package/docs/reference/variables/banner.md +10 -0
  185. package/docs/reference/variables/button.md +10 -0
  186. package/docs/reference/variables/clock.md +16 -0
  187. package/docs/reference/variables/complementary.md +10 -0
  188. package/docs/reference/variables/component.md +10 -0
  189. package/docs/reference/variables/content.md +10 -0
  190. package/docs/reference/variables/contentinfo.md +10 -0
  191. package/docs/reference/variables/dialog.md +10 -0
  192. package/docs/reference/variables/field.md +10 -0
  193. package/docs/reference/variables/form.md +10 -0
  194. package/docs/reference/variables/heading.md +10 -0
  195. package/docs/reference/variables/http.md +40 -0
  196. package/docs/reference/variables/intercept.md +11 -0
  197. package/docs/reference/variables/link.md +10 -0
  198. package/docs/reference/variables/listitem.md +10 -0
  199. package/docs/reference/variables/main.md +10 -0
  200. package/docs/reference/variables/match.md +8 -0
  201. package/docs/reference/variables/navigation.md +10 -0
  202. package/docs/reference/variables/openai.md +21 -0
  203. package/docs/reference/variables/option.md +17 -0
  204. package/docs/reference/variables/region.md +10 -0
  205. package/docs/reference/variables/row.md +10 -0
  206. package/docs/reference/variables/search.md +10 -0
  207. package/docs/reference/variables/specification.md +8 -0
  208. package/docs/reference/variables/status.md +10 -0
  209. package/docs/reference/variables/table.md +10 -0
  210. package/package.json +3 -2
  211. package/dist/ambiguity.js +0 -146
@@ -0,0 +1,246 @@
1
+ # 07 — Component specs (`component`)
2
+
3
+ `component` specifies a rendered UNIT — a React component, a React hook through a Host written in the test, or a plain DOM function — in a real headless Chromium, the same browser [08 — Website specs](08-website.md) already drives through the same `playwright` peer.
4
+
5
+ Use it when the subject is one thing that draws itself. For a whole served page use [website](08-website.md); for a module that draws nothing, a module test beside it needs no runner at all.
6
+
7
+ Why a facet on Vitest Browser Mode, and what was weighed against it: [ADR-003](decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md).
8
+
9
+ | The shape | Held below |
10
+ | ------------------- | -------------------------------------------------------------------------------- |
11
+ | What it specifies | [What it specifies](#what-it-specifies) |
12
+ | What it refuses | [What it does not do](#what-it-does-not-do) |
13
+ | Where it is written | [The constructor: there is none](#the-constructor-there-is-none) |
14
+ | The chain | [The chain](#the-chain) — `intercept` · `wrap` · `clock` · `viewport` · `render` |
15
+ | The result | [The result](#the-result) — `tree` `content` `html` `console` `errors` |
16
+ | What only it does | [Unique here](#unique-here) |
17
+ | What it will refuse | [Pitfalls](#pitfalls) |
18
+
19
+ ## What it specifies
20
+
21
+ A component test answers one question: **given this network and this parent, what does the component put on the screen, and what does it report back?** Everything it reads is what a user or a parent can see — the accessibility tree, the rendered text, the markup, the console — and everything it does is what a user or a parent can do: click, fill, press, change the props, take it off the screen.
22
+
23
+ It is a UNIT, so it lives beside its subject:
24
+
25
+ ```
26
+ src/presentation/posts/
27
+ ├── post-table.tsx
28
+ ├── post-table.test.tsx # the component's spec — the `.tsx` suffix IS the kind
29
+ ├── use-modal-dialog.ts
30
+ └── use-modal-dialog.test.tsx # a hook's spec: its Host is written in the test file
31
+ ```
32
+
33
+ The suffix decides which project collects the file and therefore which rules judge it: `unit()` takes `**/*.test.ts` and excludes `**/*.test.tsx`; `component()` takes the `.tsx`. A rendered thing in a `.test.ts` is rule G4's error, and it names the move.
34
+
35
+ ## What it does not do
36
+
37
+ The subject is a unit that draws itself IN A BROWSER, which leaves four neighbours outside the facet — each with a route, so none of them is a hole.
38
+
39
+ | Not this | Because | Route |
40
+ | -------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
41
+ | A React Native screen | The renderer is a real Chromium, and a native view never reaches one | jest stays, until the `react-native-web` spike says otherwise |
42
+ | An `.astro` page | A page is the assembled product, met through an address | a website spec — [08 — Website specs](08-website.md) |
43
+ | A React island inside an `.astro` page | The island is a rendered unit; only the file around it is not | a `.test.tsx` beside the island, here |
44
+ | A Next.js server component | It renders on the server and reads what the server reads — there is no tree to mount | a website spec, through the served page |
45
+
46
+ **`vite` as a framework function is BACKLOG, not a shape to reach for.** `component({ vite })` accepts the function a framework exports, and Astro's `getViteConfig()` is one — but no spike has yet proven that pipeline under this facet: the cross-repo probe that suggested it was confounded by two Vite versions, and the proof owed is an in-repo spike inside a `jterrazz-web` worktree. Until it lands, state the app's own `vite.config.ts` and let the allow-list take the pipeline from it.
47
+
48
+ ## The constructor: there is none
49
+
50
+ Every other facet starts something — a server, a database, a binary, a simulator — so every other facet has a `*.specification.ts` holding what was started. A component has nothing to start. What a specification file WOULD have held splits by owner:
51
+
52
+ | What | Whose | Where |
53
+ | ------------------------------------------------------ | ---------- | --------------------------------------------------------------- |
54
+ | The providers every render is dressed in | the app's | `component({ wrap })` in `vitest.config.ts` |
55
+ | The Vite pipeline the app builds with | the app's | `component({ vite })` |
56
+ | The viewport, the timezone, the locale | the app's | `component({ viewport, timezone, locale })` |
57
+ | The contracts, the wrapper, the instant, the page size | one test's | the chain: `.intercept()`, `.wrap()`, `.clock()`, `.viewport()` |
58
+
59
+ ```typescript
60
+ // apps/console/vitest.config.ts
61
+ import { component, defineSpecConfig, unit } from '@jterrazz/test/vitest';
62
+
63
+ export default defineSpecConfig({
64
+ test: {
65
+ projects: [
66
+ unit({ roots: ['src', 'web'] }),
67
+ component({ vite: './web/vite.config.ts', wrap: './web/src/providers.tsx' }),
68
+ ],
69
+ },
70
+ });
71
+ ```
72
+
73
+ **A relative `vite` or `wrap` is the CONFIG's path, never the cwd's.** Both are resolved against the directory of the config that called `component()`, so the two paths above mean `apps/console/web/…` whoever loaded the file — the runner from the app, knip and `typescript check` from the repository root, where every config of a repository is read. State `root` when the anchor cannot be read that way: a call written outside the config it configures (a shared config factory), or a runner started in another tree entirely (`vitest --config apps/console/vitest.config.ts` from the root).
74
+
75
+ `wrap` is a MODULE PATH whose default export is `(ui) => ReactNode` — a page imports a URL, not a function, so the providers cross as a setup file the project generates under `.artifacts/`. A router does not belong here: which routes exist is a test's Given, and it goes on the chain.
76
+
77
+ | Option | Description |
78
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
79
+ | `wrap` | Module path, default-exporting `(ui) => ReactNode` — the app's own providers, around every render of the project |
80
+ | `vite` | The app's pipeline: a config path, the object, or the function a framework exports. Only the PIPELINE is adopted (below) |
81
+ | `clock` | An ISO instant pinned for every render — a project whose components stamp the time |
82
+ | `viewport` | `{ width, height }` of the page. Default `1280 × 720` |
83
+ | `timezone` / `locale` | The browser context's. Defaults `'UTC'` and `'en-US'` |
84
+ | `root` | The directory `vite` and `wrap` are resolved against. Default: the directory of the config that called `component()` |
85
+ | `include` / `exclude` | The globs. Default `['**/*.test.tsx']`, kept out of `specs/` — state your own `include` and the exclusion is yours too |
86
+ | `timeout` | Raise (or lower) the preset's 30 s for this project alone |
87
+ | `serial` | `fileParallelism: false` — the project's renders run one file at a time |
88
+
89
+ **Only the pipeline of `vite` is adopted.** An app's config says where the app LIVES — its `root`, its `build`, its `server.port`, its `publicDir` — and adopting those re-roots the run so the project collects nothing. The kept keys are an allow-list (`plugins`, `resolve`, `esbuild`/`oxc`, `css`, `define`, `assetsInclude`, `envPrefix`, `json`), and the consumer's `plugins` are concatenated after the seam's, never assigned over them.
90
+
91
+ **One transformer key, chosen from the installed Vite.** Vite 8 compiles with oxc and warns for every `esbuild` option handed to it beside an `oxc` one; Vite 6 and 7 compile with esbuild and have no `oxc` key at all. `component()` reads the installed major, states the JSX default on the key that Vite actually reads, and drops the other from the adopted pipeline — and the one it states yields to yours: a `jsxImportSource` or a classic runtime is the app's statement, not the seam's to overwrite.
92
+
93
+ **A `resolve.alias` is an unlisted dependency.** An alias tells the bundler where a specifier resolves; it tells knip nothing, so a package reached only through an alias reads as undeclared, and a `paths`-less `tsconfig` makes the compiler disagree with the bundler besides. Mirror the alias in the `tsconfig`'s `paths` (the compiler and the bundler then answer alike), or name the package in `knip.ignoreDependencies` with the alias as the reason.
94
+
95
+ ## The chain
96
+
97
+ ```tsx
98
+ import { button, component, content, defineContract, http, status } from '@jterrazz/test';
99
+ import { expect, test } from 'vitest';
100
+
101
+ import { PostTable } from './post-table.js';
102
+
103
+ const listing = defineContract({
104
+ request: http.get('/api/posts'),
105
+ response: http.json({ posts: [{ id: 1, title: 'A first post' }], total: 200 }),
106
+ });
107
+
108
+ test('says how much of the collection the table is showing', async () => {
109
+ // Given - a page of one row answering for a collection of two hundred
110
+ const result = await component.intercept(listing).render(<PostTable />, async (visitor) => {
111
+ await visitor.see(content('Showing 1 of 200 posts'));
112
+ });
113
+
114
+ // Then - the table is qualified by what it is not showing
115
+ await expect(result.tree).toMatch('one-of-two-hundred.aria.yaml');
116
+ await expect(result.console).toBeEmpty();
117
+ });
118
+ ```
119
+
120
+ Every setup returns a NEW chain, so the handle a spec imports never carries the previous test's state.
121
+
122
+ | Setup | Does |
123
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
+ | `.intercept(…)` | Declares what the network replies — the same contracts and the same queue as every other facet |
125
+ | `.wrap(fn)` | Wraps every render of this chain: a router stub, a `<StrictMode>`, a provider one test needs |
126
+ | `.clock(iso)` | Pins the page's `Date` for this render |
127
+ | `.viewport(size)` | The page size THIS render gets — the Given of a component that reads `matchMedia` or a container query. The project's size is restored when the test ends |
128
+
129
+ | Terminal action | Mounts |
130
+ | --------------------------- | ----------------------------------------------------------------------------- |
131
+ | `.render(<X />, scenario?)` | A React tree |
132
+ | `.render((container) => …)` | A DOM function, handed a fresh container; its optional return is the teardown |
133
+
134
+ The scenario is the When, and assertions stay in the Then (rule W1). Its verbs are the page's — `click` `fill` `check` `select` `hover` `press` `see` `gone`, each owned and described by [13 — Elements § Verbs](13-elements.md#verbs) — plus the two only a parent has:
135
+
136
+ | Verb | Description |
137
+ | -------------- | --------------------------------------------------------- |
138
+ | `rerender(ui)` | What a PARENT does: new props, same mount |
139
+ | `unmount()` | What a parent does when it takes the thing off the screen |
140
+
141
+ `goto()` does not exist here, because a component has no address. The element vocabulary is the page's too, modifiers included: `see(focused(button('Open')))` asks where the keyboard is, `see(disabled(button('Publish')))` whether a control takes input, `see(selected(option('LinkedIn')))` which option a select is on, and `see(valued(field('Title'), 'Launch teaser'))` what a field holds. An option is asserted by PRESENCE — a collapsed select draws none of them — and [13 — Elements](13-elements.md) owns them all, including the W3 refusal and the exact-name rule.
142
+
143
+ ## The result
144
+
145
+ | Member | Type | Description |
146
+ | ---------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
147
+ | `result.tree` | `TextAccessor` | The ARIA snapshot of the rendered body — the outline a golden pins |
148
+ | `result.content` | `TextAccessor` | The RENDERED text of the document body (`innerText`) — what a reader sees, so a stylesheet's source and a node the page does not display are not in it |
149
+ | `result.html` | `TextAccessor` | The mounted markup — the escape hatch for a class or an attribute |
150
+ | `result.console` | `TextAccessor` | Every console message, one `[type] text` line per message |
151
+ | `result.errors` | `TextAccessor` | Console errors only, plus any uncaught page error |
152
+
153
+ `toMatch` is **awaited** on all five: a golden captured inside a page is read back through a server command, so the file crosses the browser seam. Which matchers are IO — and therefore awaited — is [14 — Assertions](14-assertions.md)'s to state; a directory subject is the other one, and it is awaited for the same kind of reason (it walks a disk).
154
+
155
+ ```typescript
156
+ await expect(result.tree).toMatch('two-of-two-hundred.aria.yaml');
157
+ expect(result.html).toContain('row row--live');
158
+ await expect(result.errors).toBeEmpty();
159
+ ```
160
+
161
+ The ARIA tree is the golden a rendered surface wants: it is the outline a screen reader walks, it is deterministic where a screenshot is not, and it is the SAME dialect [08 — Website specs](08-website.md) produces for a whole page. Goldens live in `_expected/` beside the test and are written by `TEST_UPDATE=1`, exactly as everywhere else ([15 — Tokens](15-tokens.md)).
162
+
163
+ ## Unique here
164
+
165
+ - **The hook recipe.** A hook has no surface, so its Given is a **Host component written in the test file**, carrying the states the test needs — an opener, a fallback target, a handler that can be swapped. The visitor drives the Host; `rerender` swaps the handler; `unmount` is the parent walking away. There is no hook-specific runner and there is no need for one.
166
+
167
+ ```tsx
168
+ import { button, component, dialog, focused } from '@jterrazz/test';
169
+ import { useState } from 'react';
170
+ import { expect, test, vi } from 'vitest';
171
+
172
+ // The Given of a hook spec is a Host, written HERE: the component carrying the
173
+ // States the test needs is part of the test, not of the app.
174
+ function Host({ onClose }: { onClose: () => void }) {
175
+ const [open, setOpen] = useState(false);
176
+
177
+ const show = (): void => {
178
+ setOpen(true);
179
+ };
180
+
181
+ const dismiss = (): void => {
182
+ setOpen(false);
183
+ onClose();
184
+ };
185
+
186
+ if (!open) {
187
+ return (
188
+ <button onClick={show} type="button">
189
+ Open
190
+ </button>
191
+ );
192
+ }
193
+
194
+ return (
195
+ <dialog aria-label="Host" open>
196
+ <button onClick={dismiss} type="button">
197
+ Close
198
+ </button>
199
+ </dialog>
200
+ );
201
+ }
202
+
203
+ test('gives the keyboard back to whatever opened it', async () => {
204
+ // Given - a dialog opened from a button and then dismissed
205
+ const onClose = vi.fn<() => void>();
206
+
207
+ await component.render(<Host onClose={onClose} />, async (visitor) => {
208
+ await visitor.click(button('Open'));
209
+ await visitor.see(dialog('Host'));
210
+ await visitor.click(button('Close'));
211
+ await visitor.gone(dialog('Host'));
212
+ await visitor.see(focused(button('Open')));
213
+ });
214
+
215
+ // Then - the owner heard the dismissal exactly once
216
+ expect(onClose).toHaveBeenCalledOnce();
217
+ });
218
+ ```
219
+
220
+ - **A DOM function is still a component.** `renderSessionRows(container, rows)` renders; it just does not use React. `.render((container) => …)` mounts it in the same Chromium under the same visitor, and `result.html` reads what it drew. No React is loaded. Write the body BRACED: a concise arrow hands back the void the render function returns, and `typescript/no-confusing-void-expression` refuses that.
221
+
222
+ ```tsx
223
+ const result = await component.render((container) => {
224
+ renderSessionRows(container, rows);
225
+ });
226
+ ```
227
+
228
+ - **A router is a test's Given.** cap01's and the console's screens render `<Link>` and read `useParams()`; without a router they throw. `createRoutesStub([...])` on `.wrap()` is that Given — React Router's own guidance, and the fork's: a route module is a website spec, a presentation component is a component spec, a loader is a module test.
229
+ - **Strictness is total.** A component that fetches something no contract declared fails the render, naming the request — including a component given NO contract at all, which is how "this subject has no network" is said. There is no bypass and no default handler. The api facet's D7 stops at a known scope because it shares a process with everything else the test does; a page does not, so here it is total. It follows that a module-scope `intercept()` opened around a `.render()` is shadowed: the render's own registration is installed last and answers everything. Declare a render's network on the chain; `intercept()` in a `.test.tsx` is for code the test calls itself ([16](16-contracts.md#intercept--the-same-double-with-no-chain)).
230
+ - **The clock is the page's.** `.clock(iso)` pins `Date` and nothing else: faking the page's timers would stop React's scheduler and nothing would ever render.
231
+
232
+ ## Pitfalls
233
+
234
+ - **Reaching for `@testing-library/react` or `happy-dom`.** Rule F6 refuses both, and the second is the reason the facet exists: a simulated DOM behaves almost like a browser, and what ships is judged by a real one.
235
+ - **Writing a `*.specification.ts` for it.** There is nothing to start. The project holds what every render shares; the chain holds what one test needs.
236
+ - **Hand-rolling `browser:` in a config.** Rule E6 refuses it: the provider is pinned to the runner's EXACT version, the service worker is served from the framework's own install, the JSX transform follows the Vite in use, and a cold dependency cache must be pre-bundled or the first run reloads the tester under the test. `component()` owns all four.
237
+ - **Putting a rendered test in a `.test.ts`.** Rule G4 refuses the DOM globals it would need. Rename it `.test.tsx` beside its component; the project that collects it opens a browser.
238
+ - **Asserting focus with a golden.** The accessibility tree carries no focus state, so a tree snapshot proves nothing about the keyboard. `see(focused(x))` / `gone(focused(x))` are the verbs that do.
239
+ - **Expecting `toMatch` to be synchronous here.** It is not: `await expect(result.tree).toMatch('…')`. A bare call still runs the assertion, but off the test's lifetime: a failure surfaces as an `Unhandled Errors` entry attributed to no test, the test itself is reported PASSED, and the run exits non-zero anyway. Under `TEST_UPDATE=1` the write happens outside the test too.
240
+ - **Reading the W3 refusal as a suggestion.** When two elements carry the SAME whole name, the refusal names the accessible name each candidate matched on and offers a landmark; read that before scoping, because the landmark it offers is the one that narrows, not necessarily the one you meant. Exact-by-default and its `{ exact: false }` escape are [13 — Elements § A name designates the accessible name WHOLE](13-elements.md#a-name-designates-the-accessible-name-whole)'s.
241
+ - **Pointing `wrap` at a router.** `wrap` is the app's frame, around every render of the project. Which routes exist changes per test, so it belongs on the chain.
242
+ - **Giving the project the app's whole Vite config and expecting the app's root.** Only the pipeline is adopted; the project's root stays the directory of `vitest.config.ts`, which is what makes its `include` globs mean what they say.
243
+
244
+ ## Related
245
+
246
+ [02 — Developing](02-developing.md) · [14 — Assertions](14-assertions.md) · [16 — Contracts](16-contracts.md) · [18 — Conventions](18-conventions.md) · [08 — Website specs](08-website.md) · [ADR-003 — a rendered component is a facet on Vitest Browser Mode](decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md)