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