@jterrazz/test 15.3.0 → 16.1.0

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 (222) hide show
  1. package/README.md +120 -427
  2. package/dist/appium.adapter.js +71 -15
  3. package/dist/artifacts.js +25 -1735
  4. package/dist/browser/index.js +304 -233
  5. package/dist/checker.d.ts +1 -1
  6. package/dist/checker.js +1103 -97
  7. package/dist/fixtures.js +110 -0
  8. package/dist/index.d.ts +956 -971
  9. package/dist/index.js +2159 -2281
  10. package/dist/literate.js +1747 -0
  11. package/dist/match.js +625 -84
  12. package/dist/oxlint.cjs +3585 -1480
  13. package/dist/oxlint.d.cts +60 -48
  14. package/dist/oxlint.d.ts +60 -48
  15. package/dist/oxlint.js +3585 -1480
  16. package/dist/playwright.adapter.js +51 -8
  17. package/dist/{intercept.js → server.js} +2 -2
  18. package/dist/stream.js +6 -6
  19. package/dist/{ambiguity.js → substring-warning.js} +106 -7
  20. package/dist/vitest.d.ts +67 -48
  21. package/dist/vitest.js +229 -93
  22. package/docs/01-architecture.md +95 -0
  23. package/docs/02-developing.md +351 -0
  24. package/docs/03-testing.md +495 -0
  25. package/docs/04-operating.md +101 -0
  26. package/docs/05-module-tests.md +152 -0
  27. package/docs/06-integration.md +180 -0
  28. package/docs/07-component.md +246 -0
  29. package/docs/08-website.md +404 -0
  30. package/docs/09-mobile.md +270 -0
  31. package/docs/10-api.md +264 -0
  32. package/docs/11-jobs.md +157 -0
  33. package/docs/12-cli.md +677 -0
  34. package/docs/13-elements.md +242 -0
  35. package/docs/14-assertions.md +314 -0
  36. package/docs/15-tokens.md +253 -0
  37. package/docs/16-contracts.md +438 -0
  38. package/docs/17-services.md +188 -0
  39. package/docs/18-conventions.md +305 -0
  40. package/docs/19-linting.md +510 -0
  41. package/docs/README.md +36 -0
  42. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  43. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  44. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  45. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  46. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  47. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  48. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  49. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  50. package/docs/decisions/_template.md +22 -0
  51. package/docs/reference/classes/Matcher.md +120 -0
  52. package/docs/reference/classes/ProcessHandle.md +368 -0
  53. package/docs/reference/functions/defineContract.md +39 -0
  54. package/docs/reference/functions/defineContracts.md +36 -0
  55. package/docs/reference/functions/disabled.md +28 -0
  56. package/docs/reference/functions/enabled.md +20 -0
  57. package/docs/reference/functions/focused.md +26 -0
  58. package/docs/reference/functions/mockOf.md +79 -0
  59. package/docs/reference/functions/postgres.md +31 -0
  60. package/docs/reference/functions/process.md +33 -0
  61. package/docs/reference/functions/redis.md +28 -0
  62. package/docs/reference/functions/required.md +41 -0
  63. package/docs/reference/functions/selected.md +26 -0
  64. package/docs/reference/functions/sqlite.md +33 -0
  65. package/docs/reference/functions/testId.md +20 -0
  66. package/docs/reference/functions/text.md +40 -0
  67. package/docs/reference/functions/valued.md +28 -0
  68. package/docs/reference/functions/waitUntil.md +46 -0
  69. package/docs/reference/functions/within.md +29 -0
  70. package/docs/reference/index.md +196 -0
  71. package/docs/reference/interfaces/BaseResult.md +87 -0
  72. package/docs/reference/interfaces/CallResult.md +160 -0
  73. package/docs/reference/interfaces/CaptureScope.md +82 -0
  74. package/docs/reference/interfaces/CliResult.md +253 -0
  75. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  76. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  77. package/docs/reference/interfaces/FetchResult.md +180 -0
  78. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  79. package/docs/reference/interfaces/HttpResult.md +126 -0
  80. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  81. package/docs/reference/interfaces/PageResult.md +356 -0
  82. package/docs/reference/interfaces/RenderResult.md +72 -0
  83. package/docs/reference/interfaces/RenderedText.md +110 -0
  84. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  85. package/docs/reference/interfaces/ScreenResult.md +128 -0
  86. package/docs/reference/interfaces/TableAccessor.md +26 -0
  87. package/docs/reference/interfaces/TextAccessor.md +96 -0
  88. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  89. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  90. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  91. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  92. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  93. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  94. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  95. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  96. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  97. package/docs/reference/type-aliases/CliEnv.md +11 -0
  98. package/docs/reference/type-aliases/CliHandle.md +80 -0
  99. package/docs/reference/type-aliases/CliInput.md +36 -0
  100. package/docs/reference/type-aliases/CliOutput.md +46 -0
  101. package/docs/reference/type-aliases/CliPort.md +58 -0
  102. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  103. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  104. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  105. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  106. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  107. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  108. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  109. package/docs/reference/type-aliases/Contract.md +68 -0
  110. package/docs/reference/type-aliases/ContractInput.md +13 -0
  111. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  112. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  113. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  114. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  115. package/docs/reference/type-aliases/Contracts.md +48 -0
  116. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  117. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  118. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  119. package/docs/reference/type-aliases/DevicePort.md +51 -0
  120. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  121. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  122. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  123. package/docs/reference/type-aliases/DomMount.md +21 -0
  124. package/docs/reference/type-aliases/ElementKind.md +27 -0
  125. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  126. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  127. package/docs/reference/type-aliases/ElementRef.md +128 -0
  128. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  129. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  130. package/docs/reference/type-aliases/HonoApp.md +31 -0
  131. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  132. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  133. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  134. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  135. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  136. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  137. package/docs/reference/type-aliases/Intercept.md +19 -0
  138. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  139. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  140. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  141. package/docs/reference/type-aliases/JobHandle.md +34 -0
  142. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  143. package/docs/reference/type-aliases/JobsResult.md +16 -0
  144. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  145. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  146. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  147. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  148. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  149. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  150. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  151. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  152. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  153. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  154. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  155. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  156. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  157. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  158. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  159. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  160. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  161. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  162. package/docs/reference/type-aliases/MockPort.md +10 -0
  163. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  164. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  165. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  166. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  167. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  168. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  169. package/docs/reference/type-aliases/ServerPort.md +36 -0
  170. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  171. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  172. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  173. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  174. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  175. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  176. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  177. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  178. package/docs/reference/type-aliases/SpecKind.md +8 -0
  179. package/docs/reference/type-aliases/SpecRun.md +108 -0
  180. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  181. package/docs/reference/type-aliases/SpecStream.md +32 -0
  182. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  183. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  184. package/docs/reference/type-aliases/SseEvent.md +58 -0
  185. package/docs/reference/type-aliases/StreamBody.md +61 -0
  186. package/docs/reference/type-aliases/TextFilter.md +22 -0
  187. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  188. package/docs/reference/type-aliases/Visitor.md +215 -0
  189. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  190. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  191. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  192. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  193. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  194. package/docs/reference/variables/anthropic.md +19 -0
  195. package/docs/reference/variables/banner.md +10 -0
  196. package/docs/reference/variables/button.md +10 -0
  197. package/docs/reference/variables/clock.md +16 -0
  198. package/docs/reference/variables/complementary.md +10 -0
  199. package/docs/reference/variables/component.md +10 -0
  200. package/docs/reference/variables/content.md +10 -0
  201. package/docs/reference/variables/contentinfo.md +10 -0
  202. package/docs/reference/variables/dialog.md +10 -0
  203. package/docs/reference/variables/field.md +10 -0
  204. package/docs/reference/variables/form.md +10 -0
  205. package/docs/reference/variables/heading.md +10 -0
  206. package/docs/reference/variables/http.md +40 -0
  207. package/docs/reference/variables/intercept.md +11 -0
  208. package/docs/reference/variables/link.md +10 -0
  209. package/docs/reference/variables/listitem.md +10 -0
  210. package/docs/reference/variables/main.md +10 -0
  211. package/docs/reference/variables/match.md +8 -0
  212. package/docs/reference/variables/navigation.md +10 -0
  213. package/docs/reference/variables/openai.md +21 -0
  214. package/docs/reference/variables/option.md +17 -0
  215. package/docs/reference/variables/region.md +10 -0
  216. package/docs/reference/variables/row.md +10 -0
  217. package/docs/reference/variables/search.md +10 -0
  218. package/docs/reference/variables/specification.md +8 -0
  219. package/docs/reference/variables/status.md +10 -0
  220. package/docs/reference/variables/table.md +10 -0
  221. package/package.json +32 -11
  222. package/dist/spec-document.js +0 -651
