@jterrazz/test 16.1.1 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +2 -2980
  3. package/dist/browser/services.js +2 -0
  4. package/dist/browser/stubs.js +2 -0
  5. package/dist/checker.d.ts +6 -1
  6. package/dist/checker.js +4534 -2628
  7. package/dist/config.d.ts +40 -0
  8. package/dist/config.js +309 -0
  9. package/dist/in-place.global-setup.d.ts +11 -0
  10. package/dist/in-place.global-setup.js +21 -0
  11. package/dist/index.d.ts +1653 -3004
  12. package/dist/index.js +5226 -3845
  13. package/dist/mobile-session.global-setup.d.ts +14 -0
  14. package/dist/mobile-session.global-setup.js +31 -0
  15. package/dist/oxlint.cjs +6069 -5634
  16. package/dist/oxlint.d.cts +43 -7212
  17. package/dist/oxlint.d.ts +43 -7212
  18. package/dist/oxlint.js +6127 -5693
  19. package/dist/route-coverage.global-setup.d.ts +42 -0
  20. package/dist/route-coverage.global-setup.js +2 -0
  21. package/dist/services.d.ts +3 -0
  22. package/dist/services.global-setup.d.ts +43 -0
  23. package/dist/services.global-setup.js +2 -0
  24. package/dist/services.js +597 -0
  25. package/dist/shared-appium.adapter.js +428 -0
  26. package/dist/shared-artifacts.js +32 -0
  27. package/dist/shared-chapter.js +7 -0
  28. package/dist/shared-container.port.d.ts +31 -0
  29. package/dist/shared-derived-projects.js +58 -0
  30. package/dist/shared-device.port.d.ts +189 -0
  31. package/dist/shared-exchange.js +447 -0
  32. package/dist/shared-in-place.js +172 -0
  33. package/dist/shared-index.browser.js +3191 -0
  34. package/dist/shared-index.services.d.ts +346 -0
  35. package/dist/shared-index.stubs.d.ts +287 -0
  36. package/dist/shared-json-shape.js +17 -0
  37. package/dist/shared-landmarks.js +71 -0
  38. package/dist/shared-ledger.js +68 -0
  39. package/dist/shared-literate-plugin.js +183 -0
  40. package/dist/shared-manifest.js +1321 -0
  41. package/dist/shared-pause.js +11 -0
  42. package/dist/shared-peer.js +88 -0
  43. package/dist/shared-playwright.adapter.js +330 -0
  44. package/dist/shared-process.adapter.js +74 -0
  45. package/dist/shared-refusal.js +35 -0
  46. package/dist/shared-resolve.js +24 -0
  47. package/dist/shared-route-coverage.global-setup.js +187 -0
  48. package/dist/shared-scenario-failure.js +198 -0
  49. package/dist/shared-serve.js +365 -0
  50. package/dist/shared-service-images.js +10 -0
  51. package/dist/shared-services.global-setup.js +179 -0
  52. package/dist/shared-session-ledger.js +301 -0
  53. package/dist/shared-setup-entry.js +12 -0
  54. package/dist/shared-source-scan.js +133 -0
  55. package/dist/shared-spec-document.js +365 -0
  56. package/dist/shared-specifications.js +26 -0
  57. package/dist/shared-stream-body.js +130 -0
  58. package/dist/shared-stub-server.js +190 -0
  59. package/dist/shared-temp-root.global-setup.js +80 -0
  60. package/dist/shared-test-baseline.js +26 -0
  61. package/dist/shared-test-section.js +58 -0
  62. package/dist/shared-testcontainers.adapter.js +203 -0
  63. package/dist/shared-tokens.js +55 -0
  64. package/dist/shared-yaml-document.js +119 -0
  65. package/dist/stubs.d.ts +2 -0
  66. package/dist/stubs.js +657 -0
  67. package/dist/temp-root.global-setup.d.ts +7 -0
  68. package/dist/temp-root.global-setup.js +2 -0
  69. package/dist/vitest.d.ts +97 -163
  70. package/dist/vitest.js +373 -525
  71. package/docs/README.md +82 -36
  72. package/docs/concepts/architecture.md +118 -0
  73. package/docs/concepts/conventions.md +333 -0
  74. package/docs/guides/api-specs.md +374 -0
  75. package/docs/guides/cli-specs.md +564 -0
  76. package/docs/guides/component-specs.md +252 -0
  77. package/docs/guides/develop.md +94 -0
  78. package/docs/guides/integration-specs.md +181 -0
  79. package/docs/guides/job-specs.md +180 -0
  80. package/docs/guides/lint-specs.md +174 -0
  81. package/docs/guides/mobile-specs.md +319 -0
  82. package/docs/guides/module-tests.md +149 -0
  83. package/docs/guides/release.md +43 -0
  84. package/docs/guides/spec-documents.md +189 -0
  85. package/docs/guides/website-specs.md +413 -0
  86. package/docs/reference/assertions.md +345 -0
  87. package/docs/reference/glossary.md +20 -0
  88. package/docs/reference/linting.md +483 -0
  89. package/docs/reference/services.md +236 -0
  90. package/docs/reference/stubs.md +498 -0
  91. package/docs/reference/time-and-doubles.md +137 -0
  92. package/docs/reference/tokens.md +258 -0
  93. package/docs/reference/visitor.md +221 -0
  94. package/docs/start/getting-started.md +187 -0
  95. package/docs/start/set-up-a-project.md +214 -0
  96. package/package.json +50 -30
  97. package/schema/spec.schema.json +43 -17
  98. package/dist/appium.adapter.js +0 -489
  99. package/dist/artifacts.js +0 -32
  100. package/dist/fixtures.js +0 -110
  101. package/dist/literate.js +0 -1747
  102. package/dist/match.js +0 -804
  103. package/dist/playwright.adapter.js +0 -438
  104. package/dist/server.js +0 -160
  105. package/dist/stream.js +0 -321
  106. package/dist/substring-warning.js +0 -282
  107. package/docs/01-architecture.md +0 -95
  108. package/docs/02-developing.md +0 -351
  109. package/docs/03-testing.md +0 -495
  110. package/docs/04-operating.md +0 -101
  111. package/docs/05-module-tests.md +0 -152
  112. package/docs/06-integration.md +0 -180
  113. package/docs/07-component.md +0 -246
  114. package/docs/08-website.md +0 -404
  115. package/docs/09-mobile.md +0 -270
  116. package/docs/10-api.md +0 -264
  117. package/docs/11-jobs.md +0 -157
  118. package/docs/12-cli.md +0 -677
  119. package/docs/13-elements.md +0 -248
  120. package/docs/14-assertions.md +0 -314
  121. package/docs/15-tokens.md +0 -253
  122. package/docs/16-contracts.md +0 -438
  123. package/docs/17-services.md +0 -188
  124. package/docs/18-conventions.md +0 -332
  125. package/docs/19-linting.md +0 -512
  126. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  127. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  128. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  129. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  130. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  131. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  132. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  133. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  134. package/docs/decisions/_template.md +0 -22
  135. package/docs/reference/classes/Matcher.md +0 -120
  136. package/docs/reference/classes/ProcessHandle.md +0 -368
  137. package/docs/reference/functions/defineContract.md +0 -39
  138. package/docs/reference/functions/defineContracts.md +0 -36
  139. package/docs/reference/functions/disabled.md +0 -28
  140. package/docs/reference/functions/enabled.md +0 -20
  141. package/docs/reference/functions/focused.md +0 -26
  142. package/docs/reference/functions/mockOf.md +0 -79
  143. package/docs/reference/functions/postgres.md +0 -31
  144. package/docs/reference/functions/process.md +0 -33
  145. package/docs/reference/functions/redis.md +0 -28
  146. package/docs/reference/functions/required.md +0 -41
  147. package/docs/reference/functions/selected.md +0 -26
  148. package/docs/reference/functions/sqlite.md +0 -33
  149. package/docs/reference/functions/testId.md +0 -20
  150. package/docs/reference/functions/text.md +0 -40
  151. package/docs/reference/functions/valued.md +0 -28
  152. package/docs/reference/functions/waitUntil.md +0 -46
  153. package/docs/reference/functions/within.md +0 -29
  154. package/docs/reference/index.md +0 -196
  155. package/docs/reference/interfaces/BaseResult.md +0 -87
  156. package/docs/reference/interfaces/CallResult.md +0 -160
  157. package/docs/reference/interfaces/CaptureScope.md +0 -82
  158. package/docs/reference/interfaces/CliResult.md +0 -253
  159. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  160. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  161. package/docs/reference/interfaces/FetchResult.md +0 -180
  162. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  163. package/docs/reference/interfaces/HttpResult.md +0 -126
  164. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  165. package/docs/reference/interfaces/PageResult.md +0 -356
  166. package/docs/reference/interfaces/RenderResult.md +0 -72
  167. package/docs/reference/interfaces/RenderedText.md +0 -110
  168. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  169. package/docs/reference/interfaces/ScreenResult.md +0 -128
  170. package/docs/reference/interfaces/TableAccessor.md +0 -26
  171. package/docs/reference/interfaces/TextAccessor.md +0 -96
  172. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  173. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  174. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  175. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  176. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  177. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  178. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  179. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  180. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  181. package/docs/reference/type-aliases/CliEnv.md +0 -11
  182. package/docs/reference/type-aliases/CliHandle.md +0 -80
  183. package/docs/reference/type-aliases/CliInput.md +0 -36
  184. package/docs/reference/type-aliases/CliOutput.md +0 -46
  185. package/docs/reference/type-aliases/CliPort.md +0 -58
  186. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  187. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  188. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  189. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  190. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  191. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  192. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  193. package/docs/reference/type-aliases/Contract.md +0 -68
  194. package/docs/reference/type-aliases/ContractInput.md +0 -13
  195. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  196. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  197. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  198. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  199. package/docs/reference/type-aliases/Contracts.md +0 -48
  200. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  201. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  202. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  203. package/docs/reference/type-aliases/DevicePort.md +0 -51
  204. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  205. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  206. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  207. package/docs/reference/type-aliases/DomMount.md +0 -21
  208. package/docs/reference/type-aliases/ElementKind.md +0 -27
  209. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  210. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  211. package/docs/reference/type-aliases/ElementRef.md +0 -128
  212. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  213. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  214. package/docs/reference/type-aliases/HonoApp.md +0 -31
  215. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  216. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  217. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  218. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  219. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  220. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  221. package/docs/reference/type-aliases/Intercept.md +0 -19
  222. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  223. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  224. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  225. package/docs/reference/type-aliases/JobHandle.md +0 -34
  226. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  227. package/docs/reference/type-aliases/JobsResult.md +0 -16
  228. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  229. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  230. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  231. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  232. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  233. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  234. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  235. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  236. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  237. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  238. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  239. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  240. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  241. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  242. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  243. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  244. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  245. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  246. package/docs/reference/type-aliases/MockPort.md +0 -10
  247. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  248. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  249. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  250. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  251. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  252. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  253. package/docs/reference/type-aliases/ServerPort.md +0 -36
  254. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  255. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  256. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  257. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  258. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  259. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  260. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  261. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  262. package/docs/reference/type-aliases/SpecKind.md +0 -8
  263. package/docs/reference/type-aliases/SpecRun.md +0 -108
  264. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  265. package/docs/reference/type-aliases/SpecStream.md +0 -32
  266. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  267. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  268. package/docs/reference/type-aliases/SseEvent.md +0 -58
  269. package/docs/reference/type-aliases/StreamBody.md +0 -61
  270. package/docs/reference/type-aliases/TextFilter.md +0 -22
  271. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  272. package/docs/reference/type-aliases/Visitor.md +0 -215
  273. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  274. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  275. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  276. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  277. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  278. package/docs/reference/variables/anthropic.md +0 -19
  279. package/docs/reference/variables/banner.md +0 -10
  280. package/docs/reference/variables/button.md +0 -10
  281. package/docs/reference/variables/clock.md +0 -16
  282. package/docs/reference/variables/complementary.md +0 -10
  283. package/docs/reference/variables/component.md +0 -10
  284. package/docs/reference/variables/content.md +0 -10
  285. package/docs/reference/variables/contentinfo.md +0 -10
  286. package/docs/reference/variables/dialog.md +0 -10
  287. package/docs/reference/variables/field.md +0 -10
  288. package/docs/reference/variables/form.md +0 -10
  289. package/docs/reference/variables/heading.md +0 -10
  290. package/docs/reference/variables/http.md +0 -40
  291. package/docs/reference/variables/intercept.md +0 -11
  292. package/docs/reference/variables/link.md +0 -10
  293. package/docs/reference/variables/listitem.md +0 -10
  294. package/docs/reference/variables/main.md +0 -10
  295. package/docs/reference/variables/match.md +0 -8
  296. package/docs/reference/variables/navigation.md +0 -10
  297. package/docs/reference/variables/openai.md +0 -21
  298. package/docs/reference/variables/option.md +0 -17
  299. package/docs/reference/variables/region.md +0 -10
  300. package/docs/reference/variables/row.md +0 -10
  301. package/docs/reference/variables/search.md +0 -10
  302. package/docs/reference/variables/specification.md +0 -8
  303. package/docs/reference/variables/status.md +0 -10
  304. package/docs/reference/variables/table.md +0 -10
