@jterrazz/test 15.3.0 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +120 -427
  2. package/dist/appium.adapter.js +71 -15
  3. package/dist/artifacts.js +25 -1735
  4. package/dist/browser/index.js +304 -233
  5. package/dist/checker.d.ts +1 -1
  6. package/dist/checker.js +1103 -97
  7. package/dist/fixtures.js +110 -0
  8. package/dist/index.d.ts +956 -971
  9. package/dist/index.js +2159 -2281
  10. package/dist/literate.js +1747 -0
  11. package/dist/match.js +625 -84
  12. package/dist/oxlint.cjs +3585 -1480
  13. package/dist/oxlint.d.cts +60 -48
  14. package/dist/oxlint.d.ts +60 -48
  15. package/dist/oxlint.js +3585 -1480
  16. package/dist/playwright.adapter.js +51 -8
  17. package/dist/{intercept.js → server.js} +2 -2
  18. package/dist/stream.js +6 -6
  19. package/dist/{ambiguity.js → substring-warning.js} +106 -7
  20. package/dist/vitest.d.ts +67 -48
  21. package/dist/vitest.js +229 -93
  22. package/docs/01-architecture.md +95 -0
  23. package/docs/02-developing.md +351 -0
  24. package/docs/03-testing.md +495 -0
  25. package/docs/04-operating.md +101 -0
  26. package/docs/05-module-tests.md +152 -0
  27. package/docs/06-integration.md +180 -0
  28. package/docs/07-component.md +246 -0
  29. package/docs/08-website.md +404 -0
  30. package/docs/09-mobile.md +270 -0
  31. package/docs/10-api.md +264 -0
  32. package/docs/11-jobs.md +157 -0
  33. package/docs/12-cli.md +677 -0
  34. package/docs/13-elements.md +242 -0
  35. package/docs/14-assertions.md +314 -0
  36. package/docs/15-tokens.md +253 -0
  37. package/docs/16-contracts.md +438 -0
  38. package/docs/17-services.md +188 -0
  39. package/docs/18-conventions.md +305 -0
  40. package/docs/19-linting.md +510 -0
  41. package/docs/README.md +36 -0
  42. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  43. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  44. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  45. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  46. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  47. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  48. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  49. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  50. package/docs/decisions/_template.md +22 -0
  51. package/docs/reference/classes/Matcher.md +120 -0
  52. package/docs/reference/classes/ProcessHandle.md +368 -0
  53. package/docs/reference/functions/defineContract.md +39 -0
  54. package/docs/reference/functions/defineContracts.md +36 -0
  55. package/docs/reference/functions/disabled.md +28 -0
  56. package/docs/reference/functions/enabled.md +20 -0
  57. package/docs/reference/functions/focused.md +26 -0
  58. package/docs/reference/functions/mockOf.md +79 -0
  59. package/docs/reference/functions/postgres.md +31 -0
  60. package/docs/reference/functions/process.md +33 -0
  61. package/docs/reference/functions/redis.md +28 -0
  62. package/docs/reference/functions/required.md +41 -0
  63. package/docs/reference/functions/selected.md +26 -0
  64. package/docs/reference/functions/sqlite.md +33 -0
  65. package/docs/reference/functions/testId.md +20 -0
  66. package/docs/reference/functions/text.md +40 -0
  67. package/docs/reference/functions/valued.md +28 -0
  68. package/docs/reference/functions/waitUntil.md +46 -0
  69. package/docs/reference/functions/within.md +29 -0
  70. package/docs/reference/index.md +196 -0
  71. package/docs/reference/interfaces/BaseResult.md +87 -0
  72. package/docs/reference/interfaces/CallResult.md +160 -0
  73. package/docs/reference/interfaces/CaptureScope.md +82 -0
  74. package/docs/reference/interfaces/CliResult.md +253 -0
  75. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  76. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  77. package/docs/reference/interfaces/FetchResult.md +180 -0
  78. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  79. package/docs/reference/interfaces/HttpResult.md +126 -0
  80. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  81. package/docs/reference/interfaces/PageResult.md +356 -0
  82. package/docs/reference/interfaces/RenderResult.md +72 -0
  83. package/docs/reference/interfaces/RenderedText.md +110 -0
  84. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  85. package/docs/reference/interfaces/ScreenResult.md +128 -0
  86. package/docs/reference/interfaces/TableAccessor.md +26 -0
  87. package/docs/reference/interfaces/TextAccessor.md +96 -0
  88. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  89. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  90. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  91. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  92. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  93. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  94. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  95. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  96. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  97. package/docs/reference/type-aliases/CliEnv.md +11 -0
  98. package/docs/reference/type-aliases/CliHandle.md +80 -0
  99. package/docs/reference/type-aliases/CliInput.md +36 -0
  100. package/docs/reference/type-aliases/CliOutput.md +46 -0
  101. package/docs/reference/type-aliases/CliPort.md +58 -0
  102. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  103. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  104. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  105. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  106. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  107. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  108. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  109. package/docs/reference/type-aliases/Contract.md +68 -0
  110. package/docs/reference/type-aliases/ContractInput.md +13 -0
  111. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  112. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  113. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  114. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  115. package/docs/reference/type-aliases/Contracts.md +48 -0
  116. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  117. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  118. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  119. package/docs/reference/type-aliases/DevicePort.md +51 -0
  120. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  121. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  122. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  123. package/docs/reference/type-aliases/DomMount.md +21 -0
  124. package/docs/reference/type-aliases/ElementKind.md +27 -0
  125. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  126. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  127. package/docs/reference/type-aliases/ElementRef.md +128 -0
  128. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  129. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  130. package/docs/reference/type-aliases/HonoApp.md +31 -0
  131. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  132. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  133. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  134. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  135. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  136. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  137. package/docs/reference/type-aliases/Intercept.md +19 -0
  138. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  139. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  140. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  141. package/docs/reference/type-aliases/JobHandle.md +34 -0
  142. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  143. package/docs/reference/type-aliases/JobsResult.md +16 -0
  144. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  145. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  146. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  147. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  148. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  149. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  150. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  151. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  152. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  153. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  154. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  155. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  156. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  157. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  158. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  159. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  160. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  161. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  162. package/docs/reference/type-aliases/MockPort.md +10 -0
  163. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  164. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  165. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  166. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  167. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  168. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  169. package/docs/reference/type-aliases/ServerPort.md +36 -0
  170. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  171. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  172. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  173. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  174. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  175. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  176. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  177. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  178. package/docs/reference/type-aliases/SpecKind.md +8 -0
  179. package/docs/reference/type-aliases/SpecRun.md +108 -0
  180. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  181. package/docs/reference/type-aliases/SpecStream.md +32 -0
  182. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  183. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  184. package/docs/reference/type-aliases/SseEvent.md +58 -0
  185. package/docs/reference/type-aliases/StreamBody.md +61 -0
  186. package/docs/reference/type-aliases/TextFilter.md +22 -0
  187. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  188. package/docs/reference/type-aliases/Visitor.md +215 -0
  189. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  190. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  191. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  192. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  193. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  194. package/docs/reference/variables/anthropic.md +19 -0
  195. package/docs/reference/variables/banner.md +10 -0
  196. package/docs/reference/variables/button.md +10 -0
  197. package/docs/reference/variables/clock.md +16 -0
  198. package/docs/reference/variables/complementary.md +10 -0
  199. package/docs/reference/variables/component.md +10 -0
  200. package/docs/reference/variables/content.md +10 -0
  201. package/docs/reference/variables/contentinfo.md +10 -0
  202. package/docs/reference/variables/dialog.md +10 -0
  203. package/docs/reference/variables/field.md +10 -0
  204. package/docs/reference/variables/form.md +10 -0
  205. package/docs/reference/variables/heading.md +10 -0
  206. package/docs/reference/variables/http.md +40 -0
  207. package/docs/reference/variables/intercept.md +11 -0
  208. package/docs/reference/variables/link.md +10 -0
  209. package/docs/reference/variables/listitem.md +10 -0
  210. package/docs/reference/variables/main.md +10 -0
  211. package/docs/reference/variables/match.md +8 -0
  212. package/docs/reference/variables/navigation.md +10 -0
  213. package/docs/reference/variables/openai.md +21 -0
  214. package/docs/reference/variables/option.md +17 -0
  215. package/docs/reference/variables/region.md +10 -0
  216. package/docs/reference/variables/row.md +10 -0
  217. package/docs/reference/variables/search.md +10 -0
  218. package/docs/reference/variables/specification.md +8 -0
  219. package/docs/reference/variables/status.md +10 -0
  220. package/docs/reference/variables/table.md +10 -0
  221. package/package.json +32 -11
  222. package/dist/spec-document.js +0 -651