@@ -0,0 +1,404 @@
1
+ # 08 — Website specs (`specification.website`)
2
+
3
+ `specification.website()` tests a rendered website — its raw HTTP surface (redirects, robots.txt, headers) and its rendered surface (title, head metadata, JSON-LD, console, and full user scenarios) — through a real chromium instance. It starts the site itself, or targets one already running.
4
+
5
+ Use it when the subject under test is a browser-rendered page. For a JSON/HTTP API surface use [api](10-api.md); for a binary use [cli](12-cli.md).
6
+
7
+ ## What it specifies
8
+
9
+ A website spec answers one question: **given this backend and this visitor, what does the served site put on the screen, and what does it report to the console?** The subject is the assembled product met through an address — a running server the runner started, or a deployment already up — and everything it reads is what a visitor can see: the rendered text, the accessibility tree, the document head, the console. A single rendered unit inside that page is a component spec ([07](07-component.md)); a route module or a loader behind it is a module test ([05](05-module-tests.md)).
10
+
11
+ ## The constructor
12
+
13
+ Exactly one of `server` (start the site locally) or `url` (target a running site) is required — passing both, or neither, throws immediately.
14
+
15
+ ```typescript
16
+ // specs/website/website.specification.ts
17
+ import { specification } from '@jterrazz/test';
18
+ import { afterAll } from 'vitest';
19
+
20
+ export const { cleanup, website } = await specification.website({
21
+ server: { command: 'node specs/_fixtures/website-app/server.mjs', ready: '/' },
22
+ });
23
+
24
+ afterAll(cleanup);
25
+ ```
26
+
27
+ ```typescript
28
+ // targeting a deployed preview instead of starting one
29
+ export const { cleanup, website } = await specification.website({
30
+ url: 'https://preview-1234.my-site.pages.dev',
31
+ });
32
+ ```
33
+
34
+ ### Options
35
+
36
+ | Option | Description |
37
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
38
+ | `server` | How the site is started locally: a `process()`, the `ProcessOptions` object itself, or `(services) => …` returning either. Exactly one of `server` / `url` |
39
+ | `url` | Target an already-running site (deployed, preview, dev server). Exactly one of `server` / `url` |
40
+ | `services` | A named record started BEFORE the site and stopped with it — a database it reads, a `process()` backend it calls. See [Services beside the site](#services-beside-the-site) |
41
+ | `backend` | `{ env, port? }` — start a declared stub backend and inject its URL into the server child. Requires `server` mode — see [Declared backend](#declared-backend) |
42
+ | `external` | `'allow' \| 'block'` — cross-origin policy for `.visit()`. Default `'block'` with `server`, `'allow'` with `url` — see [Cross-origin policy](#cross-origin-policy-external) |
43
+ | `root` | **Project-root override** (rule A9): the cwd of the `server` command. Auto-discovered from the calling file when absent. Not a fixtures root |
44
+ | `transform` | **Escape hatch only** (rule D6): a normaliser applied to every compared reading — the head projection and the json-ld among them — before the comparison, for framework noise no token can name. It never mutates what the page said |
45
+
46
+ `server` is a `ProcessOptions`, whatever of the three forms states it — `command`, `ready`, `port`, `cwd`, `env`, `before`, `timeout`. That shape has one owner, and it is not this chapter: [17 — Services § `process()`](17-services.md#process--the-one-shape-an-external-process-takes) holds every field, because a website's server is an external process like any other the framework owns.
47
+
48
+ The chosen port is injected as `PORT` — the command reads it the same way it would in production. If the process never answers on `ready` within `timeout`, or exits first, `specification.website()` fails with the command's captured output attached. On teardown the child is terminated by process group (SIGTERM, escalating to SIGKILL after a 2 s grace) — the same escalation as the [cli](12-cli.md) exec adapter, so a framework's own child processes don't outlive the run.
49
+
50
+ The handle destructures to `{ website, cleanup, url }` (rule A3) — no `docker`: a browser is not a container. `url` is the resolved base URL — the one the server started on, or the `url` option with its trailing slash trimmed.
51
+
52
+ ### Services beside the site
53
+
54
+ A site under test is rarely alone: it reads a database, or it calls an API that must be up before the first page is requested. `services` is that record — the same one every other facet takes ([17 — Services](17-services.md)) — started before the site, in declaration order, and stopped with the specification.
55
+
56
+ That is why `server` may be a FUNCTION of the record: the site is handed the URL of the thing it was started beside, resolved once that thing is listening rather than guessed at config time.
57
+
58
+ ```typescript
59
+ import { postgres, process, specification } from '@jterrazz/test';
60
+ import { afterAll } from 'vitest';
61
+
62
+ export const { cleanup, website } = await specification.website({
63
+ server: (services) =>
64
+ process({
65
+ command: 'next dev',
66
+ env: { NEXT_PUBLIC_API_URL: services.api.connectionString },
67
+ }),
68
+ services: {
69
+ api: process({ command: 'bin/server web --port $PORT', ready: '/health' }),
70
+ db: postgres(),
71
+ },
72
+ });
73
+
74
+ afterAll(cleanup);
75
+ ```
76
+
77
+ A declared database is not seeded by the chain — a website chain has no `.seed()`. It is seeded by whatever owns it: the service's own `docker/<service>/init.sql`, or a `before:` command on the `process()` that migrates it.
78
+
79
+ ## The chain
80
+
81
+ ### Two terminal actions: `.fetch()` and `.visit()`
82
+
83
+ #### `.fetch(path)` — one raw HTTP exchange
84
+
85
+ `.fetch(path)` performs a single request and never follows redirects — the redirect itself is the result, not something to chase:
86
+
87
+ ```typescript
88
+ test('surfaces a permanent redirect without following it', async () => {
89
+ // Given - the legacy path
90
+ const result = await website.fetch('/old');
91
+
92
+ // Then - the 308 IS the result, with its target readable
93
+ expect(result.status).toBe(308);
94
+ expect(result.location).toBe('/');
95
+ });
96
+
97
+ test('serves robots.txt as plain text', async () => {
98
+ // Given - the robots surface
99
+ const result = await website.fetch('/robots.txt');
100
+
101
+ // Then - the whole file matches one golden
102
+ expect(result.headers['content-type']).toBe('text/plain');
103
+ expect(result.body).toMatch('robots.txt');
104
+ });
105
+ ```
106
+
107
+ `FetchResult` accessors:
108
+
109
+ | Member | Type | Description |
110
+ | ----------------- | --------------------- | ---------------------------------------------------------- |
111
+ | `result.status` | `number` | HTTP status — 3xx surfaces exactly as sent, never followed |
112
+ | `result.location` | `string \| undefined` | The `location` header, or undefined |
113
+ | `result.headers` | flat map | Response headers, lower-cased keys |
114
+ | `result.body` | `TextAccessor` | Raw response body — `toMatch('robots.txt')`, `.grep()` |
115
+ | `result.json` | `JsonAccessor` | Response body parsed as JSON |
116
+
117
+ #### `.visit(path, scenario?)` — a rendered page
118
+
119
+ `.visit()` renders the page in a real chromium and resolves with the captured document. There is **one browser process per runner**, launched lazily on the first `.visit()` — a spec file that only calls `.fetch()` never pays the browser-launch cost. Each visit gets a fresh, isolated browser context.
120
+
121
+ ```typescript
122
+ test('captures the full head surface of a rendered page', async () => {
123
+ // Given - the fixture homepage
124
+ const result = await website.visit('/');
125
+
126
+ // Then - one golden covers title, canonical, alternates, and metas
127
+ expect(result.status).toBe(200);
128
+ expect(result.head).toMatch('home.head.json');
129
+ });
130
+ ```
131
+
132
+ ### Visit scenarios — the When
133
+
134
+ A scenario is the interaction that happens **before** the capture — the visit's When. The capture always reflects the **final** page state, after the scenario ran:
135
+
136
+ ```typescript
137
+ test('subscribes through the form and captures the final state', async () => {
138
+ // Given - a visitor on the homepage
139
+ const result = await website.visit('/', async (visitor) => {
140
+ // When - they fill the form and subscribe
141
+ await visitor.fill(field('Email'), 'visitor@site.test');
142
+ await visitor.click(button('Subscribe'));
143
+ await visitor.see(content('Thanks for subscribing'));
144
+ });
145
+
146
+ // Then - the capture reflects the page after the interaction
147
+ expect(result.content).toContain('Thanks for subscribing');
148
+ await expect(result.errors).toBeEmpty();
149
+ });
150
+ ```
151
+
152
+ **No `expect()` inside a scenario (rule W1).** A scenario is pure interaction — assertions live in the Then, on the returned result. Splitting interaction from assertion keeps the setup → action → result grammar intact, and keeps scenarios replayable independent of what they're checked against.
153
+
154
+ The visitor's verbs and the words it names elements with are [13 — Elements](13-elements.md)'s, whole: `click` `fill` `press` `select` `check` `hover` `goto` `see` `gone`, the descriptors and the landmarks, the `focused` / `disabled` / `selected` / `valued` modifiers, `within()`, exact names and `testId()`. One vocabulary, three surfaces — this chapter adds none of its own and redefines none of them. Two things are website-specific and stated there: `goto(path)` navigates within the site under test, and the ARIA landmarks are this facet's and the component facet's, never the device's.
155
+
156
+ Designating exactly one element is rule W3, and the refusal it prints — the candidates, the accessible name each matched on, and what each suggested fix LEAVES — is [13 — Elements § Designating exactly one element](13-elements.md#designating-exactly-one-element).
157
+
158
+ Navigating within a scenario changes what the capture describes:
159
+
160
+ ```typescript
161
+ test('navigates to another page and captures where it landed', async () => {
162
+ // Given - a visitor on the homepage
163
+ const result = await website.visit('/', async (visitor) => {
164
+ // When - they follow the articles link
165
+ await visitor.click(link('Articles'));
166
+ });
167
+
168
+ // Then - the result is the destination page
169
+ expect(result.url).toContain('/articles');
170
+ expect(result.content).toContain('All articles');
171
+ });
172
+ ```
173
+
174
+ ### Setup: `.headers()`
175
+
176
+ `.headers({...})` sets HTTP headers for both terminal actions — the raw `.fetch()` exchange and the browser context behind `.visit()`. Repeated calls merge. The main use case is a User-Agent override, e.g. asserting on what an AI crawler sees:
177
+
178
+ ```typescript
179
+ test('sends chain headers on the raw exchange', async () => {
180
+ // Given - an AI crawler user agent
181
+ const result = await website.headers({ 'User-Agent': 'GPTBot/1.0' }).fetch('/robots.txt');
182
+
183
+ // Then - the exchange succeeds like any other client
184
+ expect(result.status).toBe(200);
185
+ expect(result.body).toContain('Allow: /');
186
+ });
187
+ ```
188
+
189
+ ### Setup: `.clock()`
190
+
191
+ `.clock('2026-03-04T09:30:00Z')` pins the calendar of the PAGE — what the site's own scripts read when they call `new Date()` — before the first byte is parsed, so a stamp rendered on load is the stated instant and not the moment the navigation happened to start. It is released with the visit.
192
+
193
+ ```typescript
194
+ test('stamps the moment the page was opened', async () => {
195
+ // Given - the page's calendar pinned for this visit
196
+ const result = await website.clock('2026-03-04T09:30:00Z').visit('/clock');
197
+
198
+ // Then - the rendered stamp is the stated instant
199
+ expect(result.content).toContain('2026-03-04T09:30:00.000Z');
200
+ });
201
+ ```
202
+
203
+ A `.fetch()` opens no page, so it has no clock to pin: the chain refuses the pairing rather than ignoring it. Assert the moment of a raw exchange with a `{{iso8601}}` token in its golden. The primitive behind the setup is [18 — Conventions § Time](18-conventions.md#time--one-primitive-two-depths).
204
+
205
+ ## The result
206
+
207
+ | Member | Type | Description |
208
+ | ------------------- | --------------------- | ------------------------------------------------------------------------------- |
209
+ | `result.status` | `number` | HTTP status of the main document response |
210
+ | `result.url` | `string` | Final URL — after redirects and any scenario navigation |
211
+ | `result.title` | `TextAccessor` | `document.title` |
212
+ | `result.head` | `JsonAccessor` | `{ title, canonical, alternates, metas }` — the one-golden-per-page SEO surface |
213
+ | `result.jsonLd` | `JsonAccessor` | Every `application/ld+json` block, parsed, as one array |
214
+ | `result.meta(name)` | `string \| undefined` | Content of a named meta — `.meta('description')`, `.meta('og:image')` |
215
+ | `result.canonical` | `string \| null` | The canonical `<link>` href, or null |
216
+ | `result.alternates` | flat map | Hreflang alternates, keyed by language code |
217
+ | `result.links` | array | `<link>` elements of the head, in DOM order |
218
+ | `result.content` | `TextAccessor` | Rendered body text (`document.body.innerText`) |
219
+ | `result.html` | `TextAccessor` | Full serialized DOM (`document.documentElement.outerHTML`) |
220
+ | `result.console` | `TextAccessor` | Every console message, one `[type] text` line per message |
221
+ | `result.errors` | `TextAccessor` | Console messages of type `error` only |
222
+ | `result.tree` | `TextAccessor` | The ARIA snapshot of the rendered `<body>` — the page's outline, as a golden |
223
+
224
+ ### `result.tree` — the outline, as the accessibility tree draws it
225
+
226
+ ```typescript
227
+ test('lays the article out as a reader walks it', async () => {
228
+ // Given - the fixture article page
229
+ const result = await website.visit('/articles/hexagonal');
230
+
231
+ // Then - the outline is what a screen reader would walk
232
+ expect(result.tree).toMatch('article.aria.yaml');
233
+ });
234
+ ```
235
+
236
+ It is produced by the same Playwright `ariaSnapshot()` the component facet calls, so a page's outline and a component's outline are the same kind of golden and comparable to each other. Deterministic where a screenshot is not, and it says what the markup MEANS rather than what it is made of.
237
+
238
+ ### The `head` golden — one per page
239
+
240
+ `result.head` is the **stable, assertion-friendly projection** of the document head — title, canonical, hreflang alternates, and named metas collapsed into one object. It is the one golden a page needs for its SEO surface:
241
+
242
+ ```typescript
243
+ test('exposes canonical, alternates, and named metas directly', async () => {
244
+ // Given - the fixture homepage
245
+ const result = await website.visit('/');
246
+
247
+ // Then - the accessors read the head without a golden…
248
+ expect(result.canonical).toBe('https://site.test/');
249
+ expect(result.alternates['x-default']).toBe('https://site.test/');
250
+ expect(result.meta('og:title')).toBe('Fixture — Home');
251
+
252
+ // …or snapshot the whole surface at once
253
+ expect(result.head).toMatch('home.head.json');
254
+ });
255
+ ```
256
+
257
+ **A meta the FRAMEWORK owns is dropped by the runner, not absorbed by the golden.** A view-transitions router writes its own metas into every head; a golden that swallows them says the site declares something it never did, and every page's golden grows the same two lines. `transform` is the door — the same option `specification.cli()` has, for the same reason (rule D6):
258
+
259
+ ```typescript
260
+ export const { cleanup, website } = await specification.website({
261
+ server: { command: 'npm run preview', ready: '/' },
262
+ // the router's own metas, which no `{{token}}` can name
263
+ transform: (text) => text.replaceAll(/,?"astro-view-transitions-[a-z]+":\s*"[^"]*"/gu, ''),
264
+ });
265
+ ```
266
+
267
+ It applies to every COMPARED reading of a result — the head projection, the json-ld, the content, the console — so it answers for whatever it is handed; what the page said is still read whole through `meta()`, `canonical()` and the rest. Prefer a `{{token}}` wherever the noise has a shape the grammar already names.
268
+
269
+ Structured data gets the same treatment — every `ld+json` block on the page, in one golden:
270
+
271
+ ```typescript
272
+ test('parses every json-ld block into one array', async () => {
273
+ const result = await website.visit('/');
274
+
275
+ expect(result.jsonLd).toMatch('home.jsonld.json');
276
+ });
277
+ ```
278
+
279
+ ### Console assertions
280
+
281
+ The console splits into the full stream and the error-only stream — the same shape as `stdout`/`stderr` on a cli result:
282
+
283
+ ```typescript
284
+ test('keeps a clean page silent on both streams', async () => {
285
+ // Given - the healthy homepage
286
+ const result = await website.visit('/');
287
+
288
+ // Then - no console output at all
289
+ await expect(result.console).toBeEmpty();
290
+ await expect(result.errors).toBeEmpty();
291
+ });
292
+
293
+ test('separates console errors from the full stream', async () => {
294
+ // Given - a page that logs and errors
295
+ const result = await website.visit('/noisy');
296
+
297
+ // Then - the full stream carries both, the error stream only the error
298
+ expect(result.console).toMatch('noisy.console.txt');
299
+ expect(result.errors).toContain('boom');
300
+ });
301
+ ```
302
+
303
+ ## Unique here
304
+
305
+ ### Declared backend
306
+
307
+ A site under test usually talks to an API. The `backend` option starts a small **stub backend** (plain `node:http`, no extra dependency) BEFORE the server command and injects its URL into the server child's environment under `backend.env` — the site reads it the same way it would in production:
308
+
309
+ ```typescript
310
+ // specs/website/website.specification.ts
311
+ export const { cleanup, website } = await specification.website({
312
+ server: { command: 'npm run start', ready: '/' },
313
+ backend: { env: 'API_URL' },
314
+ });
315
+
316
+ afterAll(cleanup);
317
+ ```
318
+
319
+ | `backend` field | Description |
320
+ | --------------- | --------------------------------------------------------------------------------- |
321
+ | `env` | Env var receiving the stub's URL in the server child (e.g. `'API_URL'`) |
322
+ | `port` | Fixed port — pins a stable stub URL across runs. Default: a free OS-assigned port |
323
+
324
+ `backend` requires `server` mode — with `url` it refuses (the type already forbids the combination): a deployed site cannot be pointed at a local stub. The **ownership boundary**: the framework owns the server child, so it injects the env var itself — that is the whole wiring.
325
+
326
+ What the stub serves is declared per chain, as [contracts](16-contracts.md) — the feature's `contracts/` facade, exactly the form `api`/`jobs` use:
327
+
328
+ ```typescript
329
+ import newsroom from './contracts/newsroom.contracts.js';
330
+
331
+ test('renders the events feed from the declared backend', async () => {
332
+ // Given - the backend under contract for this chain
333
+ const result = await website.intercept(newsroom).visit('/events');
334
+
335
+ // Then - the page rendered what the stub declared
336
+ expect(result.content).toContain('Enquête Fauci COVID-19');
337
+ await expect(result.errors).toBeEmpty();
338
+ });
339
+ ```
340
+
341
+ The stub **resets between chains** the way databases do: one chain = one terminal action, and its contracts replace the previous chain's wholesale. Selection is the shared one: the first declared contract that matches and is not exhausted wins, and a contract with no `times` is unlimited — so a page re-fetching the same endpoint (re-render, retry) replays it.
342
+
343
+ Strictness is the analog of `external: 'block'`: a request matching no declared contract is answered **501** (a JSON body naming the path and listing the declared routes) and **recorded** — when the `.visit()` completes, the action **throws** an error enumerating every unmatched request (method, path, count). The failure evidence (screenshot on a scenario error) is captured first, as always. A chain with **zero** intercepts leaves the stub unguarded — the same boundary as MSW never mounting without an `.intercept()`.
344
+
345
+ Two pieces of plumbing are handled for you:
346
+
347
+ - **CORS** — the stub answers the `OPTIONS` preflight and stamps permissive `Access-Control-Allow-*` headers on every response, so client-side fetches from the site's origin just work.
348
+ - **`external: 'block'`** — the stub's origin is allow-listed automatically; declared-backend fetches are never aborted as third-party noise.
349
+
350
+ ### Cross-origin policy (`external`)
351
+
352
+ `external: 'block'` aborts every request leaving the site under test during a `.visit()` — analytics beacons, third-party CDNs, ad scripts never fire, so a visit stays deterministic. `'allow'` lets them through.
353
+
354
+ The default follows the mode: `'block'` with `server` (you own the deployment, third-party noise is not the point), `'allow'` with `url` (a deployed site legitimately loads third-party assets). Override with the top-level `external` option when a spec needs the opposite of its mode's default.
355
+
356
+ ### Evidence on failure
357
+
358
+ When a scenario throws — an element never becomes visible, a `see()` times out — the error carries a full-page screenshot of the state the scenario died in, referenced by its temp path in the error message. The original error is never masked; the screenshot is attached evidence, not a replacement.
359
+
360
+ ### Playwright — the optional peer
361
+
362
+ Playwright is not a hard dependency: `.fetch()`-only spec files never need it. `.visit()` imports it lazily, and needs it installed:
363
+
364
+ ```bash
365
+ npm install -D playwright && npx playwright install chromium
366
+ ```
367
+
368
+ Calling `.visit()` without playwright installed throws exactly that guidance — there is nothing else to search for.
369
+
370
+ Provisioning the environment is not this package's job. In CI, the shared validate workflow does it: `browsers: true` (jterrazz-actions) installs cached chromium, versioned from the caller's lockfile, between Build and Test. On a workstation, `j install` provisions `playwright-browsers` once.
371
+
372
+ ### Folder layout
373
+
374
+ ```
375
+ specs/website/
376
+ ├── website.specification.ts # runner at the facet ROOT (rule C1)
377
+ └── <domain>/
378
+ ├── <aspect>.spec.ts
379
+ ├── _expected/ # ALL expected fixtures, FLAT (*.head.json, *.jsonld.json, *.console.txt, …)
380
+ └── contracts/ # what the declared backend serves — with the `backend` option
381
+ ├── newsroom.contracts.ts
382
+ └── http/…
383
+ ```
384
+
385
+ No `_seeds/` or `_requests/` — a website chain has no `.seed()` setup and no request-file format, whatever its runner declares under `services`; `.fetch()`/`.visit()` calls are inline, and the golden is always `_expected/<name>`.
386
+
387
+ ## Pitfalls
388
+
389
+ - **Passing both `server` and `url`, or neither.** The options type makes the invalid combinations inexpressible — the compiler rejects them before anything runs.
390
+ - **Using `expect()` inside a scenario callback.** Forbidden (rule W1) — the scenario is the When; assertions belong on the result the `.visit()` promise resolves to.
391
+ - **Reaching for `testId()` as the default locator.** It exists as an escape hatch (rule W2 warns) — prefer `button`/`link`/`field`/`heading`/`content`, the same vocabulary a user's accessibility tree exposes.
392
+ - **Reaching for `testId()` to escape an ambiguity.** It silences the refusal without answering it: the test stops asserting the role and the accessible name, which is most of what a user-facing element was buying. Scope it with `within()` instead — that is the fix rule W3 is pointing at.
393
+ - **Using `see()` to prove something went away.** It cannot: an element that never appeared and one that disappeared read the same to it. `gone(element)` is the absence primitive.
394
+ - **Snapshotting the tree to prove focus.** The accessibility tree carries none — `see(focused(x))` is the assertion that does. Enablement and selection it DOES carry (`[disabled]`, `[selected]`), so those a golden may pin; `see(disabled(x))` is for the one control a test is about.
395
+ - **Calling `.visit()` without playwright installed.** The error names the exact fix — `npm install -D playwright && npx playwright install chromium` — there is no silent fallback.
396
+ - **Expecting `.fetch()` to follow redirects.** It never does — the 3xx status and `location` header ARE the result; chase the target with a second `.fetch()` if the spec needs to.
397
+ - **Assuming `external` defaults the same way in both modes.** It flips with the constructor mode: `'block'` with `server`, `'allow'` with `url` — pass it explicitly to override.
398
+ - **Reading `result.head` field-by-field instead of snapshotting it.** It is designed as the one golden per page (`toMatch('home.head.json')`) — use the direct accessors (`canonical`, `alternates`, `meta()`) only for a single targeted probe.
399
+ - **Calling `.intercept()` without the `backend` option.** It throws immediately, naming the fix: add `backend: { env: '…' }` to the `specification.website()` options — a chain's `.http` exchanges need a stub to serve them.
400
+ - **Combining `backend` with `url` mode.** The type forbids it and the runtime refuses: a deployed site reads its API URL at its own deploy time — there is nothing a local stub could inject into.
401
+
402
+ ## Related
403
+
404
+ [02 — Developing](02-developing.md) · [14 — Assertions](14-assertions.md) · [15 — Tokens](15-tokens.md) · [09 — Mobile specs](09-mobile.md) · [07 — Component specs](07-component.md)