package/dist/stream.js DELETED
@@ -1,321 +0,0 @@
1
- import { t as CaptureScope } from "./match.js";
2
- import { A as placeholderPatternSource, D as hasPlaceholders, P as textEquals } from "./literate.js";
3
- //#region src/model/contracts/contract.ts
4
- /**
5
- * Declare one contract. Identity function — its value is the enforced shape
6
- * and the naming convention:
7
- *
8
- * @example
9
- * // specs/api/reports/contracts/openai/classify-article.ts
10
- * import { defineContract, openai } from '@jterrazz/test';
11
- *
12
- * export default defineContract({
13
- * request: openai.responses({ user: PROMPT, tools: ['classify'] }),
14
- * response: openai.reply({ categories: ['TECH'] }),
15
- * });
16
- *
17
- * // Dynamic — the response is computed from the observed request:
18
- * export default defineContract({
19
- * request: http.post('https://api.example.com/echo'),
20
- * response: (request) => http.json({ received: request.body }),
21
- * });
22
- */
23
- function defineContract(contract) {
24
- return contract;
25
- }
26
- /** Is this a {@link Contracts} composite (rather than a contract or a list)? */
27
- function isContracts(value) {
28
- return typeof value === "object" && value !== null && Array.isArray(value.contracts) && typeof value.with === "function";
29
- }
30
- /** Is this a single {@link Contract} (rather than a bare request half)? */
31
- function isContract(value) {
32
- return typeof value === "object" && value !== null && "request" in value && "response" in value;
33
- }
34
- /**
35
- * The route a contract claims — method + the DECLARED url source. Two
36
- * contracts share a route when they would be written on the same line of a
37
- * declaration: same method, same url pattern (`.source` for a RegExp).
38
- */
39
- function routeKeyOf(contract) {
40
- const { method, url } = contract.request;
41
- return url instanceof RegExp ? `${method} re:${url.source}` : `${method} str:${url}`;
42
- }
43
- /** Human-readable route of a contract — used in every failure message. */
44
- function describeRoute(contract) {
45
- const { method, url } = contract.request;
46
- return `${method === "*" ? "ANY" : method} ${url instanceof RegExp ? String(url) : url}`;
47
- }
48
- /** Flatten contracts, lists, and composites into one ordered list. */
49
- function flatten(items) {
50
- const flat = [];
51
- for (const item of items) if (Array.isArray(item)) flat.push(...flatten(item));
52
- else if (isContracts(item)) flat.push(...item.contracts);
53
- else flat.push(item);
54
- return flat;
55
- }
56
- /**
57
- * Compose contracts into the artifact a test imports — it's contracts all the
58
- * way down: a composite may extend contracts, lists, and other composites,
59
- * recursively, order preserved.
60
- *
61
- * `.with(...)` derives a scenario: contracts whose route the overrides claim
62
- * are removed from the base, and the overrides are prepended. Under
63
- * first-match selection a more specific override (`/articles/gone-1`) also
64
- * wins over a generic base route (`/articles/{{uuid}}`) it does not replace.
65
- *
66
- * @example
67
- * // contracts/newsroom.contracts.ts
68
- * export default defineContracts(events, articleById);
69
- * export const withArticleGone = (id: string) =>
70
- * newsroom.with(articleGone(id));
71
- */
72
- function defineContracts(...items) {
73
- const contracts = Object.freeze(flatten(items));
74
- return {
75
- contracts,
76
- with(...overrides) {
77
- const added = flatten(overrides);
78
- const claimed = new Set(added.map(routeKeyOf));
79
- const kept = contracts.filter((contract) => !claimed.has(routeKeyOf(contract)));
80
- return defineContracts([...added, ...kept]);
81
- }
82
- };
83
- }
84
- /** Normalize any accepted input into a flat contract list. */
85
- function contractsOf(input) {
86
- return flatten([input]);
87
- }
88
- //#endregion
89
- //#region src/model/contracts/types.ts
90
- /**
91
- * The marker that tells a STREAMED body from a value that happens to be an
92
- * object. A body reaches three different engines (msw's server, msw's worker,
93
- * the `node:http` stub) and each of them serialises "an object" as JSON, so a
94
- * stream declared as a plain value arrives as `{}`. The tag is what each
95
- * engine recognises before it reaches for `JSON.stringify`.
96
- */
97
- const STREAM_BODY = "@jterrazz/test:stream";
98
- /** Is this body a declared stream rather than a value to serialise? */
99
- function isStreamBody(body) {
100
- return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
101
- }
102
- //#endregion
103
- //#region src/model/timing/pause.ts
104
- /**
105
- * One answer to "wait" — the only `setTimeout` the model owns.
106
- *
107
- * Two things in the framework genuinely have to wait: the spacing between the
108
- * pieces of a declared stream, and the poll behind `waitUntil`. Everything
109
- * else that waits is a spec doing it by hand, which is what J2 refuses. Stated
110
- * once so there is one place to look when time stops behaving.
111
- */
112
- async function pause(ms) {
113
- await new Promise((resolve) => {
114
- setTimeout(resolve, ms);
115
- });
116
- }
117
- //#endregion
118
- //#region src/model/contracts/queue.ts
119
- /**
120
- * The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
121
- * Both engines consume it: the MSW integration on api/jobs chains, and the
122
- * declared stub backend on website/mobile chains.
123
- *
124
- * Selection: the FIRST contract in declaration order that matches the observed
125
- * request and is not exhausted. A contract with no `times` is unlimited (a
126
- * re-render or a retry replays it); `times: n` is spent after n serves, so an
127
- * ordered sequence is written as finite contracts before an unlimited tail:
128
- * `[error500 ×3, ok]`.
129
- *
130
- * Strictness (CONVENTIONS D7): when nothing matches — including "everything
131
- * that matches is exhausted" — the engine answers 501, records the request,
132
- * and the chain fails with {@link ContractQueue.unmatchedError}. A chain that
133
- * declared zero contracts is not guarded at all (unchanged boundary).
134
- */
135
- /** Base used to parse origin-relative observed URLs (`/events?x=1`). */
136
- const RELATIVE_BASE = "http://contract.invalid";
137
- /** Split a declared url into its base (origin+path, or path) and query halves.
138
- * Never through `new URL()`, which would percent-encode `{{token}}` braces. */
139
- function splitDeclared(url) {
140
- const separator = url.indexOf("?");
141
- if (separator === -1) return {
142
- base: url,
143
- query: new URLSearchParams()
144
- };
145
- return {
146
- base: url.slice(0, separator),
147
- query: new URLSearchParams(url.slice(separator + 1))
148
- };
149
- }
150
- /** Decode percent-escapes, falling back to the raw text on malformed input. */
151
- function safeDecode(text) {
152
- try {
153
- return decodeURIComponent(text);
154
- } catch {
155
- return text;
156
- }
157
- }
158
- /** Token-aware comparison of one declared value against an observed one. */
159
- function valueMatches(declared, observed) {
160
- return textEquals(declared, observed, new CaptureScope());
161
- }
162
- /**
163
- * Does an observed URL satisfy a declared one? A RegExp tests the full URL; a
164
- * PATH FORM (`/articles/{{uuid}}`) ignores the origin; an absolute string
165
- * compares origin+pathname. In both string forms `{{token}}` segments compare
166
- * structurally and declared query params are a SUBSET of the observed ones.
167
- */
168
- function urlMatches(declared, observed) {
169
- if (declared instanceof RegExp) return declared.test(observed);
170
- let url;
171
- try {
172
- url = new URL(observed, RELATIVE_BASE);
173
- } catch {
174
- return false;
175
- }
176
- const { base, query } = splitDeclared(declared);
177
- const observedBase = base.startsWith("/") ? url.pathname : `${url.origin}${url.pathname}`;
178
- const decodedBase = base.startsWith("/") ? safeDecode(url.pathname) : `${url.origin}${safeDecode(url.pathname)}`;
179
- if (!valueMatches(base, observedBase) && !valueMatches(base, decodedBase)) return false;
180
- for (const key of new Set(query.keys())) {
181
- const observedValues = url.searchParams.getAll(key);
182
- if (!query.getAll(key).every((value) => observedValues.some((actual) => valueMatches(value, actual)))) return false;
183
- }
184
- return true;
185
- }
186
- /**
187
- * Does an observed request satisfy a contract? Method (`*` matches any), URL,
188
- * then the contract's own `match` predicate (headers/query/body filters).
189
- */
190
- function contractMatches(contract, request) {
191
- const { match, method, url } = contract.request;
192
- if (method !== "*" && method.toUpperCase() !== request.method.toUpperCase()) return false;
193
- if (!urlMatches(url, request.url)) return false;
194
- return match ? match(request) : true;
195
- }
196
- /**
197
- * The URL pattern an out-of-process router (MSW) must register so the request
198
- * REACHES the queue. Deliberately no narrower than {@link contractMatches} —
199
- * the predicate above stays the authority on what actually matches.
200
- */
201
- function routePatternOf(declared) {
202
- if (declared instanceof RegExp) return declared;
203
- const { base } = splitDeclared(declared);
204
- if (base.startsWith("/")) return new RegExp(String.raw`^https?:\/\/[^/?#]+${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
205
- if (hasPlaceholders(base)) return new RegExp(String.raw`^${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
206
- return base;
207
- }
208
- /** How a declared contract reads in a failure message: spent, served, or silent. */
209
- function stateOf({ contract, served }) {
210
- const { times } = contract;
211
- if (times !== void 0 && served >= times) return ` (exhausted after ${times})`;
212
- return served === 0 ? "" : ` (served ${served} time(s))`;
213
- }
214
- var ContractQueue = class {
215
- entries;
216
- constructor(contracts) {
217
- this.entries = contracts.map((contract) => ({
218
- contract,
219
- served: 0
220
- }));
221
- }
222
- /** How many contracts the chain declared. */
223
- get size() {
224
- return this.entries.length;
225
- }
226
- /**
227
- * Routes to register with an out-of-process router, deduped by pattern —
228
- * two contracts on the same URL share one handler and one queue.
229
- */
230
- get routes() {
231
- const routes = /* @__PURE__ */ new Map();
232
- for (const { contract } of this.entries) {
233
- const url = routePatternOf(contract.request.url);
234
- const key = String(url);
235
- const route = routes.get(key) ?? {
236
- methods: [],
237
- url
238
- };
239
- if (!route.methods.includes(contract.request.method)) route.methods.push(contract.request.method);
240
- routes.set(key, route);
241
- }
242
- return [...routes.values()];
243
- }
244
- /** The declared routes, in order — the enumeration a 501 body carries. */
245
- declaredRoutes() {
246
- return this.entries.map(({ contract }) => describeRoute(contract));
247
- }
248
- /**
249
- * Serve the first contract in declaration order that matches and is not
250
- * exhausted, or null when nothing matches (including "everything that
251
- * matches is spent") — the strict-violation signal.
252
- */
253
- take(request) {
254
- for (const entry of this.entries) {
255
- const { times } = entry.contract;
256
- if (times !== void 0 && entry.served >= times) continue;
257
- if (!contractMatches(entry.contract, request)) continue;
258
- entry.served += 1;
259
- return entry.contract;
260
- }
261
- return null;
262
- }
263
- /**
264
- * The chain-end failure for contracts declared `required: true` that were
265
- * never requested (or not exactly `times` times), or null when every
266
- * requirement held.
267
- */
268
- requiredError() {
269
- const unmet = [];
270
- for (const { contract, served } of this.entries) {
271
- if (!contract.required) continue;
272
- const { times } = contract;
273
- if (times === void 0) {
274
- if (served === 0) unmet.push(` - ${describeRoute(contract)} — declared and required but never requested`);
275
- } else if (served !== times) unmet.push(` - ${describeRoute(contract)} — declared required with times: ${times} but was requested ${served} time(s)`);
276
- }
277
- if (unmet.length === 0) return null;
278
- return /* @__PURE__ */ new Error(`Required contract(s) never satisfied during the chain:\n${unmet.join("\n")}\nA required contract states the call MUST happen — either the code no longer makes it, or the contract no longer describes it.`);
279
- }
280
- /**
281
- * The strict failure for a request that matched no contract (CONVENTIONS
282
- * D7): method + URL of the offending request, plus every declared contract
283
- * and how often it was served.
284
- */
285
- unmatchedError(method, url) {
286
- const declared = this.entries.length === 0 ? " (no contracts declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.contract)}${stateOf(entry)}`).join("\n");
287
- return /* @__PURE__ */ new Error(`Unmatched outgoing HTTP request during spec: ${method} ${url}\nDeclared contracts:\n${declared}\nEvery outgoing request of a chain that declares contracts must match one — add a contract for it (or raise its \`times\` when it is exhausted).`);
288
- }
289
- };
290
- //#endregion
291
- //#region src/model/contracts/stream.ts
292
- /**
293
- * A declared stream, made real — ONE serialisation for all three engines.
294
- *
295
- * msw's server and msw's worker take a `ReadableStream` body verbatim
296
- * (https://mswjs.io/docs/http/mocking-responses/streaming) and the `node:http`
297
- * stub pipes one into its response, so what "arriving in pieces" MEANS —
298
- * the order, the spacing, the close after the last chunk — is stated here and
299
- * nowhere else. A second copy of it would be a second answer to "what does the
300
- * subject read", and the facets would drift.
301
- */
302
- /**
303
- * The pieces of `body`, in order, `delayBetweenChunks` milliseconds apart,
304
- * closing after the last one.
305
- */
306
- function toReadableStream(body) {
307
- const encoder = new TextEncoder();
308
- let index = 0;
309
- return new ReadableStream({ async pull(controller) {
310
- const chunk = body.chunks[index];
311
- if (chunk === void 0) {
312
- controller.close();
313
- return;
314
- }
315
- if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
316
- index += 1;
317
- controller.enqueue(encoder.encode(chunk));
318
- } });
319
- }
320
- //#endregion
321
- export { isStreamBody as a, defineContracts as c, STREAM_BODY as i, isContract as l, ContractQueue as n, contractsOf as o, pause as r, defineContract as s, toReadableStream as t, isContracts as u };
@@ -1,282 +0,0 @@
1
- //#region src/model/elements/ambiguity.ts
2
- /**
3
- * The ambiguity refusal — CONVENTIONS W3.
4
- *
5
- * A visit descriptor must designate exactly one element. Acting on "the
6
- * first match" is the failure mode this module exists to prevent: the spec
7
- * keeps passing while the visitor clicks something else, and nothing ever
8
- * reports it. So when a descriptor matches several elements the framework
9
- * refuses, and the refusal has to carry everything needed to fix it without
10
- * opening a browser — the descriptor in the caller's own vocabulary, every
11
- * candidate, and the concrete rewrites that would resolve it.
12
- *
13
- * Pure string building: it takes captured data and returns a message, so the
14
- * wording is unit-testable and the browser integration stays a thin adapter.
15
- */
16
- /** `kind` → the constructor that builds it, so errors echo the caller's source. */
17
- const CONSTRUCTORS = {
18
- banner: "banner",
19
- button: "button",
20
- complementary: "complementary",
21
- contentinfo: "contentinfo",
22
- dialog: "dialog",
23
- field: "field",
24
- form: "form",
25
- heading: "heading",
26
- link: "link",
27
- listitem: "listitem",
28
- main: "main",
29
- navigation: "navigation",
30
- option: "option",
31
- region: "region",
32
- row: "row",
33
- search: "search",
34
- status: "status",
35
- table: "table",
36
- testId: "testId",
37
- text: "content"
38
- };
39
- /** The landmark a context string maps back to, for a copy-pasteable suggestion. */
40
- const CONTEXT_LANDMARKS = {
41
- aside: "complementary()",
42
- banner: "banner()",
43
- complementary: "complementary()",
44
- contentinfo: "contentinfo()",
45
- footer: "contentinfo()",
46
- form: "form()",
47
- header: "banner()",
48
- main: "main()",
49
- nav: "navigation()",
50
- navigation: "navigation()",
51
- search: "search()"
52
- };
53
- /**
54
- * Render a descriptor as the source that would build it — `link('Articles')`,
55
- * `within(navigation(), link('Articles', { exact: false }))`. The error speaks
56
- * the vocabulary the author wrote, never playwright's — so the option appears
57
- * only when the AUTHOR stated one, whichever way.
58
- */
59
- function formatElement(element) {
60
- const bare = formatBare(element);
61
- return element.scope ? `within(${formatElement(element.scope)}, ${bare})` : bare;
62
- }
63
- function formatBare(element) {
64
- const args = [];
65
- if (element.name !== void 0) args.push(JSON.stringify(element.name));
66
- if (element.exact !== void 0) args.push(`{ exact: ${String(element.exact)} }`);
67
- return `${CONSTRUCTORS[element.kind]}(${args.join(", ")})`;
68
- }
69
- /** `1. <a href="/articles">Articles</a> in <nav>` — one evidence line per candidate. */
70
- function formatMatch(match, index) {
71
- const attribute = match.detail ? ` ${quoteDetail(match)}` : "";
72
- const named = match.accessibleName ? ` named ${JSON.stringify(match.accessibleName)}` : "";
73
- const context = match.context ? ` in <${match.context}>` : "";
74
- return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${named}${context}`;
75
- }
76
- function quoteDetail(match) {
77
- return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
78
- }
79
- /** Each landmark the candidates sit in, and how many of them it holds. */
80
- function landmarkCounts(matches) {
81
- const counts = /* @__PURE__ */ new Map();
82
- for (const match of matches) {
83
- const landmark = match.context === void 0 ? void 0 : CONTEXT_LANDMARKS[match.context];
84
- if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
85
- }
86
- return [...counts].toSorted(([, a], [, b]) => a - b);
87
- }
88
- /**
89
- * The rewrites worth offering, in the order they should be tried. Scoping is
90
- * always available; `exact` only when it would actually narrow the set, and
91
- * the count it would leave is stated so a still-ambiguous suggestion never
92
- * reads as a fix.
93
- */
94
- function formatFixes(element, matches) {
95
- const fixes = [];
96
- const narrowing = landmarkCounts(matches).filter(([, count]) => count < matches.length);
97
- if (!element.scope) {
98
- const [first, ...rest] = narrowing;
99
- const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
100
- 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}`);
101
- }
102
- if (element.exact === false && element.name !== void 0) {
103
- const remaining = matches.filter((match) => match.text === element.name).length;
104
- if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatBare({
105
- ...element,
106
- exact: true
107
- })} [leaves ${remaining} of ${matches.length}]`);
108
- }
109
- fixes.push("other element a heading(), button() or field() may name one thing where this does not");
110
- return fixes;
111
- }
112
- /**
113
- * The W3 refusal. A distinct class so the visit wrapper can recognize an
114
- * already-complete message and let it through instead of nesting it inside
115
- * `visit scenario failed: …`, which would print the whole thing twice.
116
- */
117
- var AmbiguousElementError = class extends Error {
118
- name = "AmbiguousElementError";
119
- };
120
- /**
121
- * Build the refusal thrown when a descriptor matches more than one element.
122
- *
123
- * @param options.element The descriptor as the caller wrote it.
124
- * @param options.matches Every candidate, in DOM order (already truncated).
125
- * @param options.url The page the visitor was on when it happened.
126
- */
127
- function describeAmbiguity(options) {
128
- const { element, matches, url } = options;
129
- const fixes = formatFixes(element, matches).map((fix) => ` • ${fix}`);
130
- return [
131
- `Ambiguous element: ${formatElement(element)} matched ${matches.length} elements on ${url}.`,
132
- "",
133
- "A spec must designate exactly one element. Acting on the first match would let",
134
- "this test keep passing while the visitor interacts with something else.",
135
- "",
136
- "Matched:",
137
- ...matches.map((match, index) => formatMatch(match, index)),
138
- "",
139
- "Disambiguate with one of:",
140
- ...fixes,
141
- "",
142
- "Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
143
- ].join("\n");
144
- }
145
- //#endregion
146
- //#region src/model/elements/substring-warning.ts
147
- /**
148
- * The transitional warning for a name that matches only as a SUBSTRING.
149
- *
150
- * Until 16.0 a descriptor's name matched any element whose accessible name
151
- * CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
152
- * test could pass for years against the wrong element without anyone noticing,
153
- * because the wrong element was usually next to the right one. Exact is the
154
- * default now.
155
- *
156
- * A lint rule cannot carry this: whether a name matched whole or in part is a
157
- * fact about a RUN, and a verdict fed by a previous run's artefact is not
158
- * deterministic. So it is a runtime warning, printed by the adapter that did
159
- * the looking, at the moment it has both answers in hand — nothing found under
160
- * exact, something found under substring.
161
- *
162
- * It is printed ONCE per descriptor per process: a scenario that clicks the
163
- * same link in ten tests has one thing to fix, not ten lines to read.
164
- */
165
- /** How long the window lasts, stated in the message itself. */
166
- const WINDOW = "through 16.x";
167
- /** The node side's printer — the default every adapter but the page's uses. */
168
- function toStandardError(line) {
169
- if (typeof process === "undefined") {
170
- console.warn(line);
171
- return;
172
- }
173
- process.stderr.write(`${line}\n`);
174
- }
175
- /** Descriptors already reported in this process. */
176
- const reported = /* @__PURE__ */ new Set();
177
- /** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
178
- function identityOf(element) {
179
- const own = `${element.kind}:${element.name ?? ""}`;
180
- return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
181
- }
182
- /**
183
- * Report that `element` designated nothing as a whole name, but would have
184
- * designated something as a substring. Returns whether a line was printed,
185
- * which is what the adapters' own tests assert on.
186
- */
187
- async function warnSubstringOnly(element, where, found, print = toStandardError) {
188
- const identity = identityOf(element);
189
- if (reported.has(identity)) return false;
190
- reported.add(identity);
191
- const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
192
- await print(`@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.`);
193
- return true;
194
- }
195
- /**
196
- * How long a descriptor is given to ARRIVE before the window concludes it is
197
- * absent — the budget `settles` spends and no more.
198
- *
199
- * A count is one observation, and a verb called right after a navigating
200
- * `click()` takes it on the page being LEFT. The window may not decide
201
- * "nothing matches this whole name" on that reading: the destination has not
202
- * been parsed yet. Long enough for a navigation to commit, far short of the
203
- * verb's own 30 s, and paid only when there is a substring to widen to.
204
- */
205
- const WINDOW_SETTLE_MS = 5e3;
206
- /**
207
- * The descriptor to retry with, LEVEL by level, or `undefined` when nothing
208
- * would change.
209
- *
210
- * Every level of the chain is asked the same question the target is asked —
211
- * `within(button('Proof of authorship'), content('Verified'))` against a
212
- * button named "Proof of authorship Verified" failed on the SCOPE, and a
213
- * warning that only ever widened the target printed nothing at all. The chain
214
- * is walked outside-in and rebuilt as it goes, since an inner level's answer
215
- * is meaningless under a scope that designates nothing.
216
- *
217
- * A level that states an `exact` of its own is left alone: an author who wrote
218
- * `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
219
- * was already exact.
220
- *
221
- * Asked BEFORE the action, not after it fails: a verb waits its whole
222
- * actionability budget, which is the test's own budget, so a retry afterwards
223
- * never runs — the test is already over. The common case (the name matches
224
- * whole) costs one count per level, which waits for nothing.
225
- */
226
- async function widenedForWindow(element, where, probe) {
227
- const chain = [];
228
- for (let level = element; level; level = level.scope) chain.unshift(level);
229
- let rebuilt;
230
- let widened = false;
231
- for (const level of chain) {
232
- const candidate = {
233
- ...level,
234
- ...rebuilt === void 0 ? {} : { scope: rebuilt }
235
- };
236
- const loose = await widenedLevel(candidate, where, probe);
237
- widened ||= loose !== void 0;
238
- rebuilt = loose ?? candidate;
239
- }
240
- return widened ? rebuilt : void 0;
241
- }
242
- /**
243
- * The one level, widened and warned about, or `undefined` when it designates
244
- * something already, states its own `exact`, carries no name, would designate
245
- * nothing either way, or turns out to be exact on the surface that ARRIVES.
246
- *
247
- * The last is the navigation case, and it is the reason the verdict is not two
248
- * counts: a `see()` fired right after a navigating `click()` counts the page
249
- * being left, so a descriptor that is perfectly exact on the DESTINATION read
250
- * as substring-only — non-deterministically, since which page each count
251
- * landed on was a race. A widening is therefore only decided once the exact
252
- * descriptor has been given the settle budget and still designates nothing.
253
- * The tell of the old fault was the printed name equalling the name written.
254
- */
255
- async function widenedLevel(candidate, where, probe) {
256
- if (candidate.exact !== void 0 || candidate.name === void 0) return;
257
- try {
258
- if (await probe.count(candidate) > 0) return;
259
- const loose = {
260
- ...candidate,
261
- exact: false
262
- };
263
- if (await probe.count(loose) !== 1) return;
264
- if (!alreadyJudged(candidate) && await probe.settles(candidate)) return;
265
- await warnSubstringOnly(candidate, where, await probe.nameOf(loose), probe.print);
266
- return loose;
267
- } catch {
268
- return;
269
- }
270
- }
271
- /**
272
- * Has this descriptor already been judged substring-only in this run?
273
- *
274
- * The verdict is made once: a scenario that clicks the same link in ten tests
275
- * pays the settle budget on the first of them and is widened straight away on
276
- * the nine after it.
277
- */
278
- function alreadyJudged(element) {
279
- return reported.has(identityOf(element));
280
- }
281
- //#endregion
282
- export { describeAmbiguity as a, AmbiguousElementError as i, warnSubstringOnly as n, formatElement as o, widenedForWindow as r, WINDOW_SETTLE_MS as t };