@jterrazz/test 16.1.0 → 17.0.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 (304) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +2 -2867
  3. package/dist/browser/services.js +2 -0
  4. package/dist/browser/stubs.js +2 -0
  5. package/dist/checker.d.ts +6 -1
  6. package/dist/checker.js +4551 -2612
  7. package/dist/config.d.ts +40 -0
  8. package/dist/config.js +309 -0
  9. package/dist/in-place.global-setup.d.ts +11 -0
  10. package/dist/in-place.global-setup.js +21 -0
  11. package/dist/index.d.ts +1653 -3002
  12. package/dist/index.js +5226 -3845
  13. package/dist/mobile-session.global-setup.d.ts +14 -0
  14. package/dist/mobile-session.global-setup.js +31 -0
  15. package/dist/oxlint.cjs +6073 -5616
  16. package/dist/oxlint.d.cts +43 -7212
  17. package/dist/oxlint.d.ts +43 -7212
  18. package/dist/oxlint.js +6065 -5609
  19. package/dist/route-coverage.global-setup.d.ts +42 -0
  20. package/dist/route-coverage.global-setup.js +2 -0
  21. package/dist/services.d.ts +3 -0
  22. package/dist/services.global-setup.d.ts +43 -0
  23. package/dist/services.global-setup.js +2 -0
  24. package/dist/services.js +597 -0
  25. package/dist/shared-appium.adapter.js +428 -0
  26. package/dist/shared-artifacts.js +32 -0
  27. package/dist/shared-chapter.js +7 -0
  28. package/dist/shared-container.port.d.ts +31 -0
  29. package/dist/shared-derived-projects.js +58 -0
  30. package/dist/shared-device.port.d.ts +189 -0
  31. package/dist/shared-exchange.js +447 -0
  32. package/dist/shared-in-place.js +172 -0
  33. package/dist/shared-index.browser.js +3191 -0
  34. package/dist/shared-index.services.d.ts +346 -0
  35. package/dist/shared-index.stubs.d.ts +287 -0
  36. package/dist/shared-json-shape.js +17 -0
  37. package/dist/shared-landmarks.js +71 -0
  38. package/dist/shared-ledger.js +68 -0
  39. package/dist/shared-literate-plugin.js +183 -0
  40. package/dist/shared-manifest.js +1321 -0
  41. package/dist/shared-pause.js +11 -0
  42. package/dist/shared-peer.js +88 -0
  43. package/dist/shared-playwright.adapter.js +330 -0
  44. package/dist/shared-process.adapter.js +74 -0
  45. package/dist/shared-refusal.js +35 -0
  46. package/dist/shared-resolve.js +24 -0
  47. package/dist/shared-route-coverage.global-setup.js +187 -0
  48. package/dist/shared-scenario-failure.js +198 -0
  49. package/dist/shared-serve.js +365 -0
  50. package/dist/shared-service-images.js +10 -0
  51. package/dist/shared-services.global-setup.js +179 -0
  52. package/dist/shared-session-ledger.js +301 -0
  53. package/dist/shared-setup-entry.js +12 -0
  54. package/dist/shared-source-scan.js +133 -0
  55. package/dist/shared-spec-document.js +365 -0
  56. package/dist/shared-specifications.js +26 -0
  57. package/dist/shared-stream-body.js +130 -0
  58. package/dist/shared-stub-server.js +190 -0
  59. package/dist/shared-temp-root.global-setup.js +80 -0
  60. package/dist/shared-test-baseline.js +26 -0
  61. package/dist/shared-test-section.js +58 -0
  62. package/dist/shared-testcontainers.adapter.js +203 -0
  63. package/dist/shared-tokens.js +55 -0
  64. package/dist/shared-yaml-document.js +119 -0
  65. package/dist/stubs.d.ts +2 -0
  66. package/dist/stubs.js +657 -0
  67. package/dist/temp-root.global-setup.d.ts +7 -0
  68. package/dist/temp-root.global-setup.js +2 -0
  69. package/dist/vitest.d.ts +97 -163
  70. package/dist/vitest.js +374 -507
  71. package/docs/README.md +82 -36
  72. package/docs/concepts/architecture.md +118 -0
  73. package/docs/concepts/conventions.md +333 -0
  74. package/docs/guides/api-specs.md +374 -0
  75. package/docs/guides/cli-specs.md +564 -0
  76. package/docs/guides/component-specs.md +252 -0
  77. package/docs/guides/develop.md +94 -0
  78. package/docs/guides/integration-specs.md +181 -0
  79. package/docs/guides/job-specs.md +180 -0
  80. package/docs/guides/lint-specs.md +174 -0
  81. package/docs/guides/mobile-specs.md +319 -0
  82. package/docs/guides/module-tests.md +149 -0
  83. package/docs/guides/release.md +43 -0
  84. package/docs/guides/spec-documents.md +189 -0
  85. package/docs/guides/website-specs.md +413 -0
  86. package/docs/reference/assertions.md +345 -0
  87. package/docs/reference/glossary.md +20 -0
  88. package/docs/reference/linting.md +483 -0
  89. package/docs/reference/services.md +236 -0
  90. package/docs/reference/stubs.md +498 -0
  91. package/docs/reference/time-and-doubles.md +137 -0
  92. package/docs/reference/tokens.md +258 -0
  93. package/docs/reference/visitor.md +221 -0
  94. package/docs/start/getting-started.md +187 -0
  95. package/docs/start/set-up-a-project.md +214 -0
  96. package/package.json +50 -30
  97. package/schema/spec.schema.json +43 -17
  98. package/dist/appium.adapter.js +0 -489
  99. package/dist/artifacts.js +0 -32
  100. package/dist/fixtures.js +0 -110
  101. package/dist/literate.js +0 -1747
  102. package/dist/match.js +0 -804
  103. package/dist/playwright.adapter.js +0 -349
  104. package/dist/server.js +0 -160
  105. package/dist/stream.js +0 -321
  106. package/dist/substring-warning.js +0 -244
  107. package/docs/01-architecture.md +0 -95
  108. package/docs/02-developing.md +0 -351
  109. package/docs/03-testing.md +0 -495
  110. package/docs/04-operating.md +0 -101
  111. package/docs/05-module-tests.md +0 -152
  112. package/docs/06-integration.md +0 -180
  113. package/docs/07-component.md +0 -246
  114. package/docs/08-website.md +0 -404
  115. package/docs/09-mobile.md +0 -270
  116. package/docs/10-api.md +0 -264
  117. package/docs/11-jobs.md +0 -157
  118. package/docs/12-cli.md +0 -677
  119. package/docs/13-elements.md +0 -242
  120. package/docs/14-assertions.md +0 -314
  121. package/docs/15-tokens.md +0 -253
  122. package/docs/16-contracts.md +0 -438
  123. package/docs/17-services.md +0 -188
  124. package/docs/18-conventions.md +0 -305
  125. package/docs/19-linting.md +0 -510
  126. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  127. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  128. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  129. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  130. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  131. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  132. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  133. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  134. package/docs/decisions/_template.md +0 -22
  135. package/docs/reference/classes/Matcher.md +0 -120
  136. package/docs/reference/classes/ProcessHandle.md +0 -368
  137. package/docs/reference/functions/defineContract.md +0 -39
  138. package/docs/reference/functions/defineContracts.md +0 -36
  139. package/docs/reference/functions/disabled.md +0 -28
  140. package/docs/reference/functions/enabled.md +0 -20
  141. package/docs/reference/functions/focused.md +0 -26
  142. package/docs/reference/functions/mockOf.md +0 -79
  143. package/docs/reference/functions/postgres.md +0 -31
  144. package/docs/reference/functions/process.md +0 -33
  145. package/docs/reference/functions/redis.md +0 -28
  146. package/docs/reference/functions/required.md +0 -41
  147. package/docs/reference/functions/selected.md +0 -26
  148. package/docs/reference/functions/sqlite.md +0 -33
  149. package/docs/reference/functions/testId.md +0 -20
  150. package/docs/reference/functions/text.md +0 -40
  151. package/docs/reference/functions/valued.md +0 -28
  152. package/docs/reference/functions/waitUntil.md +0 -46
  153. package/docs/reference/functions/within.md +0 -29
  154. package/docs/reference/index.md +0 -196
  155. package/docs/reference/interfaces/BaseResult.md +0 -87
  156. package/docs/reference/interfaces/CallResult.md +0 -160
  157. package/docs/reference/interfaces/CaptureScope.md +0 -82
  158. package/docs/reference/interfaces/CliResult.md +0 -253
  159. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  160. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  161. package/docs/reference/interfaces/FetchResult.md +0 -180
  162. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  163. package/docs/reference/interfaces/HttpResult.md +0 -126
  164. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  165. package/docs/reference/interfaces/PageResult.md +0 -356
  166. package/docs/reference/interfaces/RenderResult.md +0 -72
  167. package/docs/reference/interfaces/RenderedText.md +0 -110
  168. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  169. package/docs/reference/interfaces/ScreenResult.md +0 -128
  170. package/docs/reference/interfaces/TableAccessor.md +0 -26
  171. package/docs/reference/interfaces/TextAccessor.md +0 -96
  172. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  173. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  174. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  175. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  176. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  177. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  178. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  179. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  180. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  181. package/docs/reference/type-aliases/CliEnv.md +0 -11
  182. package/docs/reference/type-aliases/CliHandle.md +0 -80
  183. package/docs/reference/type-aliases/CliInput.md +0 -36
  184. package/docs/reference/type-aliases/CliOutput.md +0 -46
  185. package/docs/reference/type-aliases/CliPort.md +0 -58
  186. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  187. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  188. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  189. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  190. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  191. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  192. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  193. package/docs/reference/type-aliases/Contract.md +0 -68
  194. package/docs/reference/type-aliases/ContractInput.md +0 -13
  195. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  196. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  197. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  198. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  199. package/docs/reference/type-aliases/Contracts.md +0 -48
  200. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  201. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  202. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  203. package/docs/reference/type-aliases/DevicePort.md +0 -51
  204. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  205. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  206. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  207. package/docs/reference/type-aliases/DomMount.md +0 -21
  208. package/docs/reference/type-aliases/ElementKind.md +0 -27
  209. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  210. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  211. package/docs/reference/type-aliases/ElementRef.md +0 -128
  212. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  213. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  214. package/docs/reference/type-aliases/HonoApp.md +0 -31
  215. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  216. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  217. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  218. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  219. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  220. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  221. package/docs/reference/type-aliases/Intercept.md +0 -19
  222. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  223. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  224. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  225. package/docs/reference/type-aliases/JobHandle.md +0 -34
  226. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  227. package/docs/reference/type-aliases/JobsResult.md +0 -16
  228. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  229. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  230. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  231. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  232. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  233. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  234. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  235. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  236. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  237. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  238. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  239. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  240. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  241. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  242. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  243. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  244. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  245. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  246. package/docs/reference/type-aliases/MockPort.md +0 -10
  247. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  248. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  249. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  250. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  251. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  252. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  253. package/docs/reference/type-aliases/ServerPort.md +0 -36
  254. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  255. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  256. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  257. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  258. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  259. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  260. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  261. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  262. package/docs/reference/type-aliases/SpecKind.md +0 -8
  263. package/docs/reference/type-aliases/SpecRun.md +0 -108
  264. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  265. package/docs/reference/type-aliases/SpecStream.md +0 -32
  266. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  267. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  268. package/docs/reference/type-aliases/SseEvent.md +0 -58
  269. package/docs/reference/type-aliases/StreamBody.md +0 -61
  270. package/docs/reference/type-aliases/TextFilter.md +0 -22
  271. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  272. package/docs/reference/type-aliases/Visitor.md +0 -215
  273. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  274. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  275. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  276. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  277. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  278. package/docs/reference/variables/anthropic.md +0 -19
  279. package/docs/reference/variables/banner.md +0 -10
  280. package/docs/reference/variables/button.md +0 -10
  281. package/docs/reference/variables/clock.md +0 -16
  282. package/docs/reference/variables/complementary.md +0 -10
  283. package/docs/reference/variables/component.md +0 -10
  284. package/docs/reference/variables/content.md +0 -10
  285. package/docs/reference/variables/contentinfo.md +0 -10
  286. package/docs/reference/variables/dialog.md +0 -10
  287. package/docs/reference/variables/field.md +0 -10
  288. package/docs/reference/variables/form.md +0 -10
  289. package/docs/reference/variables/heading.md +0 -10
  290. package/docs/reference/variables/http.md +0 -40
  291. package/docs/reference/variables/intercept.md +0 -11
  292. package/docs/reference/variables/link.md +0 -10
  293. package/docs/reference/variables/listitem.md +0 -10
  294. package/docs/reference/variables/main.md +0 -10
  295. package/docs/reference/variables/match.md +0 -8
  296. package/docs/reference/variables/navigation.md +0 -10
  297. package/docs/reference/variables/openai.md +0 -21
  298. package/docs/reference/variables/option.md +0 -17
  299. package/docs/reference/variables/region.md +0 -10
  300. package/docs/reference/variables/row.md +0 -10
  301. package/docs/reference/variables/search.md +0 -10
  302. package/docs/reference/variables/specification.md +0 -8
  303. package/docs/reference/variables/status.md +0 -10
  304. package/docs/reference/variables/table.md +0 -10