@@ -1,8 +1,7 @@
1
1
  import { TestRunner, expect, inject, onTestFinished, vi } from "vitest";
2
2
  import { commands, page, userEvent } from "vitest/browser";
3
3
  import { mock, mockDeep } from "vitest-mock-extended";
4
- import MockDatePackage from "mockdate";
5
- //#region src/specification/contracts/contract.ts
4
+ //#region src/model/contracts/contract.ts
6
5
  /**
7
6
  * Declare one contract. Identity function — its value is the enforced shape
8
7
  * and the naming convention:
@@ -88,7 +87,7 @@ function contractsOf(input) {
88
87
  return flatten([input]);
89
88
  }
90
89
  //#endregion
91
- //#region src/integrations/msw/scope.ts
90
+ //#region src/model/contracts/intercept.ts
92
91
  /**
93
92
  * The network double in MODULE scope — the same contracts, the same queue and
94
93
  * the same strictness as a chain's `.intercept()`, for the one kind of test
@@ -120,6 +119,38 @@ function declared(requestOrContracts, maybeResponse) {
120
119
  response: maybeResponse
121
120
  }];
122
121
  }
122
+ /** Is this argument the options object rather than a response? */
123
+ function isOptions(value) {
124
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "origin" in value;
125
+ }
126
+ /**
127
+ * Resolve every relative request against `origin` for the life of the scope.
128
+ *
129
+ * The framework owns the seam so a test never has to: this is the one place
130
+ * `fetch` is wrapped, it is put back when the scope ends, and what it does is
131
+ * the one thing a page would have done for free — turn `/api/posts` into a URL.
132
+ *
133
+ * Validating the origin and INSTALLING the wrapper are two steps because they
134
+ * happen at two moments: the refusal of a bad origin belongs before anything
135
+ * starts, while the wrapper may only go on once the engine is listening — it
136
+ * has to sit ON TOP of whatever `fetch` the engine installed, and be taken off
137
+ * before the engine takes its own back.
138
+ */
139
+ function resolveRelativeAgainst(origin) {
140
+ let base;
141
+ try {
142
+ base = new URL(origin);
143
+ } catch {
144
+ throw new Error(`intercept(): \`origin\` states where a relative request resolves — \`${origin}\` is not an absolute URL (e.g. 'http://console.test').`);
145
+ }
146
+ return () => {
147
+ const engine = globalThis.fetch;
148
+ globalThis.fetch = async (input, init) => typeof input === "string" && input.startsWith("/") ? await engine(new URL(input, base).toString(), init) : await engine(input, init);
149
+ return () => {
150
+ globalThis.fetch = engine;
151
+ };
152
+ };
153
+ }
123
154
  /**
124
155
  * Build the module-scope double on one engine. The node entry hands it msw's
125
156
  * server, the page's entry hands it msw's worker: one shape, two transports,
@@ -128,11 +159,15 @@ function declared(requestOrContracts, maybeResponse) {
128
159
  * @internal
129
160
  */
