@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
@@ -0,0 +1,365 @@
1
+ import { a as isPair, c as nodeItems, f as yamlSyntaxErrors, i as isNode, l as nodePairs, o as isScalar, r as isMap, s as isSeq, u as parseYamlSource } from "./shared-yaml-document.js";
2
+ import { GROUND_FIXTURES, isGroundFolder } from "@jterrazz/typescript/rules";
3
+ //#region lint/kit/project/relative-path.ts
4
+ function isRelativePath(value) {
5
+ return value.startsWith("./") || value.startsWith("../");
6
+ }
7
+ //#endregion
8
+ //#region lint/kit/project/dedent.ts
9
+ function indentOf(line) {
10
+ return /^[\t ]*/u.exec(line)?.[0].length ?? 0;
11
+ }
12
+ /** A text opening on a line break loses that break, the closing line's whitespace and the shared indentation. */
13
+ function dedent(text) {
14
+ if (!text.startsWith("\n")) return text;
15
+ const lines = text.slice(1).split("\n");
16
+ if (lines.at(-1)?.trim() === "") lines[lines.length - 1] = "";
17
+ const indents = lines.filter((line) => line.trim() !== "").map((line) => indentOf(line));
18
+ const common = indents.length === 0 ? 0 : Math.min(...indents);
19
+ return lines.map((line) => line.slice(common)).join("\n");
20
+ }
21
+ //#endregion
22
+ //#region lint/kit/project/spec-grammar.ts
23
+ const SPEC_EXTENSION = ".spec.yaml";
24
+ const DOCUMENT_KEYS = [
25
+ "description",
26
+ "fixture",
27
+ "env",
28
+ "serve",
29
+ "runs"
30
+ ];
31
+ const RUN_KEYS = [
32
+ "command",
33
+ "stdin",
34
+ "timeout",
35
+ "waitFor",
36
+ "exit",
37
+ "stdout",
38
+ "stderr",
39
+ "files"
40
+ ];
41
+ const FILE_CONTENT_KEYS = ["contains", "equals"];
42
+ const FILE_STATES = ["absent", "exists"];
43
+ const PRESENCE = {
44
+ document: [{
45
+ keys: ["description"],
46
+ law: "each",
47
+ refusal: () => "missing \"description:\": every document is one test, and this is its title"
48
+ }, {
49
+ keys: ["runs"],
50
+ law: "each",
51
+ refusal: () => "missing \"runs:\": description and runs are mandatory"
52
+ }],
53
+ files: [{
54
+ keys: FILE_CONTENT_KEYS,
55
+ law: "some",
56
+ refusal: (path) => `files: ${path} asserts nothing: give it contains: or equals:`
57
+ }],
58
+ run: [{
59
+ keys: ["command", "exit"],
60
+ law: "each",
61
+ refusal: (_subject, key) => `a run is missing "${key}:": command and exit are mandatory`
62
+ }, {
63
+ keys: ["waitFor"],
64
+ law: "apart",
65
+ refusal: () => "waitFor: and stdin: cannot share a run: the pattern is watched on a process nobody closed the input of",
66
+ with: "stdin"
67
+ }]
68
+ };
69
+ var SpecSyntaxError = class extends Error {
70
+ line;
71
+ reason;
72
+ constructor(fileName, line, message) {
73
+ super(`${fileName}:${line}: ${message}`);
74
+ this.line = line;
75
+ this.reason = message;
76
+ this.name = "SpecSyntaxError";
77
+ }
78
+ };
79
+ //#endregion
80
+ //#region lint/kit/project/spec-node.ts
81
+ function fail(context, node, message) {
82
+ throw new SpecSyntaxError(context.fileName, lineOf(context, node), message);
83
+ }
84
+ /** A pair sits on its key's line; a node the parser never placed, on the first. */
85
+ function lineOf(context, node) {
86
+ const placed = isPair(node) ? node.key : node;
87
+ return isNode(placed) && placed.range ? context.source.lineAt(placed.range[0]) : 1;
88
+ }
89
+ function contentLine(context, node) {
90
+ const start = lineOf(context, node);
91
+ return isScalar(node) && (node.type ?? "").startsWith("BLOCK") ? start + 1 : start;
92
+ }
93
+ function requireMap(context, node, what) {
94
+ if (!isMap(node)) fail(context, node ?? void 0, `${what} must be a mapping of keys to values`);
95
+ return node;
96
+ }
97
+ function closedPairs(context, map, allowed, what) {
98
+ const pairs = [];
99
+ for (const pair of nodePairs(map)) {
100
+ const key = scalarString(pair.key);
101
+ if (key === null) fail(context, pair, `${what}: a key must be a plain word`);
102
+ if (!allowed.includes(key)) fail(context, pair, `unknown key "${key}:" in ${what}; known keys: ${allowed.join(", ")}`);
103
+ pairs.push(pair);
104
+ }
105
+ return pairs;
106
+ }
107
+ function keyed(pairs) {
108
+ return new Map(pairs.map((pair) => [scalarString(pair.key) ?? "", pair]));
109
+ }
110
+ function holdPresence(context, found, { line, shape, subject = "" }) {
111
+ const at = (message, where = line) => {
112
+ throw new SpecSyntaxError(context.fileName, where, message);
113
+ };
114
+ for (const law of PRESENCE[shape]) if (law.law === "each") {
115
+ const missing = law.keys.find((key) => !found.has(key));
116
+ if (missing !== void 0) at(law.refusal(subject, missing));
117
+ } else if (law.law === "some") {
118
+ if (!law.keys.some((key) => found.has(key))) at(law.refusal(subject));
119
+ } else if (found.has(law.with)) {
120
+ const beside = law.keys.find((key) => found.has(key));
121
+ if (beside !== void 0) at(law.refusal(subject, beside), lineOf(context, found.get(beside)));
122
+ }
123
+ }
124
+ function held(found, key) {
125
+ const pair = found.get(key);
126
+ if (pair === void 0) throw new Error(`"${key}:" is required by a presence law and was not found`);
127
+ return pair;
128
+ }
129
+ function scalarString(node) {
130
+ return isScalar(node) && typeof node.value === "string" ? node.value : null;
131
+ }
132
+ function requireString(context, node, what) {
133
+ const value = scalarString(node);
134
+ if (value === null) fail(context, node, `${what} must be text`);
135
+ return value;
136
+ }
137
+ function requireInteger(context, node, what) {
138
+ const value = isScalar(node) ? node.value : void 0;
139
+ if (!Number.isInteger(value)) fail(context, node, `${what} must be a literal integer`);
140
+ return Number(value);
141
+ }
142
+ function stringList(context, node, what) {
143
+ if (isSeq(node)) return nodeItems(node).map((item) => ({
144
+ line: lineOf(context, item),
145
+ value: requireString(context, item, `each entry of ${what}`)
146
+ }));
147
+ return [{
148
+ line: lineOf(context, node),
149
+ value: requireString(context, node, what)
150
+ }];
151
+ }
152
+ //#endregion
153
+ //#region lint/kit/project/spec-fields.ts
154
+ function readStream(context, node, what) {
155
+ return {
156
+ line: contentLine(context, node),
157
+ text: requireString(context, node, what)
158
+ };
159
+ }
160
+ function readEnv(context, node) {
161
+ return stringList(context, node, "env:").map(({ line, value }) => {
162
+ const separator = value.indexOf("=");
163
+ if (separator === -1) return {
164
+ kind: "set",
165
+ line,
166
+ name: value
167
+ };
168
+ if (separator === 0) throw new SpecSyntaxError(context.fileName, line, `env: "${value}" has no variable name before the "="`);
169
+ return {
170
+ key: value.slice(0, separator),
171
+ kind: "pair",
172
+ line,
173
+ value: value.slice(separator + 1)
174
+ };
175
+ });
176
+ }
177
+ function readServeEntry(context, node) {
178
+ const name = scalarString(node);
179
+ if (name !== null) return {
180
+ env: {},
181
+ line: lineOf(context, node),
182
+ name
183
+ };
184
+ const map = requireMap(context, node, "a serve: entry");
185
+ if (map.items.length !== 1) fail(context, node, "a serve: entry names ONE server: `- <name>: { KEY: value }`");
186
+ const [pair] = nodePairs(map);
187
+ if (pair === void 0) fail(context, node, "a serve: entry names ONE server: `- <name>: { KEY: value }`");
188
+ const serverName = scalarString(pair.key);
189
+ if (serverName === null) fail(context, pair, "a serve: entry must name its server with a plain word");
190
+ const env = {};
191
+ const envMap = requireMap(context, pair.value, `serve: ${serverName}`);
192
+ for (const entry of nodePairs(envMap)) {
193
+ const key = scalarString(entry.key);
194
+ if (key === null) fail(context, entry, `serve: ${serverName} is no variable name, which must be a plain word`);
195
+ env[key] = requireString(context, entry.value, `serve: ${serverName}.${key}`);
196
+ }
197
+ return {
198
+ env,
199
+ line: lineOf(context, pair),
200
+ name: serverName
201
+ };
202
+ }
203
+ function readServe(context, node) {
204
+ return (isSeq(node) ? nodeItems(node) : [node]).map((item) => readServeEntry(context, item));
205
+ }
206
+ function readFiles(context, node) {
207
+ const map = requireMap(context, node, "files:");
208
+ return nodePairs(map).map((pair) => {
209
+ const path = scalarString(pair.key);
210
+ if (path === null) fail(context, pair, "files: each key is a path relative to the working directory");
211
+ const line = lineOf(context, pair);
212
+ const state = scalarString(pair.value);
213
+ if (state !== null) {
214
+ const kind = FILE_STATES.find((known) => known === state);
215
+ if (kind === void 0) fail(context, pair, `files: "${path}: ${state}" is not a state: write ${FILE_STATES.join(" or ")}, or a { ${FILE_CONTENT_KEYS.join(", ")} } assertion`);
216
+ return {
217
+ kind,
218
+ line,
219
+ path
220
+ };
221
+ }
222
+ const pairs = closedPairs(context, requireMap(context, pair.value, `files: ${path}`), FILE_CONTENT_KEYS, `files: ${path}`);
223
+ holdPresence(context, keyed(pairs), {
224
+ line,
225
+ shape: "files",
226
+ subject: path
227
+ });
228
+ const contains = [];
229
+ let equals = null;
230
+ for (const entry of pairs) {
231
+ if (scalarString(entry.key) === "contains") {
232
+ if (isSeq(entry.value)) for (const item of nodeItems(entry.value)) contains.push(readStream(context, item, `files: ${path}.contains`));
233
+ else contains.push(readStream(context, entry.value, `files: ${path}.contains`));
234
+ continue;
235
+ }
236
+ equals = readStream(context, entry.value, `files: ${path}.equals`);
237
+ }
238
+ return {
239
+ contains,
240
+ equals,
241
+ kind: "content",
242
+ line,
243
+ path
244
+ };
245
+ });
246
+ }
247
+ function readRun(context, node, isLast) {
248
+ const found = keyed(closedPairs(context, requireMap(context, node, "each entry of runs:"), RUN_KEYS, "a run"));
249
+ holdPresence(context, found, {
250
+ line: lineOf(context, node),
251
+ shape: "run"
252
+ });
253
+ const commandPair = found.get("command");
254
+ const exitPair = found.get("exit");
255
+ if (commandPair === void 0 || exitPair === void 0) fail(context, node, "a run states its command: and its exit:");
256
+ const command = requireString(context, commandPair.value, "command:");
257
+ const waitForPair = found.get("waitFor");
258
+ if (waitForPair !== void 0 && !isLast) fail(context, waitForPair, "waitFor: is only allowed on the LAST run, since a long-running command ends the session");
259
+ const stdout = found.get("stdout");
260
+ const stderr = found.get("stderr");
261
+ const stdin = found.get("stdin");
262
+ const timeout = found.get("timeout");
263
+ const files = found.get("files");
264
+ return {
265
+ command,
266
+ commandLine: lineOf(context, commandPair),
267
+ exitCode: requireInteger(context, exitPair.value, "exit:"),
268
+ files: files === void 0 ? [] : readFiles(context, files.value),
269
+ stderr: stderr === void 0 ? null : readStream(context, stderr.value, "stderr:"),
270
+ stdin: stdin === void 0 ? null : requireString(context, stdin.value, "stdin:"),
271
+ stdout: stdout === void 0 ? null : readStream(context, stdout.value, "stdout:"),
272
+ timeout: timeout === void 0 ? null : requireInteger(context, timeout.value, "timeout:"),
273
+ waitFor: waitForPair === void 0 ? null : requireString(context, waitForPair.value, "waitFor:")
274
+ };
275
+ }
276
+ function groundOf(path) {
277
+ return path.split("/").find((segment) => isGroundFolder(segment));
278
+ }
279
+ /** A fixture is laid from `__fixtures__/`, a file or a whole directory. */
280
+ function readFixtures(context, node) {
281
+ return stringList(context, node, "fixture:").map(({ line, value }) => {
282
+ if (!isRelativePath(value)) throw new SpecSyntaxError(context.fileName, line, `fixture: "${value}" is not a relative path: name it from this document (./... or ../...)`);
283
+ const found = groundOf(value);
284
+ if (found !== GROUND_FIXTURES) {
285
+ const where = `./${GROUND_FIXTURES}/ beside the document, or in the ${GROUND_FIXTURES}/ of the lowest folder the leaves share (specs/${GROUND_FIXTURES}/ across specifications)`;
286
+ throw new SpecSyntaxError(context.fileName, line, `fixture: reads ${GROUND_FIXTURES}/, and "${value}" names ${found === void 0 ? `no ${GROUND_FIXTURES}/ folder` : `${found}/`}, so put the file in ${where} (verb-reads-its-ground-folder)`);
287
+ }
288
+ return {
289
+ line,
290
+ path: value
291
+ };
292
+ });
293
+ }
294
+ //#endregion
295
+ //#region lint/kit/project/spec-document.ts
296
+ /** The grammar alone: no path resolution, no command, no filesystem. The runner, the lint checker and the JSON Schema read the same constants. */
297
+ /** `fileName` is used verbatim in refusals. Update mode rewrites the returned YAML source, never a re-serialisation. */
298
+ function readSpecFile(content, fileName) {
299
+ const source = parseYamlSource(content);
300
+ const [syntax] = yamlSyntaxErrors(source);
301
+ if (syntax !== void 0) throw new SpecSyntaxError(fileName, syntax.line, syntax.message);
302
+ const context = {
303
+ fileName,
304
+ source
305
+ };
306
+ const root = source.document.contents;
307
+ if (root === null || isScalar(root) && root.value === null) throw new SpecSyntaxError(fileName, 1, "the document is empty");
308
+ const map = requireMap(context, root, "a spec document");
309
+ const found = keyed(closedPairs(context, map, DOCUMENT_KEYS, "a spec document"));
310
+ holdPresence(context, found, {
311
+ line: lineOf(context, map),
312
+ shape: "document"
313
+ });
314
+ return {
315
+ document: {
316
+ description: requireString(context, held(found, "description").value, "description:"),
317
+ descriptionLine: lineOf(context, found.get("description")),
318
+ ...readCliFields(context, found)
319
+ },
320
+ source
321
+ };
322
+ }
323
+ function readCliFields(context, found) {
324
+ const runsNode = held(found, "runs");
325
+ if (!isSeq(runsNode.value) || runsNode.value.items.length === 0) fail(context, runsNode, "runs: is a list of at least one run");
326
+ const runItems = nodeItems(runsNode.value);
327
+ const fixturePair = found.get("fixture");
328
+ const envPair = found.get("env");
329
+ const servePair = found.get("serve");
330
+ return {
331
+ env: envPair === void 0 ? [] : readEnv(context, envPair.value),
332
+ fixtures: fixturePair === void 0 ? [] : readFixtures(context, fixturePair.value),
333
+ runs: runItems.map((item, index) => readRun(context, item, index === runItems.length - 1)),
334
+ serve: servePair === void 0 ? [] : readServe(context, servePair.value)
335
+ };
336
+ }
337
+ /** `stdin:` is an input and a description or command is prose: none appears here. */
338
+ function assertedStreams(document) {
339
+ const streams = [];
340
+ for (const run of document.runs) {
341
+ if (run.stdout !== null) streams.push(run.stdout);
342
+ if (run.stderr !== null) streams.push(run.stderr);
343
+ for (const assertion of run.files) {
344
+ if (assertion.kind !== "content") continue;
345
+ streams.push(...assertion.contains);
346
+ if (assertion.equals !== null) streams.push(assertion.equals);
347
+ }
348
+ }
349
+ return streams;
350
+ }
351
+ function documentPaths(document) {
352
+ return document.fixtures.map(({ line, path }) => ({
353
+ key: "fixture",
354
+ kind: path.endsWith("/") ? "directory" : "file",
355
+ line,
356
+ path
357
+ }));
358
+ }
359
+ function parseSpecDocument(content, fileName) {
360
+ return readSpecFile(content, fileName).document;
361
+ }
362
+ //#endregion
363
+ export { scalarString as a, SPEC_EXTENSION as c, isRelativePath as d, readSpecFile as i, SpecSyntaxError as l, documentPaths as n, DOCUMENT_KEYS as o, parseSpecDocument as r, RUN_KEYS as s, assertedStreams as t, dedent as u };
364
+
365
+ //# sourceMappingURL=shared-spec-document.js.map
@@ -0,0 +1,26 @@
1
+ import { basename } from "node:path";
2
+ //#region lint/kit/project/specifications.ts
3
+ const SPECIFICATIONS = /* @__PURE__ */ new Set([
4
+ "api",
5
+ "cli",
6
+ "integration",
7
+ "job",
8
+ "lint",
9
+ "mobile",
10
+ "website"
11
+ ]);
12
+ const IMPORT_SOURCE = /(?:\bfrom|\bimport|\brequire)\s*\(?\s*['"](?<source>[^'"]+)['"]/gu;
13
+ const CONSTRUCTS = /\bspecification\.[a-z]+\s*\(/u;
14
+ /** Constructing the runner in the file counts: a refusal spec proves the constructor that must fail to start, and has no specification module to import. */
15
+ function reachesRunner(text) {
16
+ for (const match of text.matchAll(IMPORT_SOURCE)) {
17
+ const { source = "" } = match.groups ?? {};
18
+ if (source === "@jterrazz/test" || source.startsWith("@jterrazz/test/")) return true;
19
+ if (basename(source).includes(".specification")) return true;
20
+ }
21
+ return CONSTRUCTS.test(text);
22
+ }
23
+ //#endregion
24
+ export { reachesRunner as n, SPECIFICATIONS as t };
25
+
26
+ //# sourceMappingURL=shared-specifications.js.map
@@ -0,0 +1,130 @@
1
+ import { n as refusal } from "./shared-refusal.js";
2
+ import { c as urlMatches, o as describeRoute, s as routePatternOf } from "./shared-exchange.js";
3
+ import { t as pause } from "./shared-pause.js";
4
+ //#region src/stubs/matching/stub-queue.ts
5
+ function stubMatches(stub, request) {
6
+ const { absentHeaders, match, method, url } = stub.request;
7
+ if (method !== "*" && method.toUpperCase() !== request.method.toUpperCase()) return false;
8
+ if (!urlMatches(url, request.url)) return false;
9
+ if ((absentHeaders ?? []).some((name) => request.headers[name.toLowerCase()] !== void 0)) return false;
10
+ return match ? match(request) : true;
11
+ }
12
+ function isNever({ required, times }) {
13
+ return required === true && times === 0;
14
+ }
15
+ function stateOf({ stub, served }) {
16
+ const { times } = stub;
17
+ if (isNever(stub)) return served === 0 ? " (never)" : ` (never, reached ${served} time(s))`;
18
+ if (served >= (times ?? Number.POSITIVE_INFINITY)) return ` (exhausted after ${times})`;
19
+ return served === 0 ? "" : ` (served ${served} time(s))`;
20
+ }
21
+ /** The first unexhausted stub in declaration order answers; `times: n` is spent after n serves. */
22
+ var StubQueue = class {
23
+ entries;
24
+ constructor(stubs) {
25
+ this.entries = stubs.map((stub) => ({
26
+ reached: [],
27
+ stub,
28
+ served: 0
29
+ }));
30
+ }
31
+ /** Deduped by pattern: two stubs on one URL share one handler and one queue. */
32
+ get routes() {
33
+ const routes = /* @__PURE__ */ new Map();
34
+ for (const { stub } of this.entries) {
35
+ const url = routePatternOf(stub.request.url);
36
+ const key = String(url);
37
+ const route = routes.get(key) ?? {
38
+ methods: [],
39
+ url
40
+ };
41
+ if (!route.methods.includes(stub.request.method)) route.methods.push(stub.request.method);
42
+ routes.set(key, route);
43
+ }
44
+ return [...routes.values()];
45
+ }
46
+ declaredRoutes() {
47
+ return this.entries.map(({ stub }) => describeRoute(stub.request));
48
+ }
49
+ /** `null` when nothing unexhausted matches: the strict-violation signal. */
50
+ take(request) {
51
+ for (const entry of this.entries) {
52
+ if (entry.served >= (entry.stub.times ?? Number.POSITIVE_INFINITY) && !isNever(entry.stub) || !stubMatches(entry.stub, request)) continue;
53
+ entry.served += 1;
54
+ entry.reached.push(`${request.method} ${request.url}`);
55
+ return entry.stub;
56
+ }
57
+ return null;
58
+ }
59
+ verdict(unmatched = null) {
60
+ const errors = [
61
+ this.neverError(),
62
+ unmatched,
63
+ this.unmetError()
64
+ ].filter((error) => error !== null);
65
+ if (errors.length <= 1) return errors[0] ?? null;
66
+ const reports = errors.map(({ message }) => message).join("\n\n");
67
+ return new AggregateError(errors, refusal({
68
+ fix: "correct each outcome in turn, by its own fix",
69
+ found: `${errors.length} outcomes of one action, each with its own refusal:\n\n${reports}`,
70
+ read: "docs/reference/stubs.md#strict-by-construction",
71
+ why: "A reached never stub, an unmatched request and an unmet required stub are independent facts about one action, so all are reported at once."
72
+ }));
73
+ }
74
+ neverError() {
75
+ const reached = this.entries.filter(({ stub, served }) => isNever(stub) && served > 0);
76
+ if (reached.length === 0) return null;
77
+ const lines = reached.map(({ reached: calls, stub }) => ` - ${describeRoute(stub.request)}: reached by ${calls.join(", ")}`);
78
+ return new Error(refusal({
79
+ fix: "take the call out of the path under test or, if the call is now intended, replace the never stub with one that answers it",
80
+ found: `a route the chain declared never requested was reached:\n${lines.join("\n")}`,
81
+ read: "docs/reference/stubs.md#never-a-call-that-must-not-happen",
82
+ why: "A never stub states the subject must not reach its route within the chain's terminal action, or the `intercept()` scope, it is declared for."
83
+ }));
84
+ }
85
+ unmetError() {
86
+ const unmet = [];
87
+ for (const { stub, served } of this.entries) {
88
+ if (stub.required !== true || isNever(stub)) continue;
89
+ const { times } = stub;
90
+ if (times === void 0) {
91
+ if (served === 0) unmet.push(` - ${describeRoute(stub.request)}: declared and required but never requested`);
92
+ } else if (served !== times) unmet.push(` - ${describeRoute(stub.request)}: declared required with times: ${times} but was requested ${served} time(s)`);
93
+ }
94
+ if (unmet.length === 0) return null;
95
+ return new Error(refusal({
96
+ fix: "the code no longer makes the call, or the stub no longer describes it: correct the one that drifted",
97
+ found: `required stub(s) never satisfied during the chain:\n${unmet.join("\n")}`,
98
+ read: "docs/reference/stubs.md#required-is-verified-per-terminal-action",
99
+ why: "A required stub states the call MUST happen, within the terminal action it is declared for."
100
+ }));
101
+ }
102
+ unmatchedError(method, url) {
103
+ const declared = this.entries.length === 0 ? " (no stubs declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.stub.request)}${stateOf(entry)}`).join("\n");
104
+ return new Error(refusal("network-strict-after-first-stub", {
105
+ fix: `declare a stub for ${method} ${url}, or raise the \`times\` of the one it exhausted`,
106
+ found: `an unmatched outgoing request, ${method} ${url}; declared:\n${declared}`
107
+ }));
108
+ }
109
+ };
110
+ //#endregion
111
+ //#region src/stubs/delivery/stream-body.ts
112
+ /** One serialisation for all three deliveries (msw server, msw worker, `node:http` stub). */
113
+ function toReadableStream(body) {
114
+ const encoder = new TextEncoder();
115
+ let index = 0;
116
+ return new ReadableStream({ async pull(controller) {
117
+ const chunk = body.chunks[index];
118
+ if (chunk === void 0) {
119
+ controller.close();
120
+ return;
121
+ }
122
+ if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
123
+ index += 1;
124
+ controller.enqueue(encoder.encode(chunk));
125
+ } });
126
+ }
127
+ //#endregion
128
+ export { StubQueue as n, toReadableStream as t };
129
+
130
+ //# sourceMappingURL=shared-stream-body.js.map