@@ -0,0 +1,3191 @@
1
+ import { defineInvariant, refusalBlock } from "@jterrazz/typescript/rules";
2
+ import { TestRunner, chai, expect, inject, onTestFinished, vi } from "vitest";
3
+ import { commands, locators, page, userEvent } from "vitest/browser";
4
+ import { mock, mockDeep } from "vitest-mock-extended";
5
+ //#region \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __exportAll = (all, no_symbols) => {
8
+ let target = {};
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
+ return target;
15
+ };
16
+ //#endregion
17
+ //#region lint/kit/catalogue/chapter.ts
18
+ const CATALOGUE_CHAPTER = "docs/reference/linting.md";
19
+ //#endregion
20
+ //#region src/chain/refusals/rows.ts
21
+ const REFUSAL_WHY = {
22
+ "database-named-only-when-several-at-runtime": "With two SQL databases or more, a seed or a table names the `database:` it reads, and with exactly one it names none.",
23
+ "route-written-as-template-at-runtime": "A request written as its route template is handed exactly the parameters the template names.",
24
+ "disk-named-by-relative-path-at-runtime": "A verb that reads the disk takes a `file()` or a `directory()`, whose path starts with `./` or `../` and ends in `/` exactly when it names a directory.",
25
+ "one-verb-per-meaning": "`toMatch` on a result the framework produced compares the WHOLE result, to a value written in place or to a `file()`/`directory()` golden.",
26
+ "network-strict-after-first-stub": "From its first stub, a chain's network is strict: every outgoing request matches a declared stub.",
27
+ "target-matches-one-element": "A verb acts on exactly ONE element: a target that matches several is refused, never settled by the first.",
28
+ "seam-is-an-optional-peer": "Every seam is an optional peer of @jterrazz/test, installed by the project that specifies through it.",
29
+ "no-landmark-on-mobile": "An iOS screen has no ARIA landmark: a mobile verb is scoped with `within(testId(...), ...)`."
30
+ };
31
+ `${REFUSAL_WHY["route-written-as-template-at-runtime"]}`, `${REFUSAL_WHY["database-named-only-when-several-at-runtime"]}`, `${REFUSAL_WHY["disk-named-by-relative-path-at-runtime"]}`, `${REFUSAL_WHY["network-strict-after-first-stub"]}`, `${REFUSAL_WHY["one-verb-per-meaning"]}`, `${REFUSAL_WHY["target-matches-one-element"]}`, `${REFUSAL_WHY["no-landmark-on-mobile"]}`, `${REFUSAL_WHY["seam-is-an-optional-peer"]}`;
32
+ //#endregion
33
+ //#region src/chain/refusals/refusal.ts
34
+ const invariant = defineInvariant({
35
+ bugOf: "the framework",
36
+ notOf: "the test",
37
+ package: "@jterrazz/test"
38
+ });
39
+ function refusal(rowOrLesson, teaching) {
40
+ if (typeof rowOrLesson !== "string") return refusalBlock(rowOrLesson);
41
+ if (teaching === void 0) throw invariant(`refusal('${rowOrLesson}') was handed no found and no fix for its case`);
42
+ return refusalBlock({
43
+ fix: teaching.fix,
44
+ found: teaching.found,
45
+ read: `${CATALOGUE_CHAPTER}#${rowOrLesson}`,
46
+ why: REFUSAL_WHY[rowOrLesson]
47
+ });
48
+ }
49
+ //#endregion
50
+ //#region src/chain/refusals/node-only.ts
51
+ function refuse(name) {
52
+ throw new Error(refusal({
53
+ fix: "specify it from a test collected by a node project (unit/api/job/cli/website/mobile); a rendered thing is a `.test.tsx` in the `__tests__/` of its component's folder",
54
+ found: `${name} runs under node, and was called in a page`,
55
+ read: "docs/guides/component-specs.md#what-it-does-not-do",
56
+ why: "A page has no filesystem, no child process and no socket."
57
+ }));
58
+ }
59
+ /** A constructible function, never an arrow: `new` on an arrow throws before the refusal runs. */
60
+ function nodeOnlyClass(name) {
61
+ function NodeOnly() {
62
+ return refuse(name);
63
+ }
64
+ return NodeOnly;
65
+ }
66
+ //#endregion
67
+ //#region src/chain/state.ts
68
+ const EMPTY_STATE = {
69
+ slices: {},
70
+ testDir: null
71
+ };
72
+ function readerOf(state) {
73
+ return (capability) => Object.hasOwn(state.slices, capability.name) ? state.slices[capability.name] : capability.initial;
74
+ }
75
+ function applied(state, capability, args, setup) {
76
+ const slice = capability.reduce(readerOf(state)(capability), args, setup);
77
+ return {
78
+ ...state,
79
+ slices: {
80
+ ...state.slices,
81
+ [capability.name]: slice
82
+ }
83
+ };
84
+ }
85
+ //#endregion
86
+ //#region src/chain/perform.ts
87
+ /** An adapter's recorded violation wins over the action's error and over its success; every armed effect is released in reverse. */
88
+ async function perform(setups, action, args, { config, adapters, state, testDir }) {
89
+ const read = readerOf(state);
90
+ const { workDir } = await action.prepare?.(args, read) ?? { workDir: null };
91
+ const run = {
92
+ config,
93
+ testDir,
94
+ workDir
95
+ };
96
+ const armed = [];
97
+ const violation = () => armed.map((effect) => effect.violation?.() ?? null).find((error) => error !== null) ?? null;
98
+ try {
99
+ for (const setup of setups.toSorted((one, other) => one.phase - other.phase)) {
100
+ const effect = await setup.arm?.(read(setup), adapters, run);
101
+ armed.push(effect ?? {});
102
+ }
103
+ const value = await action.perform(args, read, adapters, run);
104
+ const failed = violation();
105
+ if (failed !== null) throw failed;
106
+ return value;
107
+ } catch (error) {
108
+ throw violation() ?? error;
109
+ } finally {
110
+ for (const effect of armed.toReversed()) effect.release?.();
111
+ }
112
+ }
113
+ //#endregion
114
+ //#region src/chain/define-specification.ts
115
+ function withSharedSettings(setups, state, context, setup) {
116
+ let next = state;
117
+ for (const capability of setups) {
118
+ const value = capability.context === void 0 ? void 0 : context[capability.context];
119
+ if (value !== void 0) next = applied(next, capability, [value], setup);
120
+ }
121
+ return next;
122
+ }
123
+ function chainOf(definition, setup, state) {
124
+ const here = () => state.testDir ?? definition.locate();
125
+ const setups = definition.capabilities.filter((capability) => capability.kind === "setup");
126
+ const chain = {};
127
+ for (const capability of definition.capabilities) chain[capability.name] = capability.kind === "context" ? (context) => chainOf(definition, setup, withSharedSettings(setups, state, context, setup)) : (...args) => chainOf(definition, setup, {
128
+ ...applied(state, capability, args, setup),
129
+ testDir: here()
130
+ });
131
+ for (const action of definition.actions) chain[action.name] = async (...args) => await perform(setups, action, args, {
132
+ config: setup.config,
133
+ adapters: setup.adapters,
134
+ state,
135
+ testDir: here()
136
+ });
137
+ return chain;
138
+ }
139
+ function defineSpecification() {
140
+ return (definition) => ({
141
+ composition: definition,
142
+ open: (config) => {
143
+ return chainOf(definition, {
144
+ config,
145
+ adapters: definition.adapters(config)
146
+ }, EMPTY_STATE);
147
+ }
148
+ });
149
+ }
150
+ //#endregion
151
+ //#region src/chain/capability.ts
152
+ function defineCapability(definition) {
153
+ return {
154
+ ...definition,
155
+ kind: "setup"
156
+ };
157
+ }
158
+ //#endregion
159
+ //#region src/clock/instant.ts
160
+ /** `written` spells the call that stated the instant around a value, so the refusal speaks the words the test wrote. */
161
+ function instantOf(iso, written) {
162
+ const at = new Date(iso);
163
+ if (Number.isNaN(at.getTime())) {
164
+ const stated = written(JSON.stringify(iso));
165
+ throw new TypeError(refusal({
166
+ fix: `pass an ISO-8601 string: ${written("'2026-03-04T09:30:00Z'")}`,
167
+ found: `${stated}, and "${iso}" is not an instant`,
168
+ read: "docs/reference/time-and-doubles.md#the-clock",
169
+ why: "A clock is pinned at an instant, written as an ISO-8601 string."
170
+ }));
171
+ }
172
+ return at;
173
+ }
174
+ //#endregion
175
+ //#region src/clock/clock.capability.ts
176
+ /** The instant is judged here, before any adapter sees it, and released when the action settles (`no-sampled-value-in-expectation`); a chain stating none hands the adapter `null`. */
177
+ const clock$1 = defineCapability({
178
+ arm: (instant, adapters) => {
179
+ if (instant !== null) instantOf(instant, (value) => `.clock(${value})`);
180
+ return adapters.clock.pin(instant);
181
+ },
182
+ context: "clock",
183
+ initial: null,
184
+ name: "clock",
185
+ phase: 50,
186
+ reduce: (_pinned, [iso]) => iso,
187
+ row: {
188
+ group: "Setup",
189
+ name: ".clock()",
190
+ probe: ".clock("
191
+ }
192
+ });
193
+ //#endregion
194
+ //#region src/chain/pause.ts
195
+ async function pause(ms) {
196
+ if (ms <= 0) return;
197
+ await new Promise((resolve) => {
198
+ setTimeout(resolve, ms);
199
+ });
200
+ }
201
+ //#endregion
202
+ //#region src/stubs/exchange.ts
203
+ /** Each adapter serialises an object as JSON, so a stream declared as a plain value would arrive as `{}`: the tag tells them apart. */
204
+ const STREAM_BODY = "@jterrazz/test:stream";
205
+ function isStreamBody(body) {
206
+ return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
207
+ }
208
+ //#endregion
209
+ //#region src/expectations/tokens/capture-scope.ts
210
+ /** Named captures of `match.ref()` / `{{type#ref}}`, one scope per spec result. */
211
+ var CaptureScope = class CaptureScope {
212
+ values = /* @__PURE__ */ new Map();
213
+ /** The spec's working directory when known (cli mode), for `{{workdir}}`. */
214
+ workdir;
215
+ constructor(workdir) {
216
+ this.workdir = workdir;
217
+ }
218
+ get(name) {
219
+ return this.values.get(name);
220
+ }
221
+ has(name) {
222
+ return this.values.has(name);
223
+ }
224
+ set(name, value) {
225
+ this.values.set(name, value);
226
+ }
227
+ /** A copy to try a comparison on: its captures stay there until adopted. */
228
+ fork() {
229
+ const copy = new CaptureScope(this.workdir);
230
+ for (const [name, value] of this.values) copy.set(name, value);
231
+ return copy;
232
+ }
233
+ /** Take every capture a fork of this scope recorded. */
234
+ adopt(fork) {
235
+ for (const [name, value] of fork.values) this.values.set(name, value);
236
+ }
237
+ };
238
+ //#endregion
239
+ //#region lint/kit/project/json-shape.ts
240
+ /** `JSON.parse` answers `any`: these read a parsed value field by field, and a field of another shape reads as absent. */
241
+ function isRecord(value) {
242
+ return typeof value === "object" && value !== null && !Array.isArray(value);
243
+ }
244
+ //#endregion
245
+ //#region lint/kit/project/tokens.ts
246
+ const TOKEN_KINDS = [
247
+ "any",
248
+ "base64",
249
+ "date",
250
+ "duration",
251
+ "email",
252
+ "float",
253
+ "hex",
254
+ "int",
255
+ "ip",
256
+ "iso8601",
257
+ "number",
258
+ "path",
259
+ "port",
260
+ "semver",
261
+ "sha",
262
+ "string",
263
+ "time",
264
+ "ulid",
265
+ "url",
266
+ "uuid",
267
+ "workdir"
268
+ ];
269
+ new Set(TOKEN_KINDS);
270
+ //#endregion
271
+ //#region src/expectations/tokens/any-lines.ts
272
+ function isFramedInLine(expected, start, end) {
273
+ const before = expected.slice(expected.lastIndexOf("\n", start - 1) + 1, start);
274
+ const lineEnd = expected.indexOf("\n", end);
275
+ const after = expected.slice(end, lineEnd === -1 ? void 0 : lineEnd);
276
+ return before.trim() !== "" && after.trim() !== "";
277
+ }
278
+ function piecesMatch(pieces, text) {
279
+ const [first = "", ...rest] = pieces;
280
+ const last = rest.pop();
281
+ if (last === void 0) return text === first;
282
+ if (!text.startsWith(first)) return false;
283
+ let at = first.length;
284
+ for (const piece of rest) {
285
+ const found = text.indexOf(piece, at);
286
+ if (found === -1) return false;
287
+ at = found + piece.length;
288
+ }
289
+ return text.length - last.length >= at && text.endsWith(last);
290
+ }
291
+ //#endregion
292
+ //#region src/expectations/tokens/placeholders.ts
293
+ const UUID_SOURCE = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
294
+ const ULID_SOURCE = "[0-9A-HJKMNP-TV-Z]{26}";
295
+ const ISO8601_SOURCE = String.raw`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})`;
296
+ const DATE_SOURCE = String.raw`\d{4}-\d{2}-\d{2}`;
297
+ const TIME_SOURCE = String.raw`\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?`;
298
+ const DURATION_SOURCE = String.raw`\d+(?:\.\d+)?(?:ms|s|m|h)`;
299
+ const NUMBER_SOURCE = String.raw`-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?`;
300
+ const INT_SOURCE = String.raw`-?\d+`;
301
+ const FLOAT_SOURCE = String.raw`-?\d+\.\d+`;
302
+ const SEMVER_SOURCE = String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`;
303
+ const SHA_SOURCE = "[0-9a-f]{7,64}";
304
+ const HEX_SOURCE = "[0-9a-fA-F]+";
305
+ const BASE64_SOURCE = "[A-Za-z0-9+/]+={0,2}";
306
+ const PORT_SOURCE = String.raw`(?:6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|\d{1,4})`;
307
+ const IP_OCTET = String.raw`(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)`;
308
+ const IP_SOURCE = String.raw`(?:${IP_OCTET}\.){3}${IP_OCTET}`;
309
+ const URL_SOURCE = String.raw`https?:\/\/[^\s"'<>]+`;
310
+ const EMBEDDED_SOURCES = {
311
+ base64: BASE64_SOURCE,
312
+ date: DATE_SOURCE,
313
+ duration: DURATION_SOURCE,
314
+ email: String.raw`[^\s@"'<>]+@[^\s@"'<>]+\.[^\s@"'<>]+`,
315
+ float: FLOAT_SOURCE,
316
+ hex: HEX_SOURCE,
317
+ int: INT_SOURCE,
318
+ ip: IP_SOURCE,
319
+ iso8601: ISO8601_SOURCE,
320
+ number: NUMBER_SOURCE,
321
+ path: String.raw`\.{0,2}\/[^\s"'<>]*`,
322
+ port: PORT_SOURCE,
323
+ semver: SEMVER_SOURCE,
324
+ sha: SHA_SOURCE,
325
+ time: TIME_SOURCE,
326
+ ulid: ULID_SOURCE,
327
+ url: URL_SOURCE,
328
+ uuid: UUID_SOURCE
329
+ };
330
+ const wholeRe = (source) => new RegExp(`^(?:${source})$`, "u");
331
+ const WHOLE_RES = Object.fromEntries(Object.entries(EMBEDDED_SOURCES).map(([kind, source]) => [kind, wholeRe(source)]));
332
+ const wholeMatches = (kind, actual) => WHOLE_RES[kind]?.test(actual) === true;
333
+ const PLACEHOLDER_RE = new RegExp(String.raw`\{\{(?<kind>${[...TOKEN_KINDS].toSorted((a, b) => b.length - a.length).join("|")})(?:#(?<ref>[\w.-]+))?\}\}`, "gu");
334
+ function hasPlaceholders(value) {
335
+ PLACEHOLDER_RE.lastIndex = 0;
336
+ return PLACEHOLDER_RE.test(value);
337
+ }
338
+ function canonical(value) {
339
+ if (Array.isArray(value)) return value.map((item) => canonical(item));
340
+ if (!isRecord(value)) return value;
341
+ return Object.fromEntries(Object.keys(value).toSorted().map((key) => [key, canonical(value[key])]));
342
+ }
343
+ function capturedEquals(a, b) {
344
+ if (Object.is(a, b)) return true;
345
+ if (typeof a === "number" && typeof b === "string" || typeof a === "string" && typeof b === "number") return String(a) === String(b);
346
+ return typeof a === "object" && typeof b === "object" && JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
347
+ }
348
+ function recordRef(name, actual, scope) {
349
+ if (scope.has(name)) return capturedEquals(scope.get(name), actual);
350
+ scope.set(name, actual);
351
+ return true;
352
+ }
353
+ function isPortValue(value) {
354
+ return Number.isInteger(value) && value >= 0 && value <= 65535;
355
+ }
356
+ const NUMERIC_KINDS = {
357
+ float: Number.isFinite,
358
+ int: Number.isInteger,
359
+ number: Number.isFinite,
360
+ port: isPortValue
361
+ };
362
+ function kindMatches(kind, actual, scope) {
363
+ if (kind === "any") return true;
364
+ if (kind === "workdir") return typeof actual === "string" && actual === scope.workdir;
365
+ const numeric = NUMERIC_KINDS[kind];
366
+ if (numeric !== void 0 && typeof actual === "number") return numeric(actual);
367
+ if (typeof actual !== "string") return false;
368
+ if (kind === "string") return true;
369
+ return wholeMatches(kind, actual);
370
+ }
371
+ function matcherMatches(matcher, actual, scope) {
372
+ if (matcher.kind === "includes") return typeof actual === "string" && matcher.includes !== void 0 && actual.includes(matcher.includes);
373
+ if (matcher.kind === "regex") return typeof actual === "string" && matcher.regex?.test(actual) === true;
374
+ if (matcher.kind === "ref") {
375
+ if (matcher.notRef !== void 0 && scope.has(matcher.notRef) && capturedEquals(scope.get(matcher.notRef), actual)) return false;
376
+ return matcher.refName !== void 0 && recordRef(matcher.refName, actual, scope);
377
+ }
378
+ return kindMatches(matcher.kind, actual, scope);
379
+ }
380
+ function placeholderSource(kind, scope, inLine) {
381
+ if (kind === "workdir") return scope.workdir === void 0 ? "(?!)" : escapeRegExp(scope.workdir);
382
+ const source = EMBEDDED_SOURCES[kind];
383
+ if (source !== void 0) return source;
384
+ return kind === "any" && !inLine ? String.raw`[\s\S]*?` : String.raw`[^\n]*?`;
385
+ }
386
+ function escapeRegExp(text) {
387
+ return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
388
+ }
389
+ function parsePlaceholderString(expected, scope) {
390
+ PLACEHOLDER_RE.lastIndex = 0;
391
+ const tokens = [...expected.matchAll(PLACEHOLDER_RE)].flatMap((found) => {
392
+ const { kind: written, ref } = found.groups ?? {};
393
+ const kind = TOKEN_KINDS.find((known) => known === written);
394
+ return kind === void 0 ? [] : [{
395
+ found,
396
+ kind,
397
+ ref
398
+ }];
399
+ });
400
+ let source = "";
401
+ let lastIndex = 0;
402
+ const pieces = [];
403
+ let framed = false;
404
+ for (const { found, kind } of tokens) {
405
+ const inLine = isFramedInLine(expected, found.index, found.index + found[0].length);
406
+ framed ||= kind === "any" && inLine;
407
+ pieces.push(expected.slice(lastIndex, found.index));
408
+ source += `${escapeRegExp(expected.slice(lastIndex, found.index))}(${placeholderSource(kind, scope, inLine)})`;
409
+ lastIndex = found.index + found[0].length;
410
+ }
411
+ pieces.push(expected.slice(lastIndex));
412
+ source += escapeRegExp(expected.slice(lastIndex));
413
+ const [only] = tokens;
414
+ return {
415
+ pieces: !framed && tokens.every(({ kind, ref }) => kind === "any" && ref === void 0) ? pieces : null,
416
+ pattern: new RegExp(`^${source}$`, "u"),
417
+ refs: tokens.map(({ kind, ref }, index) => ({
418
+ index,
419
+ kind,
420
+ ref
421
+ })),
422
+ single: only?.found[0] === expected ? {
423
+ kind: only.kind,
424
+ ref: only.ref
425
+ } : null,
426
+ source
427
+ };
428
+ }
429
+ function placeholderPatternSource(expected) {
430
+ return parsePlaceholderString(expected, new CaptureScope()).source;
431
+ }
432
+ function placeholderStringMatches(expected, actual, scope) {
433
+ const parsed = parsePlaceholderString(expected, scope);
434
+ if (parsed.single) {
435
+ if (!kindMatches(parsed.single.kind, actual, scope)) return false;
436
+ if (parsed.single.ref !== void 0) return recordRef(parsed.single.ref, actual, scope);
437
+ return true;
438
+ }
439
+ if (typeof actual !== "string" && typeof actual !== "number") return false;
440
+ const text = String(actual);
441
+ if (parsed.pieces !== null) return piecesMatch(parsed.pieces, text);
442
+ const found = parsed.pattern.exec(text);
443
+ if (!found) return false;
444
+ for (const entry of parsed.refs) {
445
+ if (entry.ref === void 0) continue;
446
+ if (!recordRef(entry.ref, found[entry.index + 1], scope)) return false;
447
+ }
448
+ return true;
449
+ }
450
+ //#endregion
451
+ //#region src/expectations/tokens/match.ts
452
+ /** A dynamic-value matcher, created through the `match` factories. */
453
+ var Matcher = class {
454
+ /** For kind 'includes': the substring the actual value must contain. */
455
+ includes;
456
+ kind;
457
+ /** For kind 'ref': the capture to assert inequality against. */
458
+ notRef;
459
+ /** For kind 'ref': the capture name. */
460
+ refName;
461
+ /** For kind 'regex': the pattern the actual value must match. */
462
+ regex;
463
+ constructor(kind, options = {}) {
464
+ this.includes = options.includes;
465
+ this.kind = kind;
466
+ this.notRef = options.notRef;
467
+ this.refName = options.refName;
468
+ this.regex = options.regex;
469
+ }
470
+ /** Placeholder-style rendering used in failure diffs and serialization. */
471
+ toString() {
472
+ if (this.kind === "includes") return `{{includes:${this.includes}}}`;
473
+ if (this.kind === "regex") return `{{regex:${this.regex?.source}}}`;
474
+ if (this.kind === "ref") return this.notRef === void 0 ? `{{ref#${this.refName}}}` : `{{ref#${this.refName}!${this.notRef}}}`;
475
+ return this.refName === void 0 ? `{{${this.kind}}}` : `{{${this.kind}#${this.refName}}}`;
476
+ }
477
+ toJSON() {
478
+ return this.toString();
479
+ }
480
+ };
481
+ const typed = (kind) => () => new Matcher(kind);
482
+ const match = {
483
+ any: typed("any"),
484
+ base64: typed("base64"),
485
+ /** Matches a calendar date (`YYYY-MM-DD`). */
486
+ date: typed("date"),
487
+ /** Matches a human duration (`12ms`, `1.5s`, `2m`, `3h`). */
488
+ duration: typed("duration"),
489
+ email: typed("email"),
490
+ /** Matches a float: any finite number in JSON; in text the decimal part is required. */
491
+ float: typed("float"),
492
+ hex: typed("hex"),
493
+ /** Matches a string containing the substring; code-only, like `match.regex`. */
494
+ includes: (substring) => new Matcher("includes", { includes: substring }),
495
+ /** Matches an integer (or an integer string in text contexts). */
496
+ int: typed("int"),
497
+ /** Matches an IPv4 address. */
498
+ ip: typed("ip"),
499
+ iso8601: typed("iso8601"),
500
+ /** Matches a number (or a numeric string in text contexts). */
501
+ number: typed("number"),
502
+ /** Matches a filesystem path (`/...` or `./...`). */
503
+ path: typed("path"),
504
+ /** Matches a TCP/UDP port number (0-65535). */
505
+ port: typed("port"),
506
+ /** Capture-and-compare: the first `ref(name)` captures, every later one must equal it; `{ not }` asserts inequality. */
507
+ ref: (name, options) => new Matcher("ref", {
508
+ notRef: options?.not,
509
+ refName: name
510
+ }),
511
+ regex: (regex) => new Matcher("regex", { regex }),
512
+ /** Matches a semantic version (`1.2.3`, `2.0.0-rc.1`). */
513
+ semver: typed("semver"),
514
+ /** Matches a git SHA (7-64 hex chars). */
515
+ sha: typed("sha"),
516
+ string: typed("string"),
517
+ /** Matches a wall-clock time (`HH:MM` or `HH:MM:SS`). */
518
+ time: typed("time"),
519
+ ulid: typed("ulid"),
520
+ /** Matches an http(s) URL. */
521
+ url: typed("url"),
522
+ uuid: typed("uuid"),
523
+ /** Matches the exact working directory of the current spec. */
524
+ workdir: typed("workdir")
525
+ };
526
+ //#endregion
527
+ //#region src/expectations/tokens/structural.ts
528
+ function isPlainObject(value) {
529
+ return value !== null && typeof value === "object" && !Array.isArray(value);
530
+ }
531
+ function structurally(expected, actual, scope, subset) {
532
+ if (expected instanceof Matcher) return matcherMatches(expected, actual, scope);
533
+ if (typeof expected === "string" && hasPlaceholders(expected)) return placeholderStringMatches(expected, actual, scope);
534
+ if (Array.isArray(expected)) return Array.isArray(actual) && actual.length === expected.length && expected.every((item, index) => structurally(item, actual[index], scope, subset));
535
+ if (isPlainObject(expected)) return isPlainObject(actual) && (subset || Object.keys(actual).length === Object.keys(expected).length) && Object.keys(expected).every((key) => key in actual && structurally(expected[key], actual[key], scope, subset));
536
+ return Object.is(expected, actual);
537
+ }
538
+ function structuralEquals(expected, actual, scope) {
539
+ return structurally(expected, actual, scope, false);
540
+ }
541
+ function structuralSubset(expected, actual, scope) {
542
+ return structurally(expected, actual, scope, true);
543
+ }
544
+ /** A text without tokens reads as itself, every character literal. */
545
+ function textEquals(expected, actual, scope) {
546
+ return placeholderStringMatches(expected, actual, scope);
547
+ }
548
+ //#endregion
549
+ //#region src/stubs/matching/route.ts
550
+ const RELATIVE_BASE = "http://stub.invalid";
551
+ const UNICODE = "u";
552
+ function splitDeclared(url) {
553
+ const separator = url.indexOf("?");
554
+ if (separator === -1) return {
555
+ base: url,
556
+ query: new URLSearchParams()
557
+ };
558
+ return {
559
+ base: url.slice(0, separator),
560
+ query: new URLSearchParams(url.slice(separator + 1))
561
+ };
562
+ }
563
+ function safeDecode(text) {
564
+ try {
565
+ return decodeURIComponent(text);
566
+ } catch {
567
+ return text;
568
+ }
569
+ }
570
+ function wholeUrl(prefix, base) {
571
+ return new RegExp(String.raw`^${prefix}${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, UNICODE);
572
+ }
573
+ function valueMatches(declared, observed) {
574
+ return textEquals(declared, observed, new CaptureScope());
575
+ }
576
+ /** A RegExp tests the full URL, a path form ignores the origin; `{{token}}` segments compare structurally and declared query params are a subset. */
577
+ function urlMatches(declared, observed) {
578
+ if (declared instanceof RegExp) return declared.test(observed);
579
+ let url;
580
+ try {
581
+ url = new URL(observed, RELATIVE_BASE);
582
+ } catch {
583
+ return false;
584
+ }
585
+ const { base, query } = splitDeclared(declared);
586
+ const observedBase = base.startsWith("/") ? url.pathname : `${url.origin}${url.pathname}`;
587
+ const decodedBase = base.startsWith("/") ? safeDecode(url.pathname) : `${url.origin}${safeDecode(url.pathname)}`;
588
+ if (!valueMatches(base, observedBase) && !valueMatches(base, decodedBase)) return false;
589
+ for (const key of new Set(query.keys())) {
590
+ const observedValues = url.searchParams.getAll(key);
591
+ if (!query.getAll(key).every((value) => observedValues.some((actual) => valueMatches(value, actual)))) return false;
592
+ }
593
+ return true;
594
+ }
595
+ /** No narrower than `stubMatches`, which stays the authority on what matches. */
596
+ function routePatternOf(declared) {
597
+ if (declared instanceof RegExp) return declared;
598
+ const { base } = splitDeclared(declared);
599
+ if (base.startsWith("/")) return wholeUrl(String.raw`https?:\/\/[^/?#]+`, base);
600
+ if (hasPlaceholders(base)) return wholeUrl("", base);
601
+ return base;
602
+ }
603
+ function routeKeyOf({ method, url }) {
604
+ return url instanceof RegExp ? `${method} re:${url.source}` : `${method} str:${url}`;
605
+ }
606
+ function describeAbsent(absentHeaders) {
607
+ if (absentHeaders === void 0 || absentHeaders.length === 0) return "";
608
+ return ` (without header${absentHeaders.length > 1 ? "s" : ""} ${absentHeaders.join(", ")})`;
609
+ }
610
+ function describeRoute({ absentHeaders, method, url }) {
611
+ return `${method === "*" ? "ANY" : method} ${url instanceof RegExp ? String(url) : url}${describeAbsent(absentHeaders)}`;
612
+ }
613
+ //#endregion
614
+ //#region src/stubs/matching/stub-queue.ts
615
+ function stubMatches(stub, request) {
616
+ const { absentHeaders, match, method, url } = stub.request;
617
+ if (method !== "*" && method.toUpperCase() !== request.method.toUpperCase()) return false;
618
+ if (!urlMatches(url, request.url)) return false;
619
+ if ((absentHeaders ?? []).some((name) => request.headers[name.toLowerCase()] !== void 0)) return false;
620
+ return match ? match(request) : true;
621
+ }
622
+ function isNever({ required, times }) {
623
+ return required === true && times === 0;
624
+ }
625
+ function stateOf({ stub, served }) {
626
+ const { times } = stub;
627
+ if (isNever(stub)) return served === 0 ? " (never)" : ` (never, reached ${served} time(s))`;
628
+ if (served >= (times ?? Number.POSITIVE_INFINITY)) return ` (exhausted after ${times})`;
629
+ return served === 0 ? "" : ` (served ${served} time(s))`;
630
+ }
631
+ /** The first unexhausted stub in declaration order answers; `times: n` is spent after n serves. */
632
+ var StubQueue = class {
633
+ entries;
634
+ constructor(stubs) {
635
+ this.entries = stubs.map((stub) => ({
636
+ reached: [],
637
+ stub,
638
+ served: 0
639
+ }));
640
+ }
641
+ /** Deduped by pattern: two stubs on one URL share one handler and one queue. */
642
+ get routes() {
643
+ const routes = /* @__PURE__ */ new Map();
644
+ for (const { stub } of this.entries) {
645
+ const url = routePatternOf(stub.request.url);
646
+ const key = String(url);
647
+ const route = routes.get(key) ?? {
648
+ methods: [],
649
+ url
650
+ };
651
+ if (!route.methods.includes(stub.request.method)) route.methods.push(stub.request.method);
652
+ routes.set(key, route);
653
+ }
654
+ return [...routes.values()];
655
+ }
656
+ declaredRoutes() {
657
+ return this.entries.map(({ stub }) => describeRoute(stub.request));
658
+ }
659
+ /** `null` when nothing unexhausted matches: the strict-violation signal. */
660
+ take(request) {
661
+ for (const entry of this.entries) {
662
+ if (entry.served >= (entry.stub.times ?? Number.POSITIVE_INFINITY) && !isNever(entry.stub) || !stubMatches(entry.stub, request)) continue;
663
+ entry.served += 1;
664
+ entry.reached.push(`${request.method} ${request.url}`);
665
+ return entry.stub;
666
+ }
667
+ return null;
668
+ }
669
+ verdict(unmatched = null) {
670
+ const errors = [
671
+ this.neverError(),
672
+ unmatched,
673
+ this.unmetError()
674
+ ].filter((error) => error !== null);
675
+ if (errors.length <= 1) return errors[0] ?? null;
676
+ const reports = errors.map(({ message }) => message).join("\n\n");
677
+ return new AggregateError(errors, refusal({
678
+ fix: "correct each outcome in turn, by its own fix",
679
+ found: `${errors.length} outcomes of one action, each with its own refusal:\n\n${reports}`,
680
+ read: "docs/reference/stubs.md#strict-by-construction",
681
+ why: "A reached never stub, an unmatched request and an unmet required stub are independent facts about one action, so all are reported at once."
682
+ }));
683
+ }
684
+ neverError() {
685
+ const reached = this.entries.filter(({ stub, served }) => isNever(stub) && served > 0);
686
+ if (reached.length === 0) return null;
687
+ const lines = reached.map(({ reached: calls, stub }) => ` - ${describeRoute(stub.request)}: reached by ${calls.join(", ")}`);
688
+ return new Error(refusal({
689
+ fix: "take the call out of the path under test or, if the call is now intended, replace the never stub with one that answers it",
690
+ found: `a route the chain declared never requested was reached:\n${lines.join("\n")}`,
691
+ read: "docs/reference/stubs.md#never-a-call-that-must-not-happen",
692
+ why: "A never stub states the subject must not reach its route within the chain's terminal action, or the `intercept()` scope, it is declared for."
693
+ }));
694
+ }
695
+ unmetError() {
696
+ const unmet = [];
697
+ for (const { stub, served } of this.entries) {
698
+ if (stub.required !== true || isNever(stub)) continue;
699
+ const { times } = stub;
700
+ if (times === void 0) {
701
+ if (served === 0) unmet.push(` - ${describeRoute(stub.request)}: declared and required but never requested`);
702
+ } else if (served !== times) unmet.push(` - ${describeRoute(stub.request)}: declared required with times: ${times} but was requested ${served} time(s)`);
703
+ }
704
+ if (unmet.length === 0) return null;
705
+ return new Error(refusal({
706
+ fix: "the code no longer makes the call, or the stub no longer describes it: correct the one that drifted",
707
+ found: `required stub(s) never satisfied during the chain:\n${unmet.join("\n")}`,
708
+ read: "docs/reference/stubs.md#required-is-verified-per-terminal-action",
709
+ why: "A required stub states the call MUST happen, within the terminal action it is declared for."
710
+ }));
711
+ }
712
+ unmatchedError(method, url) {
713
+ const declared = this.entries.length === 0 ? " (no stubs declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.stub.request)}${stateOf(entry)}`).join("\n");
714
+ return new Error(refusal("network-strict-after-first-stub", {
715
+ fix: `declare a stub for ${method} ${url}, or raise the \`times\` of the one it exhausted`,
716
+ found: `an unmatched outgoing request, ${method} ${url}; declared:\n${declared}`
717
+ }));
718
+ }
719
+ };
720
+ //#endregion
721
+ //#region src/stubs/delivery/stream-body.ts
722
+ /** One serialisation for all three deliveries (msw server, msw worker, `node:http` stub). */
723
+ function toReadableStream(body) {
724
+ const encoder = new TextEncoder();
725
+ let index = 0;
726
+ return new ReadableStream({ async pull(controller) {
727
+ const chunk = body.chunks[index];
728
+ if (chunk === void 0) {
729
+ controller.close();
730
+ return;
731
+ }
732
+ if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
733
+ index += 1;
734
+ controller.enqueue(encoder.encode(chunk));
735
+ } });
736
+ }
737
+ //#endregion
738
+ //#region src/stubs/delivery/msw-handlers.ts
739
+ const NEVER_CACHED = { "cache-control": "no-store" };
740
+ function toMswResponse(msw, response) {
741
+ const { body, headers, status = 200 } = response;
742
+ if (response.transport === "network-error") return msw.HttpResponse.error();
743
+ if (body === null || body === void 0) return new msw.HttpResponse(null, {
744
+ headers: {
745
+ ...NEVER_CACHED,
746
+ ...headers
747
+ },
748
+ status
749
+ });
750
+ if (isStreamBody(body)) return new msw.HttpResponse(toReadableStream(body), {
751
+ headers: {
752
+ ...NEVER_CACHED,
753
+ "content-type": body.contentType,
754
+ ...headers
755
+ },
756
+ status
757
+ });
758
+ if (typeof body === "string") return new msw.HttpResponse(body, {
759
+ headers: {
760
+ ...NEVER_CACHED,
761
+ "content-type": "text/plain; charset=utf-8",
762
+ ...headers
763
+ },
764
+ status
765
+ });
766
+ return msw.HttpResponse.json(body, {
767
+ headers: {
768
+ ...NEVER_CACHED,
769
+ ...headers
770
+ },
771
+ status
772
+ });
773
+ }
774
+ function handlerOf(msw, method) {
775
+ return {
776
+ "*": msw.http.all,
777
+ DELETE: msw.http.delete,
778
+ GET: msw.http.get,
779
+ HEAD: msw.http.head,
780
+ OPTIONS: msw.http.options,
781
+ PATCH: msw.http.patch,
782
+ POST: msw.http.post,
783
+ PUT: msw.http.put
784
+ }[method.toUpperCase()];
785
+ }
786
+ function buildStubHandlers(msw, stubs, bypass) {
787
+ const queue = new StubQueue(stubs);
788
+ let violation = null;
789
+ const recordViolation = (method, url) => {
790
+ violation ??= queue.unmatchedError(method, url);
791
+ return msw.HttpResponse.json({ error: `@jterrazz/test strict stubs: unmatched request ${method} ${url}` }, {
792
+ headers: NEVER_CACHED,
793
+ status: 501
794
+ });
795
+ };
796
+ const handlers = [];
797
+ for (const route of queue.routes) for (const method of route.methods) {
798
+ const handlerFn = handlerOf(msw, method);
799
+ if (handlerFn === void 0) continue;
800
+ handlers.push(handlerFn(route.url, async ({ request }) => {
801
+ const observed = await observe(request);
802
+ const stub = queue.take(observed);
803
+ if (!stub) return recordViolation(request.method, request.url);
804
+ const response = typeof stub.response === "function" ? stub.response(observed) : stub.response;
805
+ await pause(response.delay ?? 0);
806
+ return toMswResponse(msw, response);
807
+ }));
808
+ }
809
+ handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
810
+ return {
811
+ handlers,
812
+ violation: () => queue.verdict(violation)
813
+ };
814
+ }
815
+ async function observe(request) {
816
+ let body = null;
817
+ const rawText = await request.clone().text();
818
+ if (rawText) try {
819
+ body = JSON.parse(rawText);
820
+ } catch {
821
+ body = rawText;
822
+ }
823
+ return {
824
+ body,
825
+ headers: Object.fromEntries(request.headers),
826
+ method: request.method.toUpperCase(),
827
+ url: request.url
828
+ };
829
+ }
830
+ //#endregion
831
+ //#region src/stubs/delivery/browser-page/msw-worker.adapter.ts
832
+ const RUNNER_PREFIXES = [
833
+ "/.artifacts/",
834
+ "/@fs/",
835
+ "/@id/",
836
+ "/@vite",
837
+ "/node_modules/",
838
+ "/__vitest"
839
+ ];
840
+ let workerInstance = null;
841
+ function isRunnerRequest(url) {
842
+ const { pathname } = new URL(url);
843
+ return RUNNER_PREFIXES.some((prefix) => pathname.startsWith(prefix));
844
+ }
845
+ async function ensureStubWorker() {
846
+ if (workerInstance !== null) return;
847
+ const { setupWorker } = await import("msw/browser");
848
+ const worker = setupWorker();
849
+ workerInstance = worker;
850
+ await worker.start({
851
+ onUnhandledRequest: (request, print) => {
852
+ if (!isRunnerRequest(request.url)) print.error();
853
+ },
854
+ quiet: true,
855
+ serviceWorker: { url: "/mockServiceWorker.js" }
856
+ });
857
+ }
858
+ /** Starts even for an empty list: `network-strict-after-first-stub` is total here, a component given no stub has no network. */
859
+ async function registerWorkerStubs(stubs) {
860
+ await ensureStubWorker();
861
+ const { handlers, violation } = buildStubHandlers(await import("msw"), stubs, isRunnerRequest);
862
+ workerInstance?.use(...handlers);
863
+ return {
864
+ cleanup: () => {
865
+ workerInstance?.resetHandlers();
866
+ },
867
+ violation
868
+ };
869
+ }
870
+ function resetWorkerStubs() {
871
+ if (workerInstance !== null) workerInstance.resetHandlers();
872
+ }
873
+ /** Dropped at test end, not render end: a mounted component keeps fetching after its render resolves. */
874
+ function mswWorker(atTestEnd) {
875
+ return {
876
+ accepts: () => {},
877
+ arm: async (stubs) => {
878
+ const registration = await registerWorkerStubs(stubs);
879
+ atTestEnd(resetWorkerStubs);
880
+ return { violation: registration.violation };
881
+ }
882
+ };
883
+ }
884
+ //#endregion
885
+ //#region src/stubs/stub.ts
886
+ function defineStub(stub) {
887
+ return stub;
888
+ }
889
+ function flatten(items) {
890
+ const flat = [];
891
+ for (const item of items) if (Array.isArray(item)) flat.push(...flatten(item));
892
+ else if ("stubs" in item) flat.push(...item.stubs);
893
+ else flat.push(item);
894
+ return flat;
895
+ }
896
+ /** Compose stubs, lists and composites, order preserved; `.with()` derives a scenario. */
897
+ function defineStubs(...items) {
898
+ const stubs = Object.freeze(flatten(items));
899
+ return {
900
+ stubs,
901
+ with(...overrides) {
902
+ const added = flatten(overrides);
903
+ const claimed = new Set(added.map((stub) => routeKeyOf(stub.request)));
904
+ const kept = stubs.filter((stub) => !claimed.has(routeKeyOf(stub.request)));
905
+ return defineStubs([...added, ...kept]);
906
+ }
907
+ };
908
+ }
909
+ function stubsOf(input) {
910
+ return flatten([input]);
911
+ }
912
+ //#endregion
913
+ //#region src/stubs/intercept/declared.ts
914
+ function isRequest(value) {
915
+ return "adapter" in value;
916
+ }
917
+ /** A bare request is refused; the compiler already refuses the form, so only an untyped call reaches the refusal. */
918
+ function declared(call, requestOrStubs, maybeResponse) {
919
+ if (!isRequest(requestOrStubs)) return stubsOf(requestOrStubs);
920
+ if (maybeResponse === void 0) throw new Error(refusal({
921
+ fix: `pass a stub, defineStub({ request, response }), or the inline pair ${call}(request, response)`,
922
+ found: `${call}(request), a bare request with no response`,
923
+ read: "docs/reference/stubs.md#using-them-intercept",
924
+ why: "A stub is a request and its response: a request alone answers nothing."
925
+ }));
926
+ return [{
927
+ request: requestOrStubs,
928
+ response: maybeResponse
929
+ }];
930
+ }
931
+ //#endregion
932
+ //#region src/stubs/intercept/intercept.capability.ts
933
+ /** Calls append (`network-strict-after-first-stub`); composition and overriding live in `defineStubs()` / `.with()`. Strict once armed. */
934
+ const intercept$1 = defineCapability({
935
+ arm: async (stubs, { network }) => await network.arm(stubs),
936
+ initial: [],
937
+ name: "intercept",
938
+ phase: 40,
939
+ reduce: (stubs, [requestOrStubs, response], { adapters }) => {
940
+ adapters.network.accepts();
941
+ return [...stubs, ...declared(".intercept", requestOrStubs, response)];
942
+ },
943
+ row: {
944
+ group: "Setup",
945
+ name: ".intercept()",
946
+ probe: ".intercept("
947
+ }
948
+ });
949
+ //#endregion
950
+ //#region src/specifications/component/page/page-runtime.ts
951
+ function pageDirectory() {
952
+ return "";
953
+ }
954
+ function afterThisTest(teardown) {
955
+ onTestFinished(teardown);
956
+ }
957
+ /** `Date` only: faking the page's timers would stop React's scheduler. A chain's instant was judged before the page; the project's is judged here. */
958
+ function pinClock(iso) {
959
+ vi.useFakeTimers({
960
+ now: instantOf(iso, (value) => `component({ clock: ${value} })`),
961
+ toFake: ["Date"]
962
+ });
963
+ }
964
+ function releaseClock() {
965
+ vi.useRealTimers();
966
+ }
967
+ function providedClock() {
968
+ return inject("componentClock");
969
+ }
970
+ function providedViewport() {
971
+ return inject("componentViewport");
972
+ }
973
+ /** `vitest/browser` loads in a page alone, so it is reached only when a size is set. */
974
+ async function setViewport(size) {
975
+ const { page } = await import("vitest/browser");
976
+ await page.viewport(size.width, size.height);
977
+ }
978
+ /** A shared symbol rather than an import: the setup module and this bundle are two graphs in one page. */
979
+ function projectWrapper() {
980
+ const wrap = globalThis[Symbol.for("@jterrazz/test:component-wrap")];
981
+ return typeof wrap === "function" ? wrap : void 0;
982
+ }
983
+ //#endregion
984
+ //#region src/specifications/component/page/page-clock.adapter.ts
985
+ /** The chain's instant wins over `component({ clock })`'s; `Date` stays pinned for the rest of the test, since a mounted component keeps running. */
986
+ const pageClock = { pin: (iso) => {
987
+ const instant = iso ?? providedClock();
988
+ if (instant !== void 0) pinClock(instant);
989
+ afterThisTest(releaseClock);
990
+ return {};
991
+ } };
992
+ //#endregion
993
+ //#region src/specifications/component/page/vitest-browser-page.adapter.ts
994
+ async function adapter() {
995
+ return await Promise.resolve().then(() => vitest_browser_dom_exports);
996
+ }
997
+ const vitestBrowserPage = {
998
+ afterThisTest,
999
+ ariaTree: async () => {
1000
+ return await (await adapter()).ariaTree();
1001
+ },
1002
+ mountDom: async (mount) => {
1003
+ return (await adapter()).mountDom(mount);
1004
+ },
1005
+ mountReact: async (ui) => {
1006
+ return await (await adapter()).mountReact(ui);
1007
+ },
1008
+ projectWrapper: () => projectWrapper(),
1009
+ visitor: async (surface) => {
1010
+ return (await adapter()).componentVerbs(surface);
1011
+ }
1012
+ };
1013
+ //#endregion
1014
+ //#region src/chain/action.ts
1015
+ function defineAction(definition) {
1016
+ return {
1017
+ ...definition,
1018
+ kind: "action"
1019
+ };
1020
+ }
1021
+ //#endregion
1022
+ //#region src/expectations/text/console.ts
1023
+ function consoleOf(lines) {
1024
+ return lines.map((line) => `[${line.type}] ${line.text}`).join("\n");
1025
+ }
1026
+ function errorsOf(lines) {
1027
+ return lines.filter((line) => line.type === "error").map((line) => line.text).join("\n");
1028
+ }
1029
+ //#endregion
1030
+ //#region src/visitor/matrix.ts
1031
+ const EVERY_SURFACE_VERBS = [{
1032
+ group: "Verb",
1033
+ name: "see",
1034
+ probe: ".see("
1035
+ }, {
1036
+ group: "Verb",
1037
+ name: "fill",
1038
+ probe: ".fill("
1039
+ }];
1040
+ const BROWSER_VERBS = [
1041
+ {
1042
+ group: "Verb",
1043
+ name: "click",
1044
+ probe: ".click("
1045
+ },
1046
+ {
1047
+ group: "Verb",
1048
+ name: "gone",
1049
+ probe: ".gone("
1050
+ },
1051
+ {
1052
+ group: "Verb",
1053
+ name: "press",
1054
+ probe: ".press("
1055
+ },
1056
+ {
1057
+ group: "Verb",
1058
+ name: "hover",
1059
+ probe: ".hover("
1060
+ },
1061
+ {
1062
+ group: "Verb",
1063
+ name: "check",
1064
+ probe: ".check("
1065
+ },
1066
+ {
1067
+ group: "Verb",
1068
+ name: "select",
1069
+ probe: ".select("
1070
+ }
1071
+ ];
1072
+ const EVERY_SURFACE_TARGETS = [
1073
+ {
1074
+ group: "Target",
1075
+ name: "button",
1076
+ probe: "button("
1077
+ },
1078
+ {
1079
+ group: "Target",
1080
+ name: "field",
1081
+ probe: "field("
1082
+ },
1083
+ {
1084
+ group: "Target",
1085
+ member: "content",
1086
+ name: "content",
1087
+ probe: "content("
1088
+ },
1089
+ {
1090
+ group: "Target",
1091
+ name: "testId",
1092
+ probe: "testId("
1093
+ }
1094
+ ];
1095
+ const BROWSER_TARGETS = [
1096
+ {
1097
+ group: "Target",
1098
+ name: "heading",
1099
+ probe: "heading("
1100
+ },
1101
+ {
1102
+ group: "Target",
1103
+ name: "link",
1104
+ probe: "link("
1105
+ },
1106
+ {
1107
+ group: "Target",
1108
+ name: "dialog",
1109
+ probe: "dialog("
1110
+ },
1111
+ {
1112
+ group: "Target",
1113
+ name: "status",
1114
+ probe: "status("
1115
+ },
1116
+ {
1117
+ group: "Target",
1118
+ name: "table",
1119
+ probe: "table("
1120
+ },
1121
+ {
1122
+ group: "Target",
1123
+ name: "row",
1124
+ probe: "row("
1125
+ },
1126
+ {
1127
+ group: "Target",
1128
+ name: "listitem",
1129
+ probe: "listitem("
1130
+ },
1131
+ {
1132
+ group: "Target",
1133
+ name: "option",
1134
+ probe: "option("
1135
+ },
1136
+ {
1137
+ group: "Target",
1138
+ name: "banner",
1139
+ probe: "banner("
1140
+ },
1141
+ {
1142
+ group: "Target",
1143
+ name: "complementary",
1144
+ probe: "complementary("
1145
+ },
1146
+ {
1147
+ group: "Target",
1148
+ name: "contentinfo",
1149
+ probe: "contentinfo("
1150
+ },
1151
+ {
1152
+ group: "Target",
1153
+ name: "form",
1154
+ probe: "form("
1155
+ },
1156
+ {
1157
+ group: "Target",
1158
+ name: "main",
1159
+ probe: "main("
1160
+ },
1161
+ {
1162
+ group: "Target",
1163
+ name: "navigation",
1164
+ probe: "navigation("
1165
+ },
1166
+ {
1167
+ group: "Target",
1168
+ name: "region",
1169
+ probe: "region("
1170
+ },
1171
+ {
1172
+ group: "Target",
1173
+ name: "search",
1174
+ probe: "search("
1175
+ }
1176
+ ];
1177
+ const WITHIN = {
1178
+ group: "Target",
1179
+ name: "within",
1180
+ probe: "within("
1181
+ };
1182
+ const BROWSER_STATES = [
1183
+ {
1184
+ group: "Target",
1185
+ name: "focused",
1186
+ probe: "focused("
1187
+ },
1188
+ {
1189
+ group: "Target",
1190
+ name: "selected",
1191
+ probe: "selected("
1192
+ },
1193
+ {
1194
+ group: "Target",
1195
+ name: "valued",
1196
+ probe: "valued("
1197
+ },
1198
+ {
1199
+ group: "Target",
1200
+ name: "disabled",
1201
+ probe: "disabled("
1202
+ },
1203
+ {
1204
+ group: "Target",
1205
+ name: "enabled",
1206
+ probe: "enabled("
1207
+ }
1208
+ ];
1209
+ //#endregion
1210
+ //#region src/specifications/component/render/console-recorder.ts
1211
+ const CONSOLE_METHODS = [
1212
+ "debug",
1213
+ "error",
1214
+ "info",
1215
+ "log",
1216
+ "warn"
1217
+ ];
1218
+ function recordConsole() {
1219
+ const entries = [];
1220
+ const originals = /* @__PURE__ */ new Map();
1221
+ for (const method of CONSOLE_METHODS) {
1222
+ const original = console[method];
1223
+ originals.set(method, original);
1224
+ console[method] = (...args) => {
1225
+ const [only] = args;
1226
+ const raised = args.length === 1 && only instanceof Error ? only.message : void 0;
1227
+ entries.push({
1228
+ text: args.map(String).join(" "),
1229
+ type: method,
1230
+ ...raised === void 0 ? {} : { raised }
1231
+ });
1232
+ original(...args);
1233
+ };
1234
+ }
1235
+ const onError = (event) => {
1236
+ const last = entries.at(-1);
1237
+ if (last?.type === "error" && last.raised === event.message) entries.pop();
1238
+ entries.push({
1239
+ text: event.message,
1240
+ type: "error"
1241
+ });
1242
+ };
1243
+ globalThis.addEventListener("error", onError);
1244
+ let stopped = false;
1245
+ return {
1246
+ entries,
1247
+ stop: () => {
1248
+ if (stopped) return;
1249
+ stopped = true;
1250
+ for (const method of CONSOLE_METHODS) {
1251
+ const original = originals.get(method);
1252
+ if (original !== void 0) console[method] = original;
1253
+ }
1254
+ globalThis.removeEventListener("error", onError);
1255
+ }
1256
+ };
1257
+ }
1258
+ //#endregion
1259
+ //#region src/expectations/text/ansi.ts
1260
+ const ANSI_RE = /\u001B\[[0-9;]*m/gu;
1261
+ /** rule `no-transform-duplicating-tokens`: a golden compares the text, never its colour. */
1262
+ function stripAnsiCodes(value) {
1263
+ return value.replace(ANSI_RE, "");
1264
+ }
1265
+ //#endregion
1266
+ //#region src/expectations/text/grep.ts
1267
+ function grep(output, pattern) {
1268
+ return stripAnsiCodes(output).split(/\n\s*\n/u).filter((block) => block.includes(pattern)).join("\n\n");
1269
+ }
1270
+ //#endregion
1271
+ //#region src/expectations/text/text.ts
1272
+ /** The captured text accessor: `.text` is what a golden compares (ANSI stripped, `transform` applied), `.raw` the capture. */
1273
+ var TextAccessor = class TextAccessor {
1274
+ /** @internal */
1275
+ captures;
1276
+ /** The stream as it was captured: never transformed, ANSI preserved. */
1277
+ raw;
1278
+ /** @internal */
1279
+ streamName;
1280
+ /** @internal */
1281
+ testDir;
1282
+ /** Normaliser applied before comparisons, never to fixtures. @internal */
1283
+ transform;
1284
+ constructor(value, streamName, testDir, options = {}) {
1285
+ this.raw = value;
1286
+ this.streamName = streamName;
1287
+ this.testDir = testDir;
1288
+ this.transform = options.transform;
1289
+ this.captures = options.captures ?? new CaptureScope();
1290
+ }
1291
+ /** The text a golden compares, and a value matcher reads: ANSI stripped, `transform` applied. */
1292
+ get text() {
1293
+ const stripped = stripAnsiCodes(this.raw);
1294
+ return this.transform ? this.transform(stripped) : stripped;
1295
+ }
1296
+ /** Keep only the blank-line-separated blocks containing `pattern`, as a new `TextAccessor`. */
1297
+ grep(pattern) {
1298
+ return new TextAccessor(grep(this.raw, pattern), this.streamName, this.testDir, {
1299
+ captures: this.captures,
1300
+ transform: this.transform
1301
+ });
1302
+ }
1303
+ toString() {
1304
+ return this.text;
1305
+ }
1306
+ valueOf() {
1307
+ return this.text;
1308
+ }
1309
+ };
1310
+ //#endregion
1311
+ //#region src/specifications/component/render/rendered-text.ts
1312
+ /** A stream captured inside the page: its golden lives on the server, so `toMatch` is awaited. */
1313
+ var RenderedText = class extends TextAccessor {
1314
+ /** The marker the `toMatch` overload reads. Never asserted on directly. */
1315
+ capturedInPage = true;
1316
+ };
1317
+ //#endregion
1318
+ //#region src/specifications/component/render/render.result.ts
1319
+ /** What a render resolves to: the component as the page saw it after the scenario. */
1320
+ var RenderResult = class {
1321
+ /** The browser console as a stream: one `[type] text` line per message. */
1322
+ console;
1323
+ /** Rendered text of the whole document body (`innerText`). */
1324
+ content;
1325
+ /** Console messages of type `error` only, plus any uncaught page error. */
1326
+ errors;
1327
+ /** The mounted markup, serialised: the escape hatch for a class or an attribute. */
1328
+ html;
1329
+ /** The ARIA snapshot of the rendered body, the outline a golden pins. */
1330
+ tree;
1331
+ /** Rendered text of the body without the text of an element drawn fully transparent: what a reader sees of an animation, where `content` keeps a node at `opacity: 0`. */
1332
+ visibleContent;
1333
+ constructor(streams) {
1334
+ this.console = new RenderedText(streams.console, "console", "");
1335
+ this.content = new RenderedText(streams.content, "content", "");
1336
+ this.errors = new RenderedText(streams.errors, "errors", "");
1337
+ this.html = new RenderedText(streams.html, "html", "");
1338
+ this.tree = new RenderedText(streams.tree, "tree", "");
1339
+ this.visibleContent = new RenderedText(streams.visibleContent, "visible-content", "");
1340
+ }
1341
+ };
1342
+ //#endregion
1343
+ //#region src/specifications/component/render/visible-text.ts
1344
+ /** The rendered text of the body without the text of an element the page draws fully transparent: `innerText` keeps a node at `opacity: 0`, which a reader does not see. */
1345
+ function visibleTextOf({ elements, opacityOf, text }) {
1346
+ const hidden = elements.filter((element) => opacityOf(element) === 0).map((element) => ({
1347
+ element,
1348
+ visibility: element.style.visibility
1349
+ }));
1350
+ for (const { element } of hidden) element.style.visibility = "hidden";
1351
+ try {
1352
+ return text();
1353
+ } finally {
1354
+ for (const { element, visibility } of hidden) element.style.visibility = visibility;
1355
+ }
1356
+ }
1357
+ //#endregion
1358
+ //#region src/specifications/component/render/wrap.capability.ts
1359
+ /** A later call replaces an earlier one: the wrappers do not nest. */
1360
+ const wrap = defineCapability({
1361
+ initial: null,
1362
+ name: "wrap",
1363
+ phase: 30,
1364
+ reduce: (_stated, [wrapper]) => wrapper,
1365
+ row: {
1366
+ group: "Setup",
1367
+ name: ".wrap()",
1368
+ probe: ".wrap("
1369
+ }
1370
+ });
1371
+ //#endregion
1372
+ //#region src/specifications/component/render/render.action.ts
1373
+ const PARENT_VERBS = [{
1374
+ group: "Verb",
1375
+ name: "rerender",
1376
+ probe: ".rerender("
1377
+ }, {
1378
+ group: "Verb",
1379
+ name: "unmount",
1380
+ probe: ".unmount("
1381
+ }];
1382
+ let mounted = null;
1383
+ async function unmountHeld() {
1384
+ const surface = mounted;
1385
+ mounted = null;
1386
+ if (surface !== null) try {
1387
+ await surface.unmount();
1388
+ } catch {}
1389
+ }
1390
+ function isDomMount(subject) {
1391
+ return typeof subject === "function";
1392
+ }
1393
+ async function mount(page, chained, subject) {
1394
+ if (isDomMount(subject)) return await page.mountDom(subject);
1395
+ const outer = page.projectWrapper();
1396
+ const dress = (ui) => {
1397
+ const inner = chained ? chained(ui) : ui;
1398
+ return outer ? outer(inner) : inner;
1399
+ };
1400
+ const surface = await page.mountReact(dress(subject));
1401
+ return {
1402
+ ...surface,
1403
+ rerender: async (ui) => {
1404
+ await surface.rerender(dress(ui));
1405
+ }
1406
+ };
1407
+ }
1408
+ /** A second render replaces the first, taken down before the second arms anything. */
1409
+ const render = defineAction({
1410
+ accessors: [
1411
+ {
1412
+ group: "Result accessor",
1413
+ name: ".tree",
1414
+ probe: "result.tree"
1415
+ },
1416
+ {
1417
+ group: "Result accessor",
1418
+ name: ".content",
1419
+ probe: "result.content"
1420
+ },
1421
+ {
1422
+ group: "Result accessor",
1423
+ name: ".visibleContent",
1424
+ probe: "result.visibleContent"
1425
+ },
1426
+ {
1427
+ group: "Result accessor",
1428
+ name: ".console",
1429
+ probe: "result.console"
1430
+ },
1431
+ {
1432
+ group: "Result accessor",
1433
+ name: ".errors",
1434
+ probe: "result.errors"
1435
+ },
1436
+ {
1437
+ group: "Result accessor",
1438
+ name: ".html",
1439
+ probe: "result.html"
1440
+ }
1441
+ ],
1442
+ targets: [
1443
+ ...EVERY_SURFACE_TARGETS,
1444
+ ...BROWSER_TARGETS,
1445
+ WITHIN,
1446
+ ...BROWSER_STATES
1447
+ ],
1448
+ name: "render",
1449
+ perform: async ([subject, scenario], read, { page }) => {
1450
+ const recorder = recordConsole();
1451
+ page.afterThisTest(async () => {
1452
+ recorder.stop();
1453
+ await unmountHeld();
1454
+ });
1455
+ const surface = await mount(page, read(wrap), subject);
1456
+ mounted = surface;
1457
+ try {
1458
+ if (scenario) await scenario(await page.visitor(surface));
1459
+ } finally {
1460
+ recorder.stop();
1461
+ }
1462
+ const { entries } = recorder;
1463
+ return new RenderResult({
1464
+ console: consoleOf(entries),
1465
+ content: document.body.innerText,
1466
+ errors: errorsOf(entries),
1467
+ html: surface.container.innerHTML,
1468
+ tree: await page.ariaTree(),
1469
+ visibleContent: visibleTextOf({
1470
+ elements: [...document.body.querySelectorAll("*")],
1471
+ opacityOf: (element) => Number(getComputedStyle(element).opacity),
1472
+ text: () => document.body.innerText
1473
+ })
1474
+ });
1475
+ },
1476
+ prepare: async () => {
1477
+ await unmountHeld();
1478
+ return { workDir: null };
1479
+ },
1480
+ row: {
1481
+ group: "Terminal action",
1482
+ name: ".render()",
1483
+ probe: ".render("
1484
+ },
1485
+ verbs: [
1486
+ ...EVERY_SURFACE_VERBS,
1487
+ ...BROWSER_VERBS,
1488
+ ...PARENT_VERBS
1489
+ ]
1490
+ });
1491
+ //#endregion
1492
+ //#region src/specifications/component/viewport/page-viewport.adapter.ts
1493
+ /** Restored to the project's size when the test ends, so a narrow render never leaks. */
1494
+ const pageViewport = { size: async (size) => {
1495
+ await setViewport(size);
1496
+ afterThisTest(async () => {
1497
+ await setViewport(providedViewport());
1498
+ });
1499
+ return {};
1500
+ } };
1501
+ //#endregion
1502
+ //#region src/specifications/component/viewport/viewport.capability.ts
1503
+ /** A chain stating none leaves the page at the project's size, unresized. */
1504
+ const viewport = defineCapability({
1505
+ arm: async (size, adapters) => size === null ? void 0 : await adapters.viewport.size(size),
1506
+ initial: null,
1507
+ name: "viewport",
1508
+ phase: 30,
1509
+ reduce: (_stated, [size]) => size,
1510
+ row: {
1511
+ group: "Setup",
1512
+ name: ".viewport()",
1513
+ probe: ".viewport("
1514
+ }
1515
+ });
1516
+ //#endregion
1517
+ //#region src/specifications/component/component.composition.ts
1518
+ const composition = {
1519
+ actions: [render],
1520
+ capabilities: [
1521
+ clock$1,
1522
+ intercept$1,
1523
+ viewport,
1524
+ wrap
1525
+ ],
1526
+ options: {
1527
+ clock: {
1528
+ group: "Constructor option",
1529
+ name: "clock",
1530
+ probe: "clock:"
1531
+ },
1532
+ locale: {
1533
+ group: "Constructor option",
1534
+ name: "locale",
1535
+ probe: "locale:"
1536
+ },
1537
+ root: {
1538
+ group: "Constructor option",
1539
+ name: "root",
1540
+ probe: "root:"
1541
+ },
1542
+ timezone: {
1543
+ group: "Constructor option",
1544
+ name: "timezone",
1545
+ probe: "timezone:"
1546
+ },
1547
+ viewport: {
1548
+ group: "Constructor option",
1549
+ name: "viewport",
1550
+ probe: "viewport:"
1551
+ },
1552
+ vite: {
1553
+ group: "Constructor option",
1554
+ name: "vite",
1555
+ probe: "vite:"
1556
+ },
1557
+ wrap: {
1558
+ group: "Constructor option",
1559
+ name: "wrap",
1560
+ probe: "wrap:"
1561
+ }
1562
+ }
1563
+ };
1564
+ const componentDefinition = defineSpecification()({
1565
+ ...composition,
1566
+ adapters: (_config) => ({
1567
+ clock: pageClock,
1568
+ network: mswWorker(afterThisTest),
1569
+ page: vitestBrowserPage,
1570
+ viewport: pageViewport
1571
+ }),
1572
+ locate: pageDirectory
1573
+ });
1574
+ //#endregion
1575
+ //#region lint/kit/project/dedent.ts
1576
+ function indentOf(line) {
1577
+ return /^[\t ]*/u.exec(line)?.[0].length ?? 0;
1578
+ }
1579
+ /** A text opening on a line break loses that break, the closing line's whitespace and the shared indentation. */
1580
+ function dedent(text) {
1581
+ if (!text.startsWith("\n")) return text;
1582
+ const lines = text.slice(1).split("\n");
1583
+ if (lines.at(-1)?.trim() === "") lines[lines.length - 1] = "";
1584
+ const indents = lines.filter((line) => line.trim() !== "").map((line) => indentOf(line));
1585
+ const common = indents.length === 0 ? 0 : Math.min(...indents);
1586
+ return lines.map((line) => line.slice(common)).join("\n");
1587
+ }
1588
+ //#endregion
1589
+ //#region lint/kit/project/relative-path.ts
1590
+ function isRelativePath(value) {
1591
+ return value.startsWith("./") || value.startsWith("../");
1592
+ }
1593
+ const DISK = Symbol.for("@jterrazz/test:disk");
1594
+ /** Name one file, by its path from the spec file. */
1595
+ function file(path) {
1596
+ requireRelativePath("file", path);
1597
+ if (path.endsWith("/")) {
1598
+ const bare = path.replace(/\/+$/u, "");
1599
+ throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
1600
+ fix: `name the directory with directory(${JSON.stringify(path)}), or the file without its /: file(${JSON.stringify(bare)})`,
1601
+ found: `file(${JSON.stringify(path)}), a path ending in /, which is a directory's`
1602
+ }));
1603
+ }
1604
+ return diskValue("file", path);
1605
+ }
1606
+ function diskValue(brand, path) {
1607
+ const value = {
1608
+ [DISK]: brand,
1609
+ path
1610
+ };
1611
+ Object.defineProperty(value, Symbol.match, { value: () => {
1612
+ throw new TypeError(refusal("one-verb-per-meaning", {
1613
+ fix: `wrap the string to golden it, \`expect(text(value)).toMatch(${brand}(...))\`, or compare it to a value, \`toMatch('...')\``,
1614
+ found: `toMatch(${brand}(${JSON.stringify(path)})) on a string, a value with no golden`
1615
+ }));
1616
+ } });
1617
+ return Object.freeze(value);
1618
+ }
1619
+ /** Name one directory, by its path from the spec file, ending in `/`. */
1620
+ function directory(path) {
1621
+ requireRelativePath("directory", path);
1622
+ if (!path.endsWith("/")) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
1623
+ fix: `end it in /: directory(${JSON.stringify(`${path}/`)}), or name a file with file(${JSON.stringify(path)})`,
1624
+ found: `directory(${JSON.stringify(path)}), a path that does not end in /`
1625
+ }));
1626
+ return diskValue("directory", path);
1627
+ }
1628
+ function isDiskFile(value) {
1629
+ return brandOf(value) === "file";
1630
+ }
1631
+ function isDiskDirectory(value) {
1632
+ return brandOf(value) === "directory";
1633
+ }
1634
+ function isDiskTarget(value) {
1635
+ return brandOf(value) !== void 0;
1636
+ }
1637
+ function brandOf(value) {
1638
+ if (value === null || typeof value !== "object") return;
1639
+ const brand = Reflect.get(value, DISK);
1640
+ return brand === "file" || brand === "directory" ? brand : void 0;
1641
+ }
1642
+ function asRelative(path) {
1643
+ const bare = typeof path === "string" ? path.replace(/^\/+/u, "") : "";
1644
+ return `./${bare === "" ? "__expected__/output.txt" : bare}`;
1645
+ }
1646
+ function requireRelativePath(call, path) {
1647
+ if (typeof path !== "string" || !isRelativePath(path)) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
1648
+ fix: `start it with ./ or ../, from the spec file: ${call}('${asRelative(path)}')`,
1649
+ found: `${call}(${JSON.stringify(path)}), a path that is not relative to the spec file`
1650
+ }));
1651
+ }
1652
+ //#endregion
1653
+ //#region src/expectations/goldens/diff.ts
1654
+ const GREEN = "\x1B[32m";
1655
+ const RED = "\x1B[31m";
1656
+ const RESET = "\x1B[0m";
1657
+ const strictEquals = (expected, actual) => expected === actual;
1658
+ function renderLines(expectedLines, actualLines, equals) {
1659
+ const rendered = [];
1660
+ let marked = false;
1661
+ for (let i = 0; i < Math.max(expectedLines.length, actualLines.length); i++) {
1662
+ const exp = expectedLines[i];
1663
+ const act = actualLines[i];
1664
+ if (exp !== void 0 && act !== void 0 && equals(exp, act)) {
1665
+ rendered.push(` ${exp}`);
1666
+ continue;
1667
+ }
1668
+ marked = true;
1669
+ rendered.push(...exp === void 0 ? [] : [`${GREEN}- ${exp}${RESET}`], ...act === void 0 ? [] : [`${RED}+ ${act}${RESET}`]);
1670
+ }
1671
+ return {
1672
+ marked,
1673
+ rendered
1674
+ };
1675
+ }
1676
+ /** `equals` is token-aware, so a matched `{{url}}` line shows as equal; if it marks nothing, the strict rendering is used. */
1677
+ function formatStdoutDiff(file, expected, actual, options) {
1678
+ const lines = [
1679
+ `Output mismatch (${file})`,
1680
+ "",
1681
+ `${GREEN}- Expected${RESET}`,
1682
+ `${RED}+ Received${RESET}`,
1683
+ ""
1684
+ ];
1685
+ const expectedLines = expected.split("\n");
1686
+ const actualLines = actual.split("\n");
1687
+ const first = renderLines(expectedLines, actualLines, options?.equals ?? strictEquals);
1688
+ const rendered = first.marked ? first.rendered : renderLines(expectedLines, actualLines, strictEquals).rendered;
1689
+ return [...lines, ...rendered].join("\n");
1690
+ }
1691
+ //#endregion
1692
+ //#region src/expectations/goldens/text-assertions.ts
1693
+ /** The golden's path carries its extension: the name IS the file. */
1694
+ function requireExtension(name, subject) {
1695
+ if (!/\.[A-Za-z0-9]+$/u.test(name)) throw new Error(refusal({
1696
+ fix: `name the file with its extension: file('./__expected__/help.txt')`,
1697
+ found: `toMatch(file("${name}")) on ${subject}, a golden path with no extension`,
1698
+ read: "docs/reference/assertions.md#one-verb-compares-a-whole-result-and-its-argument-says-where-the-expectation-lives",
1699
+ why: "The extension is part of a golden's path."
1700
+ }));
1701
+ }
1702
+ /** The diff judges each line through the same token grammar, so a matched token line shows as equal. */
1703
+ function compareStreamText(accessor, name, expected) {
1704
+ const actual = accessor.text;
1705
+ if (textEquals(expected, actual, accessor.captures)) return {
1706
+ message: () => `expected ${accessor.streamName} not to match ${name}`,
1707
+ pass: true
1708
+ };
1709
+ return {
1710
+ message: () => formatStdoutDiff(name, expected, actual, { equals: (expectedLine, actualLine) => textEquals(expectedLine, actualLine, new CaptureScope(accessor.captures.workdir)) }),
1711
+ pass: false
1712
+ };
1713
+ }
1714
+ /** An empty stream is an empty text: `expect(stream.text).toBe('')`. */
1715
+ function emptyStreamRefusal(accessor) {
1716
+ return new TypeError(refusal("one-verb-per-meaning", {
1717
+ fix: `compare its text: expect(<result>.${accessor.streamName}.text).toBe('')`,
1718
+ found: `toBeEmpty() on the stream "${accessor.streamName}": an empty stream is an empty text`
1719
+ }));
1720
+ }
1721
+ const IN_PLACE = "the value in place";
1722
+ /** A stream is compared whole to a text in place; a RegExp matches a part, which is vitest's, on `.text`. */
1723
+ function streamValueRoute(subject, value) {
1724
+ if (!(subject instanceof TextAccessor)) return;
1725
+ return typeof value === "string" ? "whole" : new TypeError(refusal("one-verb-per-meaning", {
1726
+ fix: `write the whole text in place, toMatch('...'), or match a part with vitest on its text: expect(<result>.${subject.streamName}.text).toMatch(...)`,
1727
+ found: `toMatch(a ${value instanceof RegExp ? "RegExp" : typeof value}) on the stream "${subject.streamName}"`
1728
+ }));
1729
+ }
1730
+ function compareStreamInPlace(accessor, expected) {
1731
+ return compareStreamText(accessor, IN_PLACE, dedent(expected));
1732
+ }
1733
+ /** The name the golden comparison is registered under; no member access can spell it. */
1734
+ const GOLDEN_MATCHER = "@jterrazz/test:golden";
1735
+ const WHOLE_MATCHER = "@jterrazz/test:whole";
1736
+ function optionsOf(value) {
1737
+ return value !== null && typeof value === "object" ? {
1738
+ frozen: Reflect.get(value, "frozen") === true,
1739
+ ordered: Reflect.get(value, "ordered") === true
1740
+ } : void 0;
1741
+ }
1742
+ function directoryOf(location) {
1743
+ const normalized = location.replace(/^file:\/\//u, "").replace(/[?#].*$/u, "");
1744
+ return normalized.slice(0, normalized.lastIndexOf("/"));
1745
+ }
1746
+ const OWN_DIRECTORY = directoryOf(import.meta.url);
1747
+ const FRAME_LINE = /^\s*at\s/u;
1748
+ const FRAME_FILE = /at\s+(?:async\s+)?(?:.*?\s+)?\(?(?<file>(?:file:\/\/)?\/[^():]+|https?:\/\/.+?):\d+:\d+\)?\s*$/u;
1749
+ function frameDirectoryOf(line) {
1750
+ const file = FRAME_FILE.exec(line)?.groups?.file;
1751
+ return file === void 0 ? void 0 : directoryOf(file);
1752
+ }
1753
+ /** Every frame under `directory` is cut, not a leading run: vitest's plumbing sits above this router on the stack. */
1754
+ function cutFramesUnder(stack, directory) {
1755
+ return stack.split("\n").filter((line) => !FRAME_LINE.test(line) || frameDirectoryOf(line) !== directory).join("\n");
1756
+ }
1757
+ function rethrowCut(error) {
1758
+ if (error instanceof Error && error.stack !== void 0) error.stack = cutFramesUnder(error.stack, OWN_DIRECTORY);
1759
+ throw error;
1760
+ }
1761
+ async function awaitedCut(result) {
1762
+ try {
1763
+ return await result;
1764
+ } catch (error) {
1765
+ return rethrowCut(error);
1766
+ }
1767
+ }
1768
+ function routedThrough(route) {
1769
+ let result;
1770
+ try {
1771
+ result = route();
1772
+ } catch (error) {
1773
+ return rethrowCut(error);
1774
+ }
1775
+ return result instanceof Promise ? awaitedCut(result) : result;
1776
+ }
1777
+ /** `file()`/`directory()` go to the golden, a value handed to a framework subject to the whole comparison, everything else to vitest untouched. */
1778
+ function matchThroughGolden(chai, routeValue) {
1779
+ chai.util.overwriteMethod(chai.Assertion.prototype, "toMatch", (vitestToMatch) => function toMatch(...args) {
1780
+ const golden = this[GOLDEN_MATCHER];
1781
+ if (isDiskTarget(args[0]) && typeof golden === "function") return routedThrough(() => golden.apply(this, args));
1782
+ const route = routeValue(chai.util.flag(this, "object"), args[0]);
1783
+ if (route instanceof TypeError) throw route;
1784
+ const whole = this[WHOLE_MATCHER];
1785
+ if (route === "whole" && typeof whole === "function") {
1786
+ const stack = (/* @__PURE__ */ new Error("toMatch call site")).stack ?? "";
1787
+ const call = {
1788
+ options: optionsOf(args[1]),
1789
+ stack
1790
+ };
1791
+ return routedThrough(() => whole.call(this, args[0], call));
1792
+ }
1793
+ return vitestToMatch.apply(this, args);
1794
+ });
1795
+ }
1796
+ //#endregion
1797
+ //#region src/expectations/in-place/update.ts
1798
+ const UPDATE_HINT = "Run with TEST_UPDATE=1 (or vitest -u) to create it.";
1799
+ const FROZEN_HINT = "This golden is `{ frozen }`: update mode never writes it. Write the file by hand, or drop `{ frozen }` for one TEST_UPDATE=1 run and put it back.";
1800
+ function missingHint(frozen) {
1801
+ return frozen ? FROZEN_HINT : UPDATE_HINT;
1802
+ }
1803
+ //#endregion
1804
+ //#region src/expectations/tokens/update-merge.ts
1805
+ const ANY_LINE = /^\s*\{\{any\}\}\s*$/u;
1806
+ function anchorOf(lines, from, step) {
1807
+ const side = step === 1 ? lines.slice(from + 1) : lines.slice(0, from).toReversed();
1808
+ const at = side.findIndex((line) => line.trim() !== "" && !hasPlaceholders(line));
1809
+ return at === -1 ? void 0 : {
1810
+ line: String(side[at]),
1811
+ offset: step * (at + 1)
1812
+ };
1813
+ }
1814
+ const PREFIX = 20;
1815
+ function indexOfAnchor(lines, anchor, from) {
1816
+ const exact = lines.indexOf(anchor, from);
1817
+ if (exact !== -1 || anchor.length <= PREFIX) return exact;
1818
+ const prefix = anchor.slice(0, PREFIX);
1819
+ return lines.findIndex((line, index) => index >= from && line.startsWith(prefix));
1820
+ }
1821
+ function anyRuns(prevLines, actualLines) {
1822
+ const runs = /* @__PURE__ */ new Map();
1823
+ let cursor = 0;
1824
+ for (const [index, line] of prevLines.entries()) {
1825
+ if (!ANY_LINE.test(line)) continue;
1826
+ const before = anchorOf(prevLines, index, -1);
1827
+ const after = anchorOf(prevLines, index, 1);
1828
+ let start = 0;
1829
+ if (before !== void 0) {
1830
+ const found = indexOfAnchor(actualLines, before.line, cursor);
1831
+ if (found === -1) continue;
1832
+ start = found - before.offset;
1833
+ }
1834
+ let end = actualLines.length;
1835
+ if (after !== void 0) end = indexOfAnchor(actualLines, after.line, start) - after.offset + 1;
1836
+ if (end > start) {
1837
+ runs.set(start, {
1838
+ end,
1839
+ line
1840
+ });
1841
+ cursor = end;
1842
+ }
1843
+ }
1844
+ return runs;
1845
+ }
1846
+ function withAnyRuns(previous, actual, merged) {
1847
+ const runs = anyRuns(previous.split("\n"), actual.split("\n"));
1848
+ const lines = merged.split("\n");
1849
+ const kept = [];
1850
+ for (let index = 0; index < lines.length; index += 1) {
1851
+ const run = runs.get(index);
1852
+ if (run === void 0) {
1853
+ kept.push(String(lines[index]));
1854
+ continue;
1855
+ }
1856
+ kept.push(run.line);
1857
+ index = run.end - 1;
1858
+ }
1859
+ return kept.join("\n");
1860
+ }
1861
+ function mergeTextPreservingPlaceholders(previous, actual, scope) {
1862
+ const merged = mergedLines(previous, actual, scope);
1863
+ return previous === null ? merged : withAnyRuns(previous, actual, merged);
1864
+ }
1865
+ function mergedLines(previous, actual, scope) {
1866
+ const substituted = scope.workdir === void 0 ? actual : actual.replaceAll(scope.workdir, "{{workdir}}");
1867
+ if (previous === null) return substituted;
1868
+ const prevLines = previous.split("\n");
1869
+ const actualLines = substituted.split("\n");
1870
+ const rawLines = actual.split("\n");
1871
+ const consumed = /* @__PURE__ */ new Set();
1872
+ const covers = (prevIndex, actualIndex) => {
1873
+ const prev = String(prevLines[prevIndex]);
1874
+ return hasPlaceholders(prev) && (textEquals(prev, String(actualLines[actualIndex]), new CaptureScope(scope.workdir)) || textEquals(prev, String(rawLines[actualIndex]), new CaptureScope(scope.workdir)));
1875
+ };
1876
+ const merged = actualLines.map(() => void 0);
1877
+ for (const [index] of actualLines.entries()) if (covers(index, index)) {
1878
+ merged[index] = prevLines[index];
1879
+ consumed.add(index);
1880
+ }
1881
+ for (const [index, line] of actualLines.entries()) {
1882
+ if (merged[index] !== void 0) continue;
1883
+ for (const [prevIndex] of prevLines.entries()) {
1884
+ if (consumed.has(prevIndex) || !covers(prevIndex, index)) continue;
1885
+ merged[index] = prevLines[prevIndex];
1886
+ consumed.add(prevIndex);
1887
+ break;
1888
+ }
1889
+ merged[index] ??= line;
1890
+ }
1891
+ return merged.join("\n");
1892
+ }
1893
+ //#endregion
1894
+ //#region src/visitor/targets/as-written.ts
1895
+ const FUNCTION_NAME_OF_KIND = {
1896
+ banner: "banner",
1897
+ button: "button",
1898
+ complementary: "complementary",
1899
+ contentinfo: "contentinfo",
1900
+ dialog: "dialog",
1901
+ field: "field",
1902
+ form: "form",
1903
+ heading: "heading",
1904
+ link: "link",
1905
+ listitem: "listitem",
1906
+ main: "main",
1907
+ navigation: "navigation",
1908
+ option: "option",
1909
+ region: "region",
1910
+ row: "row",
1911
+ search: "search",
1912
+ status: "status",
1913
+ table: "table",
1914
+ testId: "testId",
1915
+ text: "content"
1916
+ };
1917
+ /** Spoken in the vocabulary the author wrote, never playwright's: an option appears only when the author stated one. */
1918
+ function asWritten(target) {
1919
+ const bare = asWrittenUnscoped(target);
1920
+ return target.scope ? `within(${asWritten(target.scope)}, ${bare})` : bare;
1921
+ }
1922
+ function asWrittenUnscoped(target) {
1923
+ const args = [];
1924
+ if (target.name !== void 0) args.push(JSON.stringify(target.name));
1925
+ if (target.exact !== void 0) args.push(`{ exact: ${String(target.exact)} }`);
1926
+ return `${FUNCTION_NAME_OF_KIND[target.kind]}(${args.join(", ")})`;
1927
+ }
1928
+ //#endregion
1929
+ //#region src/visitor/matching/landmark-of-tag.ts
1930
+ const LANDMARK_ELEMENTS = {
1931
+ banner: "header",
1932
+ complementary: "aside",
1933
+ contentinfo: "footer",
1934
+ form: "form",
1935
+ main: "main",
1936
+ navigation: "nav",
1937
+ region: "section",
1938
+ search: "search"
1939
+ };
1940
+ const LANDMARK_SELECTOR = Object.entries(LANDMARK_ELEMENTS).flatMap(([kind, tag]) => [tag, `[role="${kind}"]`]).join(", ");
1941
+ function isLandmarkKind(value) {
1942
+ return Object.hasOwn(LANDMARK_ELEMENTS, value);
1943
+ }
1944
+ function landmarkOfTagOrRole(value) {
1945
+ const kind = Object.entries(LANDMARK_ELEMENTS).find(([, tag]) => tag === value)?.[0] ?? value;
1946
+ return isLandmarkKind(kind) ? kind : void 0;
1947
+ }
1948
+ //#endregion
1949
+ //#region src/visitor/matching/ambiguous.ts
1950
+ function landmarkOf(match) {
1951
+ const kind = landmarkOfTagOrRole(match.context ?? "");
1952
+ if (kind !== "region") return kind === void 0 ? void 0 : `${FUNCTION_NAME_OF_KIND[kind]}()`;
1953
+ return match.contextName === void 0 ? void 0 : `${FUNCTION_NAME_OF_KIND.region}(${JSON.stringify(match.contextName)})`;
1954
+ }
1955
+ function formatMatch(match, index) {
1956
+ const attribute = match.detail === void 0 ? "" : ` ${quoteDetail(match)}`;
1957
+ const matchedOnLabel = match.accessibleName === void 0 || match.accessibleName === "" ? "" : ` named ${JSON.stringify(match.accessibleName)}`;
1958
+ const context = match.context === void 0 || match.context === "" ? "" : ` in <${match.context}>`;
1959
+ return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${matchedOnLabel}${context}`;
1960
+ }
1961
+ function quoteDetail(match) {
1962
+ return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
1963
+ }
1964
+ function landmarkCounts(matches) {
1965
+ const counts = /* @__PURE__ */ new Map();
1966
+ for (const match of matches) {
1967
+ const landmark = landmarkOf(match);
1968
+ if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
1969
+ }
1970
+ return [...counts].toSorted(([, a], [, b]) => a - b);
1971
+ }
1972
+ function formatFixes(target, matches) {
1973
+ const fixes = [];
1974
+ const landmarksThatCutTheSet = landmarkCounts(matches).filter(([, count]) => count < matches.length);
1975
+ if (!target.scope) {
1976
+ const [first, ...rest] = landmarksThatCutTheSet;
1977
+ const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
1978
+ fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${asWrittenUnscoped(target)})` : `scope it within(${first[0]}, ${asWrittenUnscoped(target)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
1979
+ }
1980
+ if (target.exact === false) {
1981
+ const remaining = matches.filter((match) => match.text === target.name).length;
1982
+ if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${asWrittenUnscoped({
1983
+ ...target,
1984
+ exact: true
1985
+ })} [leaves ${remaining} of ${matches.length}]`);
1986
+ }
1987
+ fixes.push("other element a heading(), button() or field() may name one thing where this does not");
1988
+ return fixes;
1989
+ }
1990
+ function ambiguityError(options) {
1991
+ const { target, fixes, lines, where } = options;
1992
+ return new Error(refusal("target-matches-one-element", {
1993
+ fix: ["designate one of them:", ...fixes.map((fix) => ` • ${fix}`)].join("\n"),
1994
+ found: [`Ambiguous element: ${asWritten(target)} matched ${lines.length} elements on ${where}:`, ...lines].join("\n")
1995
+ }));
1996
+ }
1997
+ function ambiguousTargetError(options) {
1998
+ const { target, matches, url } = options;
1999
+ return ambiguityError({
2000
+ target,
2001
+ fixes: formatFixes(target, matches),
2002
+ lines: matches.map((match, index) => formatMatch(match, index)),
2003
+ where: url
2004
+ });
2005
+ }
2006
+ const CANDIDATE_LIMITS = {
2007
+ landmarks: LANDMARK_SELECTOR,
2008
+ limit: 10,
2009
+ textLength: 80
2010
+ };
2011
+ /** Serialized into the page by playwright: it reads nothing from module scope. */
2012
+ function candidatesOf(nodes, { landmarks, limit, textLength }) {
2013
+ const landmarkOf = (node) => {
2014
+ const landmark = node.parentElement?.closest(landmarks);
2015
+ if (landmark === null || landmark === void 0) return {
2016
+ context: void 0,
2017
+ contextName: void 0
2018
+ };
2019
+ const labelledBy = (landmark.getAttribute("aria-labelledby") ?? "").split(/\s+/u).filter((id) => id !== "").map((id) => document.querySelector(`#${CSS.escape(id)}`)?.textContent ?? "").join(" ");
2020
+ const name = (landmark.getAttribute("aria-label") ?? labelledBy).replaceAll(/\s+/gu, " ").trim();
2021
+ return {
2022
+ context: landmark.getAttribute("role") ?? landmark.tagName.toLowerCase(),
2023
+ contextName: name === "" ? void 0 : name
2024
+ };
2025
+ };
2026
+ return nodes.slice(0, limit).map((node) => {
2027
+ const detail = node.getAttribute("href") ?? node.getAttribute("name");
2028
+ const text = (node.textContent ?? "").replaceAll(/\s+/gu, " ").trim().slice(0, textLength);
2029
+ const label = node.getAttribute("aria-label")?.trim();
2030
+ const { context, contextName } = landmarkOf(node);
2031
+ return {
2032
+ accessibleName: label === void 0 || label === "" || label === text ? void 0 : label,
2033
+ context,
2034
+ contextName,
2035
+ detail: detail ?? void 0,
2036
+ tag: node.tagName.toLowerCase(),
2037
+ text
2038
+ };
2039
+ });
2040
+ }
2041
+ //#endregion
2042
+ //#region src/visitor/targets/roles.ts
2043
+ const stated = (options) => options?.exact === void 0 ? {} : { exact: options.exact };
2044
+ const named = (kind) => (name, options) => ({
2045
+ kind,
2046
+ name,
2047
+ ...stated(options)
2048
+ });
2049
+ const optionallyNamed = (kind) => (name, options) => ({
2050
+ kind,
2051
+ ...name === void 0 ? {} : { name },
2052
+ ...stated(options)
2053
+ });
2054
+ /** A button (or element with the button role), by accessible name. */
2055
+ const button = named("button");
2056
+ /** A form field, by label. */
2057
+ const field = named("field");
2058
+ /** A heading, by accessible name. */
2059
+ const heading = named("heading");
2060
+ /** A link, by accessible name. */
2061
+ const link = named("link");
2062
+ /** An element containing the given text. */
2063
+ const content = named("text");
2064
+ /** An option of a select or listbox, by label; asserted by presence, since a collapsed select's options have no box. */
2065
+ const option = named("option");
2066
+ /** A modal or non-modal `<dialog>`, by accessible name. A closed one is absent. */
2067
+ const dialog = optionallyNamed("dialog");
2068
+ /** A live region announcing a result: `role="status"`, an `<output>`. */
2069
+ const status = optionallyNamed("status");
2070
+ /** A table, by its caption or accessible name. */
2071
+ const table = optionallyNamed("table");
2072
+ /** A row of a table or grid, by the text of its cells. */
2073
+ const row = optionallyNamed("row");
2074
+ /** An item of a list, by its text. */
2075
+ const listitem = optionallyNamed("listitem");
2076
+ /** The escape hatch: an element by `data-testid`. Prefer user-facing targets. */
2077
+ const testId = (id) => ({
2078
+ kind: "testId",
2079
+ name: id
2080
+ });
2081
+ //#endregion
2082
+ //#region src/visitor/targets/landmarks.ts
2083
+ /** The `banner` landmark: the page header. */
2084
+ const banner = optionallyNamed("banner");
2085
+ /** The `complementary` landmark: an `<aside>`, a sidebar. */
2086
+ const complementary = optionallyNamed("complementary");
2087
+ /** The `contentinfo` landmark: the page footer. */
2088
+ const contentinfo = optionallyNamed("contentinfo");
2089
+ /** The `form` landmark: a form carrying an accessible name. */
2090
+ const form = optionallyNamed("form");
2091
+ /** The `main` landmark: the primary content of the document. */
2092
+ const main = optionallyNamed("main");
2093
+ /** The `navigation` landmark: a `<nav>`. Name it when a page has several. */
2094
+ const navigation = optionallyNamed("navigation");
2095
+ /** The `region` landmark: a `<section>` carrying an accessible name. */
2096
+ const region = optionallyNamed("region");
2097
+ /** The `search` landmark. */
2098
+ const search = optionallyNamed("search");
2099
+ /** Restrict a target to the inside of another: `click(within(navigation(), link('Articles')))`. */
2100
+ const within = (scope, target) => ({
2101
+ ...target,
2102
+ scope: target.scope ? within(scope, target.scope) : scope
2103
+ });
2104
+ function scopeChain(target) {
2105
+ const chain = [];
2106
+ for (let current = target; current; current = current.scope) chain.unshift(current);
2107
+ return chain;
2108
+ }
2109
+ //#endregion
2110
+ //#region src/specifications/component/page/vitest-browser-dom.ts
2111
+ var vitest_browser_dom_exports = /* @__PURE__ */ __exportAll({
2112
+ act: () => act,
2113
+ ariaTree: () => ariaTree,
2114
+ componentVerbs: () => componentVerbs,
2115
+ locate: () => locate,
2116
+ mountDom: () => mountDom,
2117
+ mountReact: () => mountReact,
2118
+ readGolden: () => readGolden,
2119
+ writeGolden: () => writeGolden,
2120
+ writeInPlaceText: () => writeInPlaceText
2121
+ });
2122
+ const FIELD_ROLES = [
2123
+ "checkbox",
2124
+ "combobox",
2125
+ "radio",
2126
+ "searchbox",
2127
+ "slider",
2128
+ "spinbutton",
2129
+ "switch",
2130
+ "textbox"
2131
+ ];
2132
+ const SEARCH_ELEMENT = "css=search:not([role])";
2133
+ locators.extend({ getBySearchElement: () => SEARCH_ELEMENT });
2134
+ function answersSearch(scope) {
2135
+ return "getBySearchElement" in scope;
2136
+ }
2137
+ function locateSearch(scope, target, exact) {
2138
+ const byRole = target.name === void 0 ? scope.getByRole("search", { exact }) : scope.getByRole("search", {
2139
+ exact,
2140
+ name: target.name
2141
+ });
2142
+ if (!answersSearch(scope)) return byRole;
2143
+ const byElement = scope.getBySearchElement();
2144
+ return byRole.or(target.name === void 0 ? byElement : byElement.and(scope.getByLabelText(target.name, { exact })));
2145
+ }
2146
+ /** No `.first()`: taking the first match is the silent wrong-element bug `target-matches-one-element` prevents. */
2147
+ function locate(root, target) {
2148
+ const scope = target.scope ? locate(root, target.scope) : root;
2149
+ const exact = target.exact ?? true;
2150
+ const name = target.name ?? "";
2151
+ if (target.kind === "field") {
2152
+ let field = scope.getByLabelText(name, { exact });
2153
+ for (const role of FIELD_ROLES) field = field.or(scope.getByRole(role, {
2154
+ exact,
2155
+ name
2156
+ }));
2157
+ return field;
2158
+ }
2159
+ if (target.kind === "testId") return scope.getByTestId(name);
2160
+ if (target.kind === "text") return scope.getByText(name, { exact });
2161
+ if (target.kind === "search") return locateSearch(scope, target, exact);
2162
+ return scope.getByRole(target.kind, target.name === void 0 ? { exact } : {
2163
+ exact,
2164
+ name: target.name
2165
+ });
2166
+ }
2167
+ function ambiguousLevel(target) {
2168
+ for (const level of scopeChain(target).slice(0, -1)) if (locate(page, level).elements().length > 1) return level;
2169
+ return target;
2170
+ }
2171
+ /** The ambiguous level is found first: a scope matching several landmarks is the real fault. */
2172
+ async function act(target, action) {
2173
+ const locator = locate(page, target);
2174
+ try {
2175
+ return await action(locator);
2176
+ } catch (error) {
2177
+ if (locator.elements().length <= 1) throw error;
2178
+ const culprit = ambiguousLevel(target);
2179
+ const matches = candidatesOf(locate(page, culprit).elements(), CANDIDATE_LIMITS);
2180
+ const url = globalThis.location.href;
2181
+ throw ambiguousTargetError({
2182
+ target: culprit,
2183
+ matches,
2184
+ url
2185
+ });
2186
+ }
2187
+ }
2188
+ function byPresence(target) {
2189
+ return target.kind === "option";
2190
+ }
2191
+ function valueOf(node) {
2192
+ return node instanceof HTMLInputElement || node instanceof HTMLSelectElement || node instanceof HTMLTextAreaElement ? node.value : void 0;
2193
+ }
2194
+ function isGone(locator, target) {
2195
+ const node = locator.query();
2196
+ if (target.focused === true) return node !== document.activeElement;
2197
+ if (target.disabled !== void 0) return node === null || node.matches(":disabled") !== target.disabled;
2198
+ if (target.selected !== void 0) return node === null || node.matches(":checked") !== target.selected;
2199
+ if (target.value !== void 0) return node === null || valueOf(node) !== target.value;
2200
+ return node === null || (byPresence(target) ? false : !node.checkVisibility());
2201
+ }
2202
+ function remainingBudget() {
2203
+ const current = TestRunner.getCurrentTest();
2204
+ const started = (current?.result?.startTime ?? NaN) - performance.timeOrigin;
2205
+ if (current === void 0 || Number.isNaN(started)) return;
2206
+ return Math.max(current.timeout - (performance.now() - started) - 100, 0);
2207
+ }
2208
+ function polling(probe, message) {
2209
+ const timeout = remainingBudget();
2210
+ return expect.poll(probe, {
2211
+ message,
2212
+ ...timeout === void 0 ? {} : { timeout }
2213
+ });
2214
+ }
2215
+ function keystroke(key) {
2216
+ return key.length > 1 ? `{${key}}` : key;
2217
+ }
2218
+ async function seen(locator, target) {
2219
+ if (target.focused === true) await expect.element(locator).toHaveFocus();
2220
+ else if (target.disabled === true) await expect.element(locator).toBeDisabled();
2221
+ else if (target.disabled === false) await expect.element(locator).toBeEnabled();
2222
+ else if (target.selected !== void 0) await polling(() => locator.query()?.matches(":checked") === true, `${asWritten(target)} is not the selected one`).toBe(target.selected);
2223
+ else if (target.value !== void 0) await expect.element(locator).toHaveValue(target.value);
2224
+ else if (byPresence(target)) await expect.element(locator).toBeInTheDocument();
2225
+ else await expect.element(locator).toBeVisible();
2226
+ }
2227
+ function componentVerbs(surface) {
2228
+ return {
2229
+ check: async (target) => {
2230
+ await act(target, async (locator) => {
2231
+ const node = locator.element();
2232
+ if (!(node instanceof HTMLInputElement) || !node.checked) await locator.click();
2233
+ });
2234
+ },
2235
+ click: async (target) => {
2236
+ await act(target, async (locator) => {
2237
+ await locator.click();
2238
+ });
2239
+ },
2240
+ fill: async (target, value) => {
2241
+ await act(target, async (locator) => {
2242
+ await locator.fill(value);
2243
+ });
2244
+ },
2245
+ gone: async (target) => {
2246
+ await act(target, async (locator) => {
2247
+ await polling(() => isGone(locator, target), `${asWritten(target)} is still on the screen`).toBeTruthy();
2248
+ });
2249
+ },
2250
+ hover: async (target) => {
2251
+ await act(target, async (locator) => {
2252
+ await locator.hover();
2253
+ });
2254
+ },
2255
+ press: async (key) => {
2256
+ await userEvent.keyboard(keystroke(key));
2257
+ },
2258
+ rerender: surface.rerender,
2259
+ see: async (target) => {
2260
+ await act(target, async (locator) => {
2261
+ await seen(locator, target);
2262
+ });
2263
+ },
2264
+ select: async (target, option) => {
2265
+ await act(target, async (locator) => {
2266
+ await locator.selectOptions(option);
2267
+ });
2268
+ },
2269
+ unmount: surface.unmount
2270
+ };
2271
+ }
2272
+ async function ariaTree() {
2273
+ return await commands.ariaTree();
2274
+ }
2275
+ async function readGolden(name) {
2276
+ return await commands.goldenRead(name);
2277
+ }
2278
+ async function writeGolden(name, content) {
2279
+ return await commands.goldenWrite(name, content);
2280
+ }
2281
+ async function writeInPlaceText(stack, text) {
2282
+ return await commands.inPlaceWrite(stack, text);
2283
+ }
2284
+ async function mountReact(ui) {
2285
+ let render;
2286
+ try {
2287
+ ({render} = await import("vitest-browser-react"));
2288
+ } catch (error) {
2289
+ throw new Error(refusal("seam-is-an-optional-peer", {
2290
+ fix: "npm install -D vitest-browser-react react react-dom",
2291
+ found: `component.render(<X />) requires \`vitest-browser-react\` and \`react\`, and they do not load (${String(error)})`
2292
+ }), { cause: error });
2293
+ }
2294
+ const rendered = await render(ui);
2295
+ return {
2296
+ container: rendered.container,
2297
+ rerender: rendered.rerender,
2298
+ unmount: rendered.unmount
2299
+ };
2300
+ }
2301
+ /** The function may return its own teardown; no react is loaded. */
2302
+ function mountDom(mount) {
2303
+ const container = document.createElement("div");
2304
+ document.body.append(container);
2305
+ const teardown = mount(container);
2306
+ return {
2307
+ container,
2308
+ rerender: () => {
2309
+ throw new Error(refusal({
2310
+ fix: "render the DOM function again, in a new render()",
2311
+ found: "rerender() on a DOM function",
2312
+ read: "docs/guides/component-specs.md#the-chain",
2313
+ why: "rerender() replaces a React tree, what a parent does with new props."
2314
+ }));
2315
+ },
2316
+ unmount: async () => {
2317
+ teardown?.();
2318
+ container.remove();
2319
+ }
2320
+ };
2321
+ }
2322
+ //#endregion
2323
+ //#region src/specifications/component/golden/golden.ts
2324
+ function updating() {
2325
+ return inject("update") === true;
2326
+ }
2327
+ async function matchGolden(accessor, name, frozen) {
2328
+ requireExtension(name, "stream");
2329
+ const actual = accessor.text;
2330
+ const stored = await readGolden(name);
2331
+ if (stored === null) {
2332
+ if (updating() && !frozen) {
2333
+ const path = await writeGolden(name, actual);
2334
+ return {
2335
+ message: () => `wrote ${path}`,
2336
+ pass: true
2337
+ };
2338
+ }
2339
+ return {
2340
+ message: () => `${accessor.streamName} fixture "${name}" does not exist beside this test.\n${missingHint(frozen)}`,
2341
+ pass: false
2342
+ };
2343
+ }
2344
+ const comparison = compareStreamText(accessor, name, stored);
2345
+ if (!comparison.pass && updating() && !frozen) {
2346
+ await writeGolden(name, actual);
2347
+ return {
2348
+ message: () => `updated ${name}`,
2349
+ pass: true
2350
+ };
2351
+ }
2352
+ return comparison;
2353
+ }
2354
+ async function toMatchGolden(received, expected, options) {
2355
+ if (!isDiskFile(expected)) throw new TypeError(refusal("one-verb-per-meaning", {
2356
+ fix: "compare the tree under node, or a captured text of the page to its file(...)",
2357
+ found: "toMatch(directory(...)) in a page, which has no working directory"
2358
+ }));
2359
+ if (!(received instanceof TextAccessor)) throw new TypeError(refusal("one-verb-per-meaning", {
2360
+ fix: `compare a captured text (result.tree, result.html, text(...)) to file(${JSON.stringify(expected.path)})`,
2361
+ found: `toMatch(file(${JSON.stringify(expected.path)})) in a page, on a subject that is no captured text`
2362
+ }));
2363
+ return await matchGolden(received, expected.path, options?.frozen === true);
2364
+ }
2365
+ async function toMatchWhole(received, expected, call) {
2366
+ if (!(received instanceof TextAccessor) || typeof expected !== "string") throw new TypeError(refusal("one-verb-per-meaning", {
2367
+ fix: "compare a captured text (result.tree, result.html, text(...)) to a text written in place",
2368
+ found: "toMatch(<value>) in a page, on a subject or a value that is no text"
2369
+ }));
2370
+ const comparison = compareStreamInPlace(received, expected);
2371
+ const frozen = call?.options?.frozen === true;
2372
+ if (comparison.pass || !updating() || frozen || this.isNot === true) return comparison;
2373
+ const previous = dedent(expected);
2374
+ const merged = mergeTextPreservingPlaceholders(previous === "" ? null : previous, received.text, received.captures);
2375
+ const where = await writeInPlaceText(call?.stack ?? "", merged);
2376
+ return {
2377
+ message: () => `updated the value in place at ${where}`,
2378
+ pass: true
2379
+ };
2380
+ }
2381
+ async function toBeEmpty(received) {
2382
+ return await Promise.reject(received instanceof TextAccessor ? emptyStreamRefusal(received) : new TypeError(refusal("one-verb-per-meaning", {
2383
+ fix: "compare the stream's text: expect(<result>.<stream>.text).toBe('')",
2384
+ found: "toBeEmpty() in a page, which holds no table"
2385
+ })));
2386
+ }
2387
+ let registered = false;
2388
+ /** Idempotent; the browser entry's import is the registration. */
2389
+ function registerBrowserMatchers() {
2390
+ if (registered) return;
2391
+ registered = true;
2392
+ expect.extend({
2393
+ [GOLDEN_MATCHER]: toMatchGolden,
2394
+ [WHOLE_MATCHER]: toMatchWhole,
2395
+ toBeEmpty
2396
+ });
2397
+ matchThroughGolden(chai, streamValueRoute);
2398
+ }
2399
+ //#endregion
2400
+ //#region src/specifications/component/component.face.browser.ts
2401
+ /** The component chain, opened once for the page; shared configuration is the project's. */
2402
+ const component = componentDefinition.open({});
2403
+ registerBrowserMatchers();
2404
+ //#endregion
2405
+ //#region src/stubs/kinds/http-filter.ts
2406
+ function rawOf(body) {
2407
+ return typeof body === "string" ? body : JSON.stringify(body ?? "");
2408
+ }
2409
+ function matchesBody(body, expected) {
2410
+ if (typeof expected === "string") return rawOf(body).includes(expected);
2411
+ if (expected instanceof RegExp) return expected.test(rawOf(body));
2412
+ return structuralSubset(expected, body, new CaptureScope());
2413
+ }
2414
+ function matchesEntries(expected, lookup) {
2415
+ return Object.entries(expected).every(([key, value]) => {
2416
+ const actual = lookup(key);
2417
+ if (actual === void 0) return false;
2418
+ return value instanceof RegExp ? value.test(actual) : actual === value;
2419
+ });
2420
+ }
2421
+ function buildMatch(filter) {
2422
+ if (!filter) return;
2423
+ return (request) => {
2424
+ if (filter.body !== void 0 && !matchesBody(request.body, filter.body)) return false;
2425
+ if (filter.headers && !matchesEntries(filter.headers, (key) => request.headers[key.toLowerCase()])) return false;
2426
+ if (filter.query) {
2427
+ let params;
2428
+ try {
2429
+ params = new URL(request.url, "http://stub.invalid").searchParams;
2430
+ } catch {
2431
+ return false;
2432
+ }
2433
+ if (!matchesEntries(filter.query, (key) => params.get(key) ?? void 0)) return false;
2434
+ }
2435
+ return true;
2436
+ };
2437
+ }
2438
+ function absentHeadersOf(filter = {}) {
2439
+ const absent = filter.without?.headers?.map((name) => name.toLowerCase()) ?? [];
2440
+ if (absent.length === 0) return;
2441
+ const required = new Set(Object.keys(filter.headers ?? {}).map((name) => name.toLowerCase()));
2442
+ const contradicted = absent.filter((name) => required.has(name));
2443
+ if (contradicted.length > 0) {
2444
+ const names = contradicted.join(", ");
2445
+ throw new Error(refusal({
2446
+ fix: `state ${names} in \`headers\` or in \`without.headers\`, never both`,
2447
+ found: `an http stub filter that both requires and forbids the header(s) ${names}`,
2448
+ read: "docs/reference/stubs.md#http-any-url",
2449
+ why: "A header both required and absent matches no request, so the stub could never fire."
2450
+ }));
2451
+ }
2452
+ return absent;
2453
+ }
2454
+ //#endregion
2455
+ //#region src/stubs/kinds/http.ts
2456
+ function wrapJson(data) {
2457
+ return {
2458
+ status: 200,
2459
+ body: data
2460
+ };
2461
+ }
2462
+ const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
2463
+ const STREAM_CONTENT_TYPE = "application/octet-stream";
2464
+ const SSE_CONTENT_TYPE = "text/event-stream";
2465
+ function frameEvent(event) {
2466
+ const lines = [];
2467
+ if (event.event !== void 0) lines.push(`event: ${event.event}`);
2468
+ if (event.id !== void 0) lines.push(`id: ${event.id}`);
2469
+ if (event.retry !== void 0) lines.push(`retry: ${event.retry}`);
2470
+ const payload = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
2471
+ for (const line of payload.split("\n")) lines.push(`data: ${line}`);
2472
+ return `${lines.join("\n")}\n\n`;
2473
+ }
2474
+ function declare(method, url, filter) {
2475
+ const absentHeaders = absentHeadersOf(filter);
2476
+ return {
2477
+ ...absentHeaders && { absentHeaders },
2478
+ adapter: "http",
2479
+ match: buildMatch(filter),
2480
+ method,
2481
+ url,
2482
+ wrap: wrapJson
2483
+ };
2484
+ }
2485
+ /** Generic HTTP stub helpers: an absolute URL, or a path form (`/articles/{{uuid}}`) matching any origin. */
2486
+ const http = {
2487
+ any(url, filter) {
2488
+ return declare("*", url, filter);
2489
+ },
2490
+ delete(url, filter) {
2491
+ return declare("DELETE", url, filter);
2492
+ },
2493
+ get(url, filter) {
2494
+ return declare("GET", url, filter);
2495
+ },
2496
+ patch(url, filter) {
2497
+ return declare("PATCH", url, filter);
2498
+ },
2499
+ post(url, filter) {
2500
+ return declare("POST", url, filter);
2501
+ },
2502
+ put(url, filter) {
2503
+ return declare("PUT", url, filter);
2504
+ },
2505
+ /** A stub the chain fails on if the subject reaches its route: `required` with `times: 0`. */
2506
+ never(request) {
2507
+ return {
2508
+ request,
2509
+ required: true,
2510
+ response: {
2511
+ status: 418,
2512
+ body: { error: `@jterrazz/test: ${describeRoute(request)} is declared never requested` }
2513
+ },
2514
+ times: 0
2515
+ };
2516
+ },
2517
+ /** Response: a body-less reply (204 by default). */
2518
+ empty(status = 204) {
2519
+ return {
2520
+ status,
2521
+ body: null
2522
+ };
2523
+ },
2524
+ /** Response: an error status. Without a body, `{ error: 'HTTP <status>' }`. */
2525
+ error(status, body) {
2526
+ return {
2527
+ status,
2528
+ body: body === void 0 ? { error: `HTTP ${status}` } : body
2529
+ };
2530
+ },
2531
+ /** Response: a JSON body (200 by default). */
2532
+ json(body, init) {
2533
+ return {
2534
+ status: init?.status ?? 200,
2535
+ body,
2536
+ delay: init?.delay,
2537
+ headers: init?.headers
2538
+ };
2539
+ },
2540
+ /** Response: no response at all; `fetch` rejects, as when no server is running. */
2541
+ unreachable() {
2542
+ return {
2543
+ body: null,
2544
+ transport: "network-error"
2545
+ };
2546
+ },
2547
+ /** Response: a body that arrives in pieces, written in order. */
2548
+ stream(chunks, init) {
2549
+ return {
2550
+ status: init?.status ?? 200,
2551
+ body: {
2552
+ chunks: [...chunks],
2553
+ contentType: init?.contentType ?? STREAM_CONTENT_TYPE,
2554
+ delayBetweenChunks: init?.delay ?? 0,
2555
+ kind: STREAM_BODY
2556
+ },
2557
+ headers: init?.headers
2558
+ };
2559
+ },
2560
+ /** Response: a server-sent event stream, served as `text/event-stream`. */
2561
+ sse(events, init) {
2562
+ return {
2563
+ status: init?.status ?? 200,
2564
+ body: {
2565
+ chunks: events.map(frameEvent),
2566
+ contentType: SSE_CONTENT_TYPE,
2567
+ delayBetweenChunks: init?.delay ?? 0,
2568
+ kind: STREAM_BODY
2569
+ },
2570
+ headers: {
2571
+ "cache-control": "no-cache",
2572
+ ...init?.headers
2573
+ }
2574
+ };
2575
+ },
2576
+ /** Response: a text body, served as `text/plain` (200 by default). */
2577
+ text(body, init) {
2578
+ return {
2579
+ status: init?.status ?? 200,
2580
+ body,
2581
+ delay: init?.delay,
2582
+ headers: {
2583
+ "content-type": TEXT_CONTENT_TYPE,
2584
+ ...init?.headers
2585
+ }
2586
+ };
2587
+ }
2588
+ };
2589
+ //#endregion
2590
+ //#region src/stubs/kinds/text-filter.ts
2591
+ function matchesText(filter, actual) {
2592
+ if (filter === void 0) return true;
2593
+ if (typeof filter === "string") return actual === filter;
2594
+ if (filter instanceof RegExp) return filter.test(actual);
2595
+ return structuralEquals(filter, actual, new CaptureScope());
2596
+ }
2597
+ /** A field of a request body read as JSON: absent wherever the body is not an object holding it. */
2598
+ function fieldOf(value, key) {
2599
+ return isRecord(value) ? value[key] : void 0;
2600
+ }
2601
+ const listOf = (value) => Array.isArray(value) ? value : [];
2602
+ const textOf = (value) => typeof value === "string" ? value : "";
2603
+ /** The content of the first message of `role`, as text: a structured content is its JSON. */
2604
+ function messageText(messages, role) {
2605
+ const content = fieldOf(listOf(messages).find((message) => fieldOf(message, "role") === role), "content");
2606
+ return content === void 0 || typeof content === "string" ? textOf(content) : JSON.stringify(content);
2607
+ }
2608
+ const namesOf = (tools, read) => listOf(tools).map((tool) => textOf(read(tool))).filter((name) => name !== "");
2609
+ //#endregion
2610
+ //#region src/stubs/kinds/anthropic.ts
2611
+ const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
2612
+ function matchesFilter(body, filter) {
2613
+ const names = namesOf(fieldOf(body, "tools"), (tool) => fieldOf(tool, "name"));
2614
+ return matchesText(filter.model, textOf(fieldOf(body, "model"))) && matchesText(filter.system, textOf(fieldOf(body, "system"))) && matchesText(filter.user, messageText(fieldOf(body, "messages"), "user")) && (filter.tools?.every((tool) => names.includes(tool)) ?? true);
2615
+ }
2616
+ function buildReply(data) {
2617
+ return {
2618
+ status: 200,
2619
+ body: {
2620
+ id: "msg-test",
2621
+ type: "message",
2622
+ role: "assistant",
2623
+ content: [{
2624
+ type: "text",
2625
+ text: typeof data === "string" ? data : JSON.stringify(data)
2626
+ }],
2627
+ model: "claude-sonnet-4-20250514",
2628
+ stop_reason: "end_turn",
2629
+ usage: {
2630
+ input_tokens: 10,
2631
+ output_tokens: 10
2632
+ }
2633
+ }
2634
+ };
2635
+ }
2636
+ function streamEvents(pieces) {
2637
+ return [
2638
+ {
2639
+ type: "message_start",
2640
+ message: {
2641
+ id: "msg-test",
2642
+ type: "message",
2643
+ role: "assistant",
2644
+ content: [],
2645
+ model: "claude-sonnet-4-20250514",
2646
+ stop_reason: null,
2647
+ stop_sequence: null,
2648
+ usage: {
2649
+ input_tokens: 10,
2650
+ output_tokens: 1
2651
+ }
2652
+ }
2653
+ },
2654
+ {
2655
+ type: "content_block_start",
2656
+ index: 0,
2657
+ content_block: {
2658
+ type: "text",
2659
+ text: ""
2660
+ }
2661
+ },
2662
+ { type: "ping" },
2663
+ ...pieces.map((text) => ({
2664
+ type: "content_block_delta",
2665
+ index: 0,
2666
+ delta: {
2667
+ type: "text_delta",
2668
+ text
2669
+ }
2670
+ })),
2671
+ {
2672
+ type: "content_block_stop",
2673
+ index: 0
2674
+ },
2675
+ {
2676
+ type: "message_delta",
2677
+ delta: {
2678
+ stop_reason: "end_turn",
2679
+ stop_sequence: null
2680
+ },
2681
+ usage: { output_tokens: 10 }
2682
+ },
2683
+ { type: "message_stop" }
2684
+ ].map((data) => ({
2685
+ data,
2686
+ event: data.type
2687
+ }));
2688
+ }
2689
+ /** Anthropic API stub helpers. */
2690
+ const anthropic = {
2691
+ /** Match Messages API calls, optionally through a gateway URL; a string filter is exact equality. */
2692
+ messages(filter, url) {
2693
+ return {
2694
+ adapter: "anthropic",
2695
+ method: "POST",
2696
+ url: url ?? ANTHROPIC_MESSAGES_URL,
2697
+ match: filter ? ({ body }) => matchesFilter(body, filter) : void 0,
2698
+ wrap(data) {
2699
+ if (isRecord(data)) return {
2700
+ status: 200,
2701
+ body: data
2702
+ };
2703
+ return buildReply(data);
2704
+ }
2705
+ };
2706
+ },
2707
+ /** A stub the chain fails on if the subject reaches this endpoint: `http.never()`. */
2708
+ never(request) {
2709
+ return http.never(request);
2710
+ },
2711
+ /** Response: data wrapped in the Messages format. */
2712
+ reply(data) {
2713
+ return buildReply(data);
2714
+ },
2715
+ /** Response: the text streamed as server-sent events, one `text_delta` per piece. */
2716
+ stream(pieces, init) {
2717
+ return http.sse(streamEvents(pieces), { delay: init?.delay ?? 0 });
2718
+ },
2719
+ /** Response: return an Anthropic error. */
2720
+ error(status, message) {
2721
+ return {
2722
+ status,
2723
+ body: {
2724
+ type: "error",
2725
+ error: {
2726
+ type: status === 429 ? "rate_limit_error" : "api_error",
2727
+ message: message ?? `Anthropic error (${status})`
2728
+ }
2729
+ }
2730
+ };
2731
+ },
2732
+ /** Response: simulate a timeout. */
2733
+ timeout() {
2734
+ return {
2735
+ status: 200,
2736
+ body: {},
2737
+ delay: 3e4
2738
+ };
2739
+ }
2740
+ };
2741
+ //#endregion
2742
+ //#region src/stubs/kinds/openai.ts
2743
+ const OPENAI_CHAT_URL = "https://api.openai.com/v1/chat/completions";
2744
+ const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
2745
+ function matchesChatFilter(body, filter) {
2746
+ const messages = fieldOf(body, "messages");
2747
+ const names = namesOf(fieldOf(body, "tools"), (tool) => fieldOf(fieldOf(tool, "function"), "name"));
2748
+ return matchesText(filter.model, textOf(fieldOf(body, "model"))) && matchesText(filter.system, messageText(messages, "system")) && matchesText(filter.user, messageText(messages, "user")) && (filter.tools?.every((tool) => names.includes(tool)) ?? true) && (filter.temperature === void 0 || fieldOf(body, "temperature") === filter.temperature);
2749
+ }
2750
+ function matchesResponsesFilter(body, filter) {
2751
+ const input = fieldOf(body, "input");
2752
+ const instructions = textOf(fieldOf(body, "instructions"));
2753
+ const userText = listOf(input).filter((message) => fieldOf(message, "role") === "user").map((message) => {
2754
+ const content = fieldOf(message, "content");
2755
+ return typeof content === "string" ? content : JSON.stringify(content);
2756
+ }).join(" ");
2757
+ const names = namesOf(fieldOf(body, "tools"), (tool) => fieldOf(tool, "name") ?? fieldOf(fieldOf(tool, "function"), "name"));
2758
+ return matchesText(filter.model, textOf(fieldOf(body, "model"))) && matchesText(filter.system, instructions === "" ? messageText(input, "system") : instructions) && matchesText(filter.user, userText) && (filter.tools?.every((tool) => names.includes(tool)) ?? true);
2759
+ }
2760
+ function buildChatReply(data) {
2761
+ const content = typeof data === "string" ? data : JSON.stringify(data);
2762
+ return {
2763
+ status: 200,
2764
+ body: {
2765
+ id: "chatcmpl-test",
2766
+ object: "chat.completion",
2767
+ created: Math.floor(Date.now() / 1e3),
2768
+ model: "gpt-4o-test",
2769
+ choices: [{
2770
+ index: 0,
2771
+ message: {
2772
+ role: "assistant",
2773
+ content
2774
+ },
2775
+ finish_reason: "stop"
2776
+ }],
2777
+ usage: {
2778
+ prompt_tokens: 10,
2779
+ completion_tokens: 10,
2780
+ total_tokens: 20
2781
+ }
2782
+ }
2783
+ };
2784
+ }
2785
+ function responsesEnvelope(text, usage = DEFAULT_USAGE) {
2786
+ return {
2787
+ id: "resp-test",
2788
+ object: "response",
2789
+ created_at: Math.floor(Date.now() / 1e3),
2790
+ model: "gpt-4o-test",
2791
+ output: [{
2792
+ type: "message",
2793
+ id: "msg-test",
2794
+ role: "assistant",
2795
+ content: [{
2796
+ type: "output_text",
2797
+ text,
2798
+ annotations: []
2799
+ }]
2800
+ }],
2801
+ usage: {
2802
+ input_tokens: usage.promptTokens,
2803
+ output_tokens: usage.completionTokens,
2804
+ total_tokens: usage.promptTokens + usage.completionTokens
2805
+ }
2806
+ };
2807
+ }
2808
+ function buildResponsesReply(data) {
2809
+ return {
2810
+ status: 200,
2811
+ body: responsesEnvelope(typeof data === "string" ? data : JSON.stringify(data))
2812
+ };
2813
+ }
2814
+ const DEFAULT_USAGE = {
2815
+ completionTokens: 10,
2816
+ promptTokens: 10
2817
+ };
2818
+ function chatStreamEvents(pieces, usage) {
2819
+ const created = Math.floor(Date.now() / 1e3);
2820
+ const chunk = (delta, finishReason) => ({ data: {
2821
+ id: "chatcmpl-test",
2822
+ object: "chat.completion.chunk",
2823
+ created,
2824
+ model: "gpt-4o-test",
2825
+ choices: [{
2826
+ index: 0,
2827
+ delta,
2828
+ logprobs: null,
2829
+ finish_reason: finishReason
2830
+ }]
2831
+ } });
2832
+ return [
2833
+ chunk({
2834
+ role: "assistant",
2835
+ content: ""
2836
+ }, null),
2837
+ ...pieces.map((piece) => chunk({ content: piece }, null)),
2838
+ chunk({}, "stop"),
2839
+ ...usage === void 0 ? [] : [{ data: {
2840
+ id: "chatcmpl-test",
2841
+ object: "chat.completion.chunk",
2842
+ created,
2843
+ model: "gpt-4o-test",
2844
+ choices: [],
2845
+ usage: {
2846
+ prompt_tokens: usage.promptTokens,
2847
+ completion_tokens: usage.completionTokens,
2848
+ total_tokens: usage.promptTokens + usage.completionTokens
2849
+ }
2850
+ } }],
2851
+ { data: "[DONE]" }
2852
+ ];
2853
+ }
2854
+ function responsesStreamEvents(pieces, usage) {
2855
+ const text = pieces.join("");
2856
+ const part = {
2857
+ type: "output_text",
2858
+ text,
2859
+ annotations: []
2860
+ };
2861
+ const item = {
2862
+ id: "msg-test",
2863
+ type: "message",
2864
+ role: "assistant"
2865
+ };
2866
+ const done = {
2867
+ ...item,
2868
+ status: "completed",
2869
+ content: [part]
2870
+ };
2871
+ const completed = {
2872
+ ...responsesEnvelope(text, usage),
2873
+ status: "completed"
2874
+ };
2875
+ const at = {
2876
+ item_id: "msg-test",
2877
+ output_index: 0,
2878
+ content_index: 0
2879
+ };
2880
+ return [
2881
+ {
2882
+ type: "response.created",
2883
+ response: {
2884
+ ...completed,
2885
+ status: "in_progress",
2886
+ output: [],
2887
+ usage: null
2888
+ }
2889
+ },
2890
+ {
2891
+ type: "response.output_item.added",
2892
+ output_index: 0,
2893
+ item: {
2894
+ ...item,
2895
+ status: "in_progress",
2896
+ content: []
2897
+ }
2898
+ },
2899
+ {
2900
+ type: "response.content_part.added",
2901
+ ...at,
2902
+ part: {
2903
+ ...part,
2904
+ text: ""
2905
+ }
2906
+ },
2907
+ ...pieces.map((delta) => ({
2908
+ type: "response.output_text.delta",
2909
+ ...at,
2910
+ delta
2911
+ })),
2912
+ {
2913
+ type: "response.output_text.done",
2914
+ ...at,
2915
+ text
2916
+ },
2917
+ {
2918
+ type: "response.content_part.done",
2919
+ ...at,
2920
+ part
2921
+ },
2922
+ {
2923
+ type: "response.output_item.done",
2924
+ output_index: 0,
2925
+ item: done
2926
+ },
2927
+ {
2928
+ type: "response.completed",
2929
+ response: {
2930
+ ...completed,
2931
+ output: [done]
2932
+ }
2933
+ }
2934
+ ].map((body, index) => ({
2935
+ data: {
2936
+ ...body,
2937
+ sequence_number: index
2938
+ },
2939
+ event: body.type
2940
+ }));
2941
+ }
2942
+ /** OpenAI API stub helpers. */
2943
+ const openai = {
2944
+ /** Match Chat Completions calls, at `url` where the client reaches another host; a string filter is exact equality, a RegExp or `match.includes` loosens it. */
2945
+ chat(filter, url) {
2946
+ return {
2947
+ adapter: "openai",
2948
+ method: "POST",
2949
+ url: url ?? OPENAI_CHAT_URL,
2950
+ match: filter ? ({ body }) => matchesChatFilter(body, filter) : void 0,
2951
+ wrap: buildChatReply
2952
+ };
2953
+ },
2954
+ /** Match Responses API calls; a JSON file is wrapped in the Responses envelope. */
2955
+ responses(filter, url) {
2956
+ return {
2957
+ adapter: "openai",
2958
+ method: "POST",
2959
+ url: url ?? OPENAI_RESPONSES_URL,
2960
+ match: filter ? ({ body }) => matchesResponsesFilter(body, filter) : void 0,
2961
+ wrap: buildResponsesReply
2962
+ };
2963
+ },
2964
+ /** A stub the chain fails on if the subject reaches this endpoint: `http.never()`. */
2965
+ never(request) {
2966
+ return http.never(request);
2967
+ },
2968
+ /** Response: data wrapped in the Chat Completions format. */
2969
+ reply(data) {
2970
+ return buildChatReply(data);
2971
+ },
2972
+ /** Response: the text streamed as server-sent events; `api` picks Chat Completions (default) or Responses. */
2973
+ stream(pieces, init) {
2974
+ const events = init?.api === "responses" ? responsesStreamEvents(pieces, init.usage) : chatStreamEvents(pieces, init?.usage);
2975
+ return http.sse(events, { delay: init?.delay ?? 0 });
2976
+ },
2977
+ /** Response: return an OpenAI error. */
2978
+ error(status, message) {
2979
+ return {
2980
+ status,
2981
+ body: { error: {
2982
+ message: message ?? `OpenAI error (${status})`,
2983
+ type: status === 429 ? "rate_limit_error" : "api_error",
2984
+ code: status === 429 ? "rate_limit_exceeded" : null
2985
+ } }
2986
+ };
2987
+ },
2988
+ /** Response: return malformed content. */
2989
+ malformed(content) {
2990
+ return buildChatReply(content);
2991
+ },
2992
+ /** Response: simulate a timeout. */
2993
+ timeout() {
2994
+ return {
2995
+ status: 200,
2996
+ body: {},
2997
+ delay: 3e4
2998
+ };
2999
+ }
3000
+ };
3001
+ //#endregion
3002
+ //#region src/stubs/intercept/intercept.ts
3003
+ function isOptions(value) {
3004
+ return isRecord(value) && "origin" in value;
3005
+ }
3006
+ function resolveRelativeAgainst(origin) {
3007
+ let base;
3008
+ try {
3009
+ base = new URL(origin);
3010
+ } catch {
3011
+ throw new Error(refusal({
3012
+ fix: "state an absolute URL: intercept(stubs, { origin: 'http://console.test' })",
3013
+ found: `intercept(): \`origin\` is \`${origin}\`, not an absolute URL`,
3014
+ read: "docs/reference/stubs.md#intercept-the-same-double-with-no-chain",
3015
+ why: "`origin` states where a relative request resolves, so it is an absolute URL."
3016
+ }));
3017
+ }
3018
+ return () => {
3019
+ const underlying = globalThis.fetch;
3020
+ globalThis.fetch = async (input, init) => typeof input === "string" ? await underlying(new URL(input, base).toString(), init) : await underlying(input, init);
3021
+ return () => {
3022
+ globalThis.fetch = underlying;
3023
+ };
3024
+ };
3025
+ }
3026
+ function interceptThrough(register) {
3027
+ async function intercept(requestOrStubs, maybeResponseOrOptions, maybeOptions) {
3028
+ const options = isOptions(maybeOptions) ? maybeOptions : isOptions(maybeResponseOrOptions) ? maybeResponseOrOptions : void 0;
3029
+ const stubs = declared("intercept", requestOrStubs, isOptions(maybeResponseOrOptions) ? void 0 : maybeResponseOrOptions);
3030
+ if (stubs.length === 0) throw new Error(refusal({
3031
+ fix: "declare at least one stub, or drop the intercept: a subject with no network needs none",
3032
+ found: "intercept(), and the list of stubs is empty",
3033
+ read: "docs/reference/stubs.md#intercept-the-same-double-with-no-chain",
3034
+ why: "An empty list guards nothing."
3035
+ }));
3036
+ const installOrigin = options?.origin === void 0 ? void 0 : resolveRelativeAgainst(options.origin);
3037
+ const registration = await register(stubs);
3038
+ const restoreOrigin = installOrigin?.();
3039
+ return { [Symbol.asyncDispose]: async () => {
3040
+ restoreOrigin?.();
3041
+ const violation = registration.violation();
3042
+ registration.cleanup();
3043
+ if (violation) throw violation;
3044
+ } };
3045
+ }
3046
+ return intercept;
3047
+ }
3048
+ //#endregion
3049
+ //#region src/stubs/delivery/browser-page/intercept.ts
3050
+ /** The module-scope network double, on msw's worker. */
3051
+ const intercept = interceptThrough(registerWorkerStubs);
3052
+ //#endregion
3053
+ //#region src/disk/packed.ts
3054
+ const PACKED = Symbol.for("@jterrazz/test:packed");
3055
+ /** Name the package a consumer installs: the project the spec belongs to, or the package a directory holds, packed as `npm pack` packs it. */
3056
+ function packed(source) {
3057
+ return Object.freeze({
3058
+ [PACKED]: true,
3059
+ path: isDiskDirectory(source) ? source.path : null
3060
+ });
3061
+ }
3062
+ //#endregion
3063
+ //#region src/clock/clock.ts
3064
+ function take(method, options) {
3065
+ if (vi.isFakeTimers()) throw new Error(refusal({
3066
+ fix: "end the first scope before taking another, or state the instant on the chain (`.clock(iso)`) rather than around it",
3067
+ found: `clock.${method}(), and a clock is already pinned`,
3068
+ read: "docs/reference/time-and-doubles.md#the-clock",
3069
+ why: "One clock per test: ending a nested scope would end the outer one too."
3070
+ }));
3071
+ vi.useFakeTimers(options);
3072
+ return { [Symbol.dispose]: () => {
3073
+ vi.useRealTimers();
3074
+ } };
3075
+ }
3076
+ /** The one time primitive (`no-sampled-value-in-expectation`): `at()` pins the calendar, `run()` the scheduler too. */
3077
+ const clock = {
3078
+ /** Move the pinned clock forward; under `clock.run` fire what came due. Refused with no clock taken. */
3079
+ async advance(ms) {
3080
+ if (!vi.isFakeTimers()) throw new Error(refusal({
3081
+ fix: "take one first: `using _ = clock.at('<iso>')` (the calendar) or `using _ = clock.run()` (the scheduler)",
3082
+ found: "clock.advance(), and no clock is pinned",
3083
+ read: "docs/reference/time-and-doubles.md#the-clock",
3084
+ why: "Only a pinned clock moves: the real one holds no time of the test's to advance."
3085
+ }));
3086
+ await vi.advanceTimersByTimeAsync(ms);
3087
+ },
3088
+ /** Pin `Date` at `iso` for the current scope; the scheduler stays real. */
3089
+ at(iso) {
3090
+ return take("at", {
3091
+ now: instantOf(iso, (value) => `clock.at(${value})`),
3092
+ toFake: ["Date"]
3093
+ });
3094
+ },
3095
+ /** Take the scheduler as well as the calendar; `clock.advance` makes timers due. */
3096
+ run(iso) {
3097
+ return take("run", iso === void 0 ? {} : { now: instantOf(iso, (value) => `clock.run(${value})`) });
3098
+ }
3099
+ };
3100
+ //#endregion
3101
+ //#region src/mocks/mock-of.ts
3102
+ function mockOf(options) {
3103
+ return options?.deep === false ? mock() : mockDeep();
3104
+ }
3105
+ //#endregion
3106
+ //#region src/visitor/targets/modifiers.ts
3107
+ /** Where the keyboard is: `see(focused(button('Open')))`. */
3108
+ const focused = (target) => ({
3109
+ ...target,
3110
+ focused: true
3111
+ });
3112
+ /** The element refuses input: `see(disabled(button('Delete')))`. */
3113
+ const disabled = (target) => ({
3114
+ ...target,
3115
+ disabled: true
3116
+ });
3117
+ /** The other direction of {@link disabled}: the element takes input. */
3118
+ const enabled = (target) => ({
3119
+ ...target,
3120
+ disabled: false
3121
+ });
3122
+ /** The option the field is on: `see(selected(option('LinkedIn')))`. */
3123
+ const selected = (target) => ({
3124
+ ...target,
3125
+ selected: true
3126
+ });
3127
+ /** What the field holds, read from its live value: `see(valued(field('Title'), '...'))`. */
3128
+ const valued = (target, value) => ({
3129
+ ...target,
3130
+ value
3131
+ });
3132
+ //#endregion
3133
+ //#region src/expectations/text/text-subject.browser.ts
3134
+ /** `text()` in a page: no anchor, since the golden commands read the test's path server-side. */
3135
+ function text(value) {
3136
+ return new TextAccessor(value, "text", "");
3137
+ }
3138
+ //#endregion
3139
+ //#region src/expectations/conditions/required.ts
3140
+ /** The value, or a failure naming what was missing and why it mattered. */
3141
+ function required(value, why) {
3142
+ if (value === null || value === void 0) throw new Error(refusal({
3143
+ fix: "state it in the Given, or assert on what the subject actually produced",
3144
+ found: `required(): ${why}, got ${value === null ? "null" : "undefined"}`,
3145
+ read: "docs/reference/assertions.md#two-helpers-that-are-not-matchers",
3146
+ why: "`required()` answers a present value, and `null` and `undefined` are the two it refuses."
3147
+ }));
3148
+ return value;
3149
+ }
3150
+ //#endregion
3151
+ //#region src/expectations/conditions/wait-until.ts
3152
+ const DEFAULT_TIMEOUT_MS = 5e3;
3153
+ const DEFAULT_INTERVAL_MS = 50;
3154
+ /** Wait until a condition holds; measured on `performance.now()`, so `clock.at()` cannot freeze it. Under `clock.run()` it never wakes: use `clock.advance()`. */
3155
+ async function waitUntil(predicate, options = {}) {
3156
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
3157
+ const interval = options.interval ?? DEFAULT_INTERVAL_MS;
3158
+ const deadline = performance.now() + timeout;
3159
+ for (;;) {
3160
+ if (await predicate()) return;
3161
+ if (performance.now() >= deadline) throw new Error(refusal({
3162
+ fix: "raise the budget if the wait is legitimate, or wait on what actually changes",
3163
+ found: `waitUntil(): ${options.why ?? "the condition"} did not hold within ${timeout}ms`,
3164
+ read: "docs/reference/assertions.md#two-helpers-that-are-not-matchers",
3165
+ why: "`waitUntil()` waits on a condition, within a budget."
3166
+ }));
3167
+ await pause(interval);
3168
+ }
3169
+ }
3170
+ //#endregion
3171
+ //#region src/index.browser.ts
3172
+ /** The seven constructors, present and refusing in a page. */
3173
+ const specification = {
3174
+ api: () => refuse("specification.api()"),
3175
+ cli: () => refuse("specification.cli()"),
3176
+ integration: () => refuse("specification.integration()"),
3177
+ job: () => refuse("specification.job()"),
3178
+ lint: () => refuse("specification.lint()"),
3179
+ mobile: () => refuse("specification.mobile()"),
3180
+ website: () => refuse("specification.website()")
3181
+ };
3182
+ const ruleTester = () => refuse("ruleTester()");
3183
+ const process$1 = () => refuse("process()");
3184
+ const ProcessHandle = nodeOnlyClass("ProcessHandle");
3185
+ const postgres = () => refuse("postgres()");
3186
+ const redis = () => refuse("redis()");
3187
+ const sqlite = () => refuse("sqlite()");
3188
+ //#endregion
3189
+ export { region as A, option as B, component as C, form as D, contentinfo as E, dialog as F, directory as G, status as H, field as I, defineStubs as J, file as K, heading as L, within as M, button as N, main as O, content as P, link as R, http as S, complementary as T, table as U, row as V, testId as W, match as X, Matcher as Y, clock as _, ruleTester as a, openai as b, waitUntil as c, disabled as d, enabled as f, mockOf as g, valued as h, redis as i, search as j, navigation as k, required as l, selected as m, postgres as n, specification as o, focused as p, defineStub as q, process$1 as r, sqlite as s, ProcessHandle as t, text as u, packed as v, banner as w, anthropic as x, intercept as y, listitem as z };
3190
+
3191
+ //# sourceMappingURL=shared-index.browser.js.map