130
161
  function interceptThrough(register) {
131
- async function intercept(requestOrContracts, maybeResponse) {
132
- const contracts = declared(requestOrContracts, maybeResponse);
162
+ async function intercept(requestOrContracts, maybeResponseOrOptions, maybeOptions) {
163
+ const options = isOptions(maybeOptions) ? maybeOptions : isOptions(maybeResponseOrOptions) ? maybeResponseOrOptions : void 0;
164
+ const contracts = declared(requestOrContracts, options === maybeResponseOrOptions ? void 0 : maybeResponseOrOptions);
133
165
  if (contracts.length === 0) throw new Error("intercept(): declare at least one contract — an empty list guards nothing, and a subject with no network needs no intercept at all.");
166
+ const installOrigin = options?.origin === void 0 ? void 0 : resolveRelativeAgainst(options.origin);
134
167
  const registration = await register(contracts);
168
+ const restoreOrigin = installOrigin?.();
135
169
  return { [Symbol.asyncDispose]: async () => {
170
+ restoreOrigin?.();
136
171
  const violation = registration.violation();
137
172
  registration.cleanup();
138
173
  await Promise.resolve();
@@ -142,7 +177,7 @@ function interceptThrough(register) {
142
177
  return intercept;
143
178
  }
144
179
  //#endregion
145
- //#region src/specification/matching/match.ts
180
+ //#region src/model/matching/match.ts
146
181
  /** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
147
182
  const TOKEN_KINDS = [
148
183
  "any",
@@ -295,7 +330,94 @@ var CaptureScope = class {
295
330
  }
296
331
  };
297
332
  //#endregion
298
- //#region src/specification/matching/structural.ts
333
+ //#region src/model/result/grep.ts
334
+ /**
335
+ * Extract text blocks from output that contain a pattern.
336
+ * Splits by blank lines (how linter/compiler output is structured),
337
+ * returns only blocks matching the pattern.
338
+ *
339
+ * @example
340
+ * expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
341
+ * expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
342
+ */
343
+ function grep(output, pattern) {
344
+ return output.replaceAll(/\u001B\[[0-9;]*m/gu, "").split(/\n\s*\n/u).filter((block) => block.includes(pattern)).join("\n\n");
345
+ }
346
+ //#endregion
347
+ //#region src/model/result/text.ts
348
+ const ANSI_RE = /\u001B\[[0-9;]*m/gu;
349
+ /** Strip ANSI escape sequences from a string. */
350
+ function stripAnsiCodes(value) {
351
+ return value.replace(ANSI_RE, "");
352
+ }
353
+ /**
354
+ * Read-only accessor for a captured text handle — THE universal one: stdout,
355
+ * stderr, container logs, and file text all surface as a `TextAccessor`.
356
+ *
357
+ * Backed by a primitive string, exposed via {@link text}, but also provides
358
+ * `toString()` / `valueOf()` for string coercion, so common patterns like
359
+ * `String(result.stdout)` and template-literal interpolation still work.
360
+ *
361
+ * ANSI escape sequences are stripped by default before every comparison
362
+ * (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
363
+ * an escape hatch for application noise not covered by the `{{token}}`
364
+ * grammar.
365
+ *
366
+ * Text operations are **closed** over the type: `.grep(pattern)` returns a
367
+ * `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
368
+ * context and the `transform`, so results are chainable
369
+ * (`result.stdout.grep(a).grep(b)`) and snapshot-able
370
+ * (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
371
+ *
372
+ * Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
373
+ * and `expect(result.stdout).toMatch('name.txt')` (resolved against
374
+ * `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
375
+ */
376
+ var TextAccessor = class TextAccessor {
377
+ /** @internal Ref-capture scope shared by the current spec execution. */
378
+ captures;
379
+ /** @internal Stream label used in failure messages ("stdout" / "stderr"). */
380
+ streamName;
381
+ /** @internal Test-file directory — fixture resolution root for matchers. */
382
+ testDir;
383
+ /** The raw captured text (never transformed, ANSI preserved). */
384
+ text;
385
+ /** @internal Normaliser applied before comparisons, never to fixtures. */
386
+ transform;
387
+ constructor(value, streamName, testDir, options = {}) {
388
+ this.text = value;
389
+ this.streamName = streamName;
390
+ this.testDir = testDir;
391
+ this.transform = options.transform;
392
+ this.captures = options.captures ?? new CaptureScope();
393
+ }
394
+ /** @internal The text as compared by matchers — ANSI stripped, transform applied. */
395
+ get comparableText() {
396
+ const stripped = stripAnsiCodes(this.text);
397
+ return this.transform ? this.transform(stripped) : stripped;
398
+ }
399
+ /**
400
+ * Keep only the blank-line-separated blocks of the text that contain
401
+ * `pattern` (how linter/compiler output is structured), returned as a new
402
+ * `TextAccessor` — the same subject type, so the result is chainable and
403
+ * snapshot-able. The scalpel for probing large tool outputs; snapshot the
404
+ * whole surface by default, reach for `.grep()` for targeted checks.
405
+ */
406
+ grep(pattern) {
407
+ return new TextAccessor(grep(this.text, pattern), this.streamName, this.testDir, {
408
+ captures: this.captures,
409
+ transform: this.transform
410
+ });
411
+ }
412
+ toString() {
413
+ return this.text;
414
+ }
415
+ valueOf() {
416
+ return this.text;
417
+ }
418
+ };
419
+ //#endregion
420
+ //#region src/model/matching/structural.ts
299
421
  /**
300
422
  * Structural comparison engine shared by every fixture matcher.
301
423
  *
@@ -528,7 +650,7 @@ function textEquals(expected, actual, scope) {
528
650
  return placeholderStringMatches(expected, actual, scope);
529
651
  }
530
652
  //#endregion
531
- //#region src/specification/contracts/queue.ts
653
+ //#region src/model/contracts/queue.ts
532
654
  /**
533
655
  * The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
534
656
  * Both engines consume it: the MSW integration on api/jobs chains, and the
@@ -701,7 +823,7 @@ var ContractQueue = class {
701
823
  }
702
824
  };
703
825
  //#endregion
704
- //#region src/specification/timing/pause.ts
826
+ //#region src/model/timing/pause.ts
705
827
  /**
706
828
  * One answer to "wait" — the only `setTimeout` the model owns.
707
829
  *
@@ -716,7 +838,7 @@ async function pause(ms) {
716
838
  });
717
839
  }
718
840
  //#endregion
719
- //#region src/specification/contracts/stream.ts
841
+ //#region src/model/contracts/stream.ts
720
842
  /**
721
843
  * A declared stream, made real — ONE serialisation for all three engines.
722
844
  *
@@ -746,7 +868,7 @@ function toReadableStream(body) {
746
868
  } });
747
869
  }
748
870
  //#endregion
749
- //#region src/specification/contracts/types.ts
871
+ //#region src/model/contracts/types.ts
750
872
  /**
751
873
  * The marker that tells a STREAMED body from a value that happens to be an
752
874
  * object. A body reaches three different engines (msw's server, msw's worker,
@@ -760,7 +882,7 @@ function isStreamBody(body) {
760
882
  return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
761
883
  }
762
884
  //#endregion
763
- //#region src/integrations/msw/handlers.ts
885
+ //#region src/seams/msw/handlers.ts
764
886
  /**
765
887
  * A contract answers the request that was made, never a copy of an earlier one.
766
888
  *
@@ -866,7 +988,7 @@ async function observe(request) {
866
988
  };
867
989
  }
868
990
  //#endregion
869
- //#region src/integrations/msw/worker.ts
991
+ //#region src/seams/msw/worker.ts
870
992
  /**
871
993
  * What the runner itself fetches. Everything else a page asks for is the
872
994
  * subject's own traffic and answers to its contracts (D7).
@@ -935,7 +1057,7 @@ function resetWorkerContracts() {
935
1057
  if (workerInstance !== null) workerInstance.resetHandlers();
936
1058
  }
937
1059
  //#endregion
938
- //#region src/specification/facets/_common/ground.ts
1060
+ //#region src/model/chain/ground.ts
939
1061
  /**
940
1062
  * The GROUND a row of specs stands on — the naming law of a spec tree.
941
1063
  *
@@ -958,7 +1080,7 @@ function resetWorkerContracts() {
958
1080
  /** Goldens: every expected fixture, flat, extension included. */
959
1081
  const GROUND_EXPECTED = "_expected";
960
1082
  //#endregion
961
- //#region src/specification/facets/_common/reporter.ts
1083
+ //#region src/model/chain/reporter.ts
962
1084
  const GREEN = "\x1B[32m";
963
1085
  const RED = "\x1B[31m";
964
1086
  const RESET = "\x1B[0m";
@@ -1009,7 +1131,7 @@ function formatStdoutDiff(file, expected, actual, options) {
1009
1131
  return [...lines, ...rendered].join("\n");
1010
1132
  }
1011
1133
  //#endregion
1012
- //#region src/specification/facets/_common/result/text-assertions.ts
1134
+ //#region src/model/result/text-assertions.ts
1013
1135
  /** The fixture name carries its extension — the name IS the file. */
1014
1136
  function requireExtension(name, subject) {
1015
1137
  if (!/\.[A-Za-z0-9]+$/u.test(name)) throw new Error(`toMatch("${name}"): the extension is part of the name and is required for ${subject} subjects (e.g. "help.txt").`);
@@ -1091,94 +1213,7 @@ function isIterable(value) {
1091
1213
  return value !== null && value !== void 0 && typeof value[Symbol.iterator] === "function";
1092
1214
  }
1093
1215
  //#endregion
1094
- //#region src/specification/facets/_common/result/grep.ts
1095
- /**
1096
- * Extract text blocks from output that contain a pattern.
1097
- * Splits by blank lines (how linter/compiler output is structured),
1098
- * returns only blocks matching the pattern.
1099
- *
1100
- * @example
1101
- * expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
1102
- * expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
1103
- */
1104
- function grep(output, pattern) {
1105
- return output.replaceAll(/\u001B\[[0-9;]*m/gu, "").split(/\n\s*\n/u).filter((block) => block.includes(pattern)).join("\n\n");
1106
- }
1107
- //#endregion
1108
- //#region src/specification/facets/_common/result/text.ts
1109
- const ANSI_RE = /\u001B\[[0-9;]*m/gu;
1110
- /** Strip ANSI escape sequences from a string. */
1111
- function stripAnsiCodes(value) {
1112
- return value.replace(ANSI_RE, "");
1113
- }
1114
- /**
1115
- * Read-only accessor for a captured text handle — THE universal one: stdout,
1116
- * stderr, container logs, and file text all surface as a `TextAccessor`.
1117
- *
1118
- * Backed by a primitive string, exposed via {@link text}, but also provides
1119
- * `toString()` / `valueOf()` for string coercion, so common patterns like
1120
- * `String(result.stdout)` and template-literal interpolation still work.
1121
- *
1122
- * ANSI escape sequences are stripped by default before every comparison
1123
- * (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
1124
- * an escape hatch for application noise not covered by the `{{token}}`
1125
- * grammar.
1126
- *
1127
- * Text operations are **closed** over the type: `.grep(pattern)` returns a
1128
- * `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
1129
- * context and the `transform`, so results are chainable
1130
- * (`result.stdout.grep(a).grep(b)`) and snapshot-able
1131
- * (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
1132
- *
1133
- * Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
1134
- * and `expect(result.stdout).toMatch('name.txt')` (resolved against
1135
- * `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
1136
- */
1137
- var TextAccessor = class TextAccessor {
1138
- /** @internal Ref-capture scope shared by the current spec execution. */
1139
- captures;
1140
- /** @internal Stream label used in failure messages ("stdout" / "stderr"). */
1141
- streamName;
1142
- /** @internal Test-file directory — fixture resolution root for matchers. */
1143
- testDir;
1144
- /** The raw captured text (never transformed, ANSI preserved). */
1145
- text;
1146
- /** @internal Normaliser applied before comparisons, never to fixtures. */
1147
- transform;
1148
- constructor(value, streamName, testDir, options = {}) {
1149
- this.text = value;
1150
- this.streamName = streamName;
1151
- this.testDir = testDir;
1152
- this.transform = options.transform;
1153
- this.captures = options.captures ?? new CaptureScope();
1154
- }
1155
- /** @internal The text as compared by matchers — ANSI stripped, transform applied. */
1156
- get comparableText() {
1157
- const stripped = stripAnsiCodes(this.text);
1158
- return this.transform ? this.transform(stripped) : stripped;
1159
- }
1160
- /**
1161
- * Keep only the blank-line-separated blocks of the text that contain
1162
- * `pattern` (how linter/compiler output is structured), returned as a new
1163
- * `TextAccessor` — the same subject type, so the result is chainable and
1164
- * snapshot-able. The scalpel for probing large tool outputs; snapshot the
1165
- * whole surface by default, reach for `.grep()` for targeted checks.
1166
- */
1167
- grep(pattern) {
1168
- return new TextAccessor(grep(this.text, pattern), this.streamName, this.testDir, {
1169
- captures: this.captures,
1170
- transform: this.transform
1171
- });
1172
- }
1173
- toString() {
1174
- return this.text;
1175
- }
1176
- valueOf() {
1177
- return this.text;
1178
- }
1179
- };
1180
- //#endregion
1181
- //#region src/specification/facets/website/ambiguity.ts
1216
+ //#region src/model/elements/ambiguity.ts
1182
1217
  /**
1183
1218
  * The ambiguity refusal — CONVENTIONS W3.
1184
1219
  *
@@ -1232,8 +1267,9 @@ const CONTEXT_LANDMARKS = {
1232
1267
  };
1233
1268
  /**
1234
1269
  * Render a descriptor as the source that would build it — `link('Articles')`,
1235
- * `within(navigation(), link('Articles', { exact: true }))`. The error speaks
1236
- * the vocabulary the author wrote, never playwright's.
1270
+ * `within(navigation(), link('Articles', { exact: false }))`. The error speaks
1271
+ * the vocabulary the author wrote, never playwright's — so the option appears
1272
+ * only when the AUTHOR stated one, whichever way.
1237
1273
  */
1238
1274
  function formatElement(element) {
1239
1275
  const bare = formatBare(element);
@@ -1242,7 +1278,7 @@ function formatElement(element) {
1242
1278
  function formatBare(element) {
1243
1279
  const args = [];
1244
1280
  if (element.name !== void 0) args.push(JSON.stringify(element.name));
1245
- if (element.exact) args.push("{ exact: true }");
1281
+ if (element.exact !== void 0) args.push(`{ exact: ${String(element.exact)} }`);
1246
1282
  return `${CONSTRUCTORS[element.kind]}(${args.join(", ")})`;
1247
1283
  }
1248
1284
  /** `1. <a href="/articles">Articles</a> in <nav>` — one evidence line per candidate. */
@@ -1278,7 +1314,7 @@ function formatFixes(element, matches) {
1278
1314
  const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
1279
1315
  fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${formatBare(element)})` : `scope it within(${first[0]}, ${formatBare(element)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
1280
1316
  }
1281
- if (!element.exact && element.name !== void 0) {
1317
+ if (element.exact === false && element.name !== void 0) {
1282
1318
  const remaining = matches.filter((match) => match.text === element.name).length;
1283
1319
  if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatBare({
1284
1320
  ...element,
@@ -1318,11 +1354,109 @@ function describeAmbiguity(options) {
1318
1354
  "Disambiguate with one of:",
1319
1355
  ...fixes,
1320
1356
  "",
1321
- "Docs: docs/14-website.md#designating-exactly-one-element (CONVENTIONS W3)"
1357
+ "Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
1322
1358
  ].join("\n");
1323
1359
  }
1324
1360
  //#endregion
1325
- //#region src/integrations/vitest-browser/vitest-browser.adapter.ts
1361
+ //#region src/model/elements/substring-warning.ts
1362
+ /**
1363
+ * The transitional warning for a name that matches only as a SUBSTRING.
1364
+ *
1365
+ * Until 16.0 a descriptor's name matched any element whose accessible name
1366
+ * CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
1367
+ * test could pass for years against the wrong element without anyone noticing,
1368
+ * because the wrong element was usually next to the right one. Exact is the
1369
+ * default now.
1370
+ *
1371
+ * A lint rule cannot carry this: whether a name matched whole or in part is a
1372
+ * fact about a RUN, and a verdict fed by a previous run's artefact is not
1373
+ * deterministic. So it is a runtime warning, printed by the adapter that did
1374
+ * the looking, at the moment it has both answers in hand — nothing found under
1375
+ * exact, something found under substring.
1376
+ *
1377
+ * It is printed ONCE per descriptor per process: a scenario that clicks the
1378
+ * same link in ten tests has one thing to fix, not ten lines to read.
1379
+ */
1380
+ /** How long the window lasts, stated in the message itself. */
1381
+ const WINDOW = "through 16.x";
1382
+ /** Descriptors already reported in this process. */
1383
+ const reported = /* @__PURE__ */ new Set();
1384
+ /** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
1385
+ function identityOf(element) {
1386
+ const own = `${element.kind}:${element.name ?? ""}`;
1387
+ return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
1388
+ }
1389
+ /**
1390
+ * Report that `element` designated nothing as a whole name, but would have
1391
+ * designated something as a substring. Returns whether a line was printed,
1392
+ * which is what the adapters' own tests assert on.
1393
+ */
1394
+ function warnSubstringOnly(element, where, found) {
1395
+ const identity = identityOf(element);
1396
+ if (reported.has(identity)) return false;
1397
+ reported.add(identity);
1398
+ const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
1399
+ console.warn(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
1400
+ return true;
1401
+ }
1402
+ /**
1403
+ * The descriptor to retry with, LEVEL by level, or `undefined` when nothing
1404
+ * would change.
1405
+ *
1406
+ * Every level of the chain is asked the same question the target is asked —
1407
+ * `within(button('Proof of authorship'), content('Verified'))` against a
1408
+ * button named "Proof of authorship Verified" failed on the SCOPE, and a
1409
+ * warning that only ever widened the target printed nothing at all. The chain
1410
+ * is walked outside-in and rebuilt as it goes, since an inner level's answer
1411
+ * is meaningless under a scope that designates nothing.
1412
+ *
1413
+ * A level that states an `exact` of its own is left alone: an author who wrote
1414
+ * `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
1415
+ * was already exact.
1416
+ *
1417
+ * Asked BEFORE the action, not after it fails: a verb waits its whole
1418
+ * actionability budget, which is the test's own budget, so a retry afterwards
1419
+ * never runs — the test is already over. The probes are counts, which wait for
1420
+ * nothing, and the common case (the name matches whole) costs one per level.
1421
+ */
1422
+ async function widenedForWindow(element, where, probe) {
1423
+ const chain = [];
1424
+ for (let level = element; level; level = level.scope) chain.unshift(level);
1425
+ let rebuilt;
1426
+ let widened = false;
1427
+ for (const level of chain) {
1428
+ const candidate = {
1429
+ ...level,
1430
+ ...rebuilt === void 0 ? {} : { scope: rebuilt }
1431
+ };
1432
+ const loose = await widenedLevel(candidate, where, probe);
1433
+ widened ||= loose !== void 0;
1434
+ rebuilt = loose ?? candidate;
1435
+ }
1436
+ return widened ? rebuilt : void 0;
1437
+ }
1438
+ /**
1439
+ * The one level, widened and warned about, or `undefined` when it designates
1440
+ * something already, states its own `exact`, carries no name, or would
1441
+ * designate nothing either way.
1442
+ */
1443
+ async function widenedLevel(candidate, where, probe) {
1444
+ if (candidate.exact !== void 0 || candidate.name === void 0) return;
1445
+ try {
1446
+ if (await probe.count(candidate) > 0) return;
1447
+ const loose = {
1448
+ ...candidate,
1449
+ exact: false
1450
+ };
1451
+ if (await probe.count(loose) !== 1) return;
1452
+ warnSubstringOnly(candidate, where, await probe.nameOf(loose));
1453
+ return loose;
1454
+ } catch {
1455
+ return;
1456
+ }
1457
+ }
1458
+ //#endregion
1459
+ //#region src/seams/vitest-browser/vitest-browser.adapter.ts
1326
1460
  /**
1327
1461
  * The browser-mode adapter — the component facet's half of the one element
1328
1462
  * vocabulary.
@@ -1343,6 +1477,29 @@ const MAX_REPORTED_MATCHES = 10;
1343
1477
  /** The landmark ancestors the ambiguity evidence names. */
1344
1478
  const LANDMARKS = "nav, header, footer, main, aside, section, form, [role]";
1345
1479
  /**
1480
+ * The roles a FIELD can carry — what `field('Name')` designates.
1481
+ *
1482
+ * A field is named by its ACCESSIBLE name, not by the text of the label
1483
+ * element that happens to wrap it: `<label><span>Channel</span><select>…` has
1484
+ * a label whose text carries every option of the select, while the control's
1485
+ * accessible name is `Channel` — which is what the ARIA tree shows and what a
1486
+ * screen reader reads. Resolving by label text alone made the ordinary
1487
+ * wrapping-label form unreachable under the 16.0 whole-name default.
1488
+ *
1489
+ * The label-text locator stays in the union for the controls ARIA gives no
1490
+ * role at all (a date or colour input), which nothing else would find.
1491
+ */
1492
+ const FIELD_ROLES = [
1493
+ "checkbox",
1494
+ "combobox",
1495
+ "radio",
1496
+ "searchbox",
1497
+ "slider",
1498
+ "spinbutton",
1499
+ "switch",
1500
+ "textbox"
1501
+ ];
1502
+ /**
1346
1503
  * Translate a user-facing descriptor into a Browser Mode locator, resolving
1347
1504
  * `scope` outside-in so `within(navigation(), link('X'))` searches the nav
1348
1505
  * subtree. No `.first()` anywhere: designating exactly one element is
@@ -1351,9 +1508,16 @@ const LANDMARKS = "nav, header, footer, main, aside, section, form, [role]";
1351
1508
  */
1352
1509
  function locate(root, element) {
1353
1510
  const scope = element.scope ? locate(root, element.scope) : root;
1354
- const exact = element.exact ?? false;
1511
+ const exact = element.exact ?? true;
1355
1512
  const name = element.name ?? "";
1356
- if (element.kind === "field") return scope.getByLabelText(name, { exact });
1513
+ if (element.kind === "field") {
1514
+ let field = scope.getByLabelText(name, { exact });
1515
+ for (const role of FIELD_ROLES) field = field.or(scope.getByRole(role, {
1516
+ exact,
1517
+ name
1518
+ }));
1519
+ return field;
1520
+ }
1357
1521
  if (element.kind === "testId") return scope.getByTestId(name);
1358
1522
  if (element.kind === "text") return scope.getByText(name, { exact });
1359
1523
  return scope.getByRole(element.kind, element.name === void 0 ? { exact } : {
@@ -1385,6 +1549,14 @@ function ambiguousLevel(element) {
1385
1549
  for (const level of chain.slice(0, -1)) if (locate(page, level).elements().length > 1) return level;
1386
1550
  return element;
1387
1551
  }
1552
+ /** What the transitional window may ask of the mounted page. */
1553
+ const PROBE = {
1554
+ count: async (element) => await Promise.resolve(locate(page, element).elements().length),
1555
+ nameOf: async (element) => {
1556
+ const [match] = candidates(locate(page, element));
1557
+ return await Promise.resolve(match?.accessibleName ?? match?.text);
1558
+ }
1559
+ };
1388
1560
  /**
1389
1561
  * Run one visitor action, turning "more than one match" into the W3 refusal.
1390
1562
  * The ambiguous LEVEL is identified first: a scope matching several landmarks
@@ -1392,12 +1564,13 @@ function ambiguousLevel(element) {
1392
1564
  * wrong descriptor.
1393
1565
  */
1394
1566
  async function act(element, action) {
1395
- const locator = locate(page, element);
1567
+ const target = await widenedForWindow(element, globalThis.location.href, PROBE) ?? element;
1568
+ const locator = locate(page, target);
1396
1569
  try {
1397
1570
  return await action(locator);
1398
1571
  } catch (error) {
1399
1572
  if (locator.elements().length <= 1) throw error;
1400
- const culprit = ambiguousLevel(element);
1573
+ const culprit = ambiguousLevel(target);
1401
1574
  const matches = candidates(locate(page, culprit));
1402
1575
  const url = globalThis.location.href;
1403
1576
  throw new AmbiguousElementError(describeAmbiguity({
@@ -1584,7 +1757,7 @@ function mountDom(mount) {
1584
1757
  };
1585
1758
  }
1586
1759
  //#endregion
1587
- //#region src/integrations/vitest-browser/golden.ts
1760
+ //#region src/seams/vitest-browser/golden.ts
1588
1761
  /** Update mode crosses the seam as a provided value, never as an env read. */
1589
1762
  function updating() {
1590
1763
  return inject("update") === true;
@@ -1679,7 +1852,7 @@ const nodeOnlyClass = (name) => function NodeOnly() {
1679
1852
  return refuse(name);
1680
1853
  };
1681
1854
  //#endregion
1682
- //#region src/integrations/vitest-browser/page-runtime.ts
1855
+ //#region src/seams/vitest-browser/page-runtime.ts
1683
1856
  /**
1684
1857
  * The runner, as the page reaches it.
1685
1858
  *
@@ -1728,7 +1901,7 @@ function projectWrapper() {
1728
1901
  return typeof wrap === "function" ? wrap : void 0;
1729
1902
  }
1730
1903
  //#endregion
1731
- //#region src/specification/facets/component/rendered-text.ts
1904
+ //#region src/facets/component/rendered-text.ts
1732
1905
  /**
1733
1906
  * A stream captured INSIDE the page.
1734
1907
  *
@@ -1743,7 +1916,7 @@ var RenderedText = class extends TextAccessor {
1743
1916
  capturedInPage = true;
1744
1917
  };
1745
1918
  //#endregion
1746
- //#region src/specification/facets/component/component.result.ts
1919
+ //#region src/facets/component/component.result.ts
1747
1920
  /**
1748
1921
  * What a render resolves to — the component as the page saw it, after the
1749
1922
  * scenario ran.
@@ -1773,7 +1946,7 @@ var RenderResult = class {
1773
1946
  }
1774
1947
  };
1775
1948
  //#endregion
1776
- //#region src/specification/facets/component/component.chain.ts
1949
+ //#region src/facets/component/component.chain.ts
1777
1950
  /** The console methods a render records; everything else is left alone. */
1778
1951
  const CONSOLE_METHODS = [
1779
1952
  "debug",
@@ -1936,91 +2109,11 @@ function chainOf(state) {
1936
2109
  /** The handle a component test imports. */
1937
2110
  const component = chainOf(EMPTY);
1938
2111
  //#endregion
1939
- //#region src/specification/facets/_common/result/json.ts
1940
- /**
1941
- * Read-only accessor for a JSON payload parsed from a text stream (stdout).
1942
- *
1943
- * Lazily parses the JSON on first use; parse errors are deferred until the
1944
- * value is read so that callers can still read the raw stream text without
1945
- * triggering a throw.
1946
- *
1947
- * ANSI escape sequences are stripped by default before `JSON.parse`
1948
- * (CONVENTIONS D6). When a `transform` is configured on the spec runner, it
1949
- * runs on the stripped text — an escape hatch for application noise not
1950
- * covered by the `{{token}}` grammar.
1951
- *
1952
- * Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
1953
- * (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
1954
- */
1955
- var JsonAccessor = class {
1956
- /** @internal Ref-capture scope shared by the current spec execution. */
1957
- captures;
1958
- /** @internal */
1959
- rawText;
1960
- /** @internal Test-file directory — fixture resolution root for matchers. */
1961
- testDir;
1962
- /** @internal */
1963
- transform;
1964
- constructor(rawText, testDir, transform, captures) {
1965
- this.rawText = rawText;
1966
- this.testDir = testDir;
1967
- this.transform = transform;
1968
- this.captures = captures ?? new CaptureScope();
1969
- }
1970
- /**
1971
- * The parsed JSON value. Throws if the text is not valid JSON.
1972
- *
1973
- * `Value` is `unknown` unless the subject that produced this accessor
1974
- * knows better — `.call<T>()` does, and hands its T straight through so a
1975
- * one-field reading needs no cast in the test.
1976
- */
1977
- get value() {
1978
- const stripped = stripAnsiCodes(this.rawText);
1979
- const source = this.transform ? this.transform(stripped) : stripped;
1980
- try {
1981
- return JSON.parse(source);
1982
- } catch {
1983
- const preview = source.slice(0, 200);
1984
- throw new Error(`stdout is not valid JSON: ${preview}`);
1985
- }
1986
- }
1987
- };
1988
- //#endregion
1989
- //#region src/specification/facets/_common/result/table.ts
1990
- /**
1991
- * Read-only accessor for a database table after a specification run.
1992
- *
1993
- * Assertions go through `expect()` (async — they query the database):
1994
- *
1995
- * await expect(result.table('users', { database: 'db' })).toMatchRows({
1996
- * columns: ['name'],
1997
- * rows: [['Alice']],
1998
- * });
1999
- * await expect(result.table('users', { database: 'db' })).toBeEmpty();
2000
- */
2001
- var TableAccessor = class {
2002
- /** @internal Ref-capture scope shared by the current spec execution. */
2003
- captures;
2004
- /** @internal */
2005
- db;
2006
- /** The table name this accessor reads. */
2007
- name;
2008
- constructor(name, db, captures) {
2009
- this.name = name;
2010
- this.db = db;
2011
- this.captures = captures ?? new CaptureScope();
2012
- }
2013
- /** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
2014
- async query(columns) {
2015
- return await this.db.query(this.name, columns);
2016
- }
2017
- };
2018
- //#endregion
2019
- //#region src/specification/facets/website/elements.ts
2112
+ //#region src/model/elements/elements.ts
2020
2113
  const named = (kind) => (name, options) => ({
2021
2114
  kind,
2022
2115
  name,
2023
- ...options?.exact ? { exact: true } : {}
2116
+ ...options?.exact === void 0 ? {} : { exact: options.exact }
2024
2117
  });
2025
2118
  /** A button (or element with the button role), by accessible name. */
2026
2119
  const button = named("button");
@@ -2040,7 +2133,7 @@ const testId = (id) => ({
2040
2133
  const landmark = (kind) => (name, options) => ({
2041
2134
  kind,
2042
2135
  ...name === void 0 ? {} : { name },
2043
- ...options?.exact ? { exact: true } : {}
2136
+ ...options?.exact === void 0 ? {} : { exact: options.exact }
2044
2137
  });
2045
2138
  /** The `banner` landmark — the page header. */
2046
2139
  const banner = landmark("banner");
@@ -2158,7 +2251,7 @@ const valued = (element, value) => ({
2158
2251
  value
2159
2252
  });
2160
2253
  //#endregion
2161
- //#region src/specification/contracts/filters.ts
2254
+ //#region src/model/contracts/filters.ts
2162
2255
  /** Does an observed text satisfy a declared {@link TextFilter}? */
2163
2256
  function matchesText(filter, actual) {
2164
2257
  if (filter === void 0) return true;
@@ -2167,7 +2260,7 @@ function matchesText(filter, actual) {
2167
2260
  return structuralEquals(filter, actual, new CaptureScope());
2168
2261
  }
2169
2262
  //#endregion
2170
- //#region src/integrations/anthropic/anthropic.ts
2263
+ //#region src/seams/anthropic/anthropic.ts
2171
2264
  const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
2172
2265
  function matchesFilter(body, filter) {
2173
2266
  if (!matchesText(filter.model, body?.model ?? "")) return false;
@@ -2258,7 +2351,7 @@ const anthropic = {
2258
2351
  }
2259
2352
  };
2260
2353
  //#endregion
2261
- //#region src/specification/contracts/http.ts
2354
+ //#region src/model/contracts/http.ts
2262
2355
  function wrapJson(data) {
2263
2356
  return {
2264
2357
  status: 200,
@@ -2447,7 +2540,7 @@ const http = {
2447
2540
  }
2448
2541
  };
2449
2542
  //#endregion
2450
- //#region src/integrations/openai/openai.ts
2543
+ //#region src/seams/openai/openai.ts
2451
2544
  const OPENAI_CHAT_URL = "https://api.openai.com/v1/chat/completions";
2452
2545
  const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
2453
2546
  function matchesChatFilter(body, filter) {
@@ -2604,19 +2697,12 @@ const openai = {
2604
2697
  }
2605
2698
  };
2606
2699
  //#endregion
2607
- //#region src/vitest/mock-of.ts
2700
+ //#region src/model/doubles/mock-of.ts
2608
2701
  function mockOf(options) {
2609
2702
  return options?.deep === false ? mock() : mockDeep();
2610
2703
  }
2611
2704
  //#endregion
2612
- //#region src/vitest/mock-of-date.ts
2613
- /**
2614
- * Freeze or reset the global Date for deterministic time-dependent tests.
2615
- * Wraps the `mockdate` package.
2616
- */
2617
- const mockOfDate = MockDatePackage;
2618
- //#endregion
2619
- //#region src/vitest/clock.ts
2705
+ //#region src/model/clock/clock.ts
2620
2706
  /** The instant `iso` names, or a refusal that says what an instant looks like. */
2621
2707
  function instantOf(method, iso) {
2622
2708
  const at = new Date(iso);
@@ -2683,7 +2769,7 @@ const clock = {
2683
2769
  }
2684
2770
  };
2685
2771
  //#endregion
2686
- //#region src/specification/assertions/required.ts
2772
+ //#region src/model/assertions/required.ts
2687
2773
  /**
2688
2774
  * The value, or a failure that says what was missing and why it mattered.
2689
2775
  *
@@ -2703,7 +2789,7 @@ function required(value, why) {
2703
2789
  return value;
2704
2790
  }
2705
2791
  //#endregion
2706
- //#region src/specification/assertions/wait-until.ts
2792
+ //#region src/model/assertions/wait-until.ts
2707
2793
  const DEFAULT_TIMEOUT_MS = 5e3;
2708
2794
  const DEFAULT_INTERVAL_MS = 50;
2709
2795
  /**
@@ -2768,21 +2854,6 @@ const ProcessHandle = nodeOnlyClass("ProcessHandle");
2768
2854
  const postgres = () => refuse("postgres()");
2769
2855
  const redis = () => refuse("redis()");
2770
2856
  const sqlite = () => refuse("sqlite()");
2771
- const ContainerAccessor = nodeOnlyClass("ContainerAccessor");
2772
- const findContainersByLabel = () => refuse("findContainersByLabel()");
2773
- const inspectContainer = () => refuse("inspectContainer()");
2774
- const removeContainers = () => refuse("removeContainers()");
2775
- const Orchestrator = nodeOnlyClass("Orchestrator");
2776
- const BaseResult = nodeOnlyClass("BaseResult");
2777
- const CallResult = nodeOnlyClass("CallResult");
2778
- const CliResult = nodeOnlyClass("CliResult");
2779
- const DirectoryAccessor = nodeOnlyClass("DirectoryAccessor");
2780
- const FetchResult = nodeOnlyClass("FetchResult");
2781
- const FilesystemAccessor = nodeOnlyClass("FilesystemAccessor");
2782
- const HttpResult = nodeOnlyClass("HttpResult");
2783
- const PageResult = nodeOnlyClass("PageResult");
2784
- const ResponseAccessor = nodeOnlyClass("ResponseAccessor");
2785
- const ScreenResult = nodeOnlyClass("ScreenResult");
2786
2857
  /**
2787
2858
  * `text()` is real here: only its anchor differs. Under node the accessor
2788
2859
  * remembers the caller's directory so a golden resolves against it; in a page
@@ -2793,4 +2864,4 @@ function text(value) {
2793
2864
  return new TextAccessor(value, "text", "");
2794
2865
  }
2795
2866
  //#endregion
2796
- export { BaseResult, CallResult, CliResult, ContainerAccessor, DirectoryAccessor, FetchResult, FilesystemAccessor, HttpResult, JsonAccessor, Matcher, Orchestrator, PageResult, ProcessHandle, ResponseAccessor, ScreenResult, TableAccessor, TextAccessor, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineContract, defineContracts, dialog, disabled, enabled, field, findContainersByLabel, focused, form, heading, http, inspectContainer, intercept, link, listitem, main, match, mockOf, mockOfDate, navigation, openai, option, postgres, process, redis, region, removeContainers, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
2867
+ export { Matcher, ProcessHandle, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineContract, defineContracts, dialog, disabled, enabled, field, focused, form, heading, http, intercept, link, listitem, main, match, mockOf, navigation, openai, option, postgres, process, redis, region, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };