@jterrazz/test 16.1.1 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +1 -2980
  3. package/dist/browser/services.js +1 -0
  4. package/dist/browser/stubs.js +1 -0
  5. package/dist/browser-shared-index.browser.js +3233 -0
  6. package/dist/checker.d.ts +6 -1
  7. package/dist/checker.js +3721 -2624
  8. package/dist/config.d.ts +40 -0
  9. package/dist/config.js +309 -0
  10. package/dist/in-place.global-setup.d.ts +12 -0
  11. package/dist/in-place.global-setup.js +21 -0
  12. package/dist/index.d.ts +1641 -3004
  13. package/dist/index.js +5095 -3835
  14. package/dist/mobile-session.global-setup.d.ts +15 -0
  15. package/dist/mobile-session.global-setup.js +31 -0
  16. package/dist/oxlint.cjs +6069 -5634
  17. package/dist/oxlint.d.cts +43 -7212
  18. package/dist/oxlint.d.ts +43 -7212
  19. package/dist/oxlint.js +3979 -5799
  20. package/dist/route-coverage.global-setup.d.ts +43 -0
  21. package/dist/route-coverage.global-setup.js +1 -0
  22. package/dist/services.d.ts +3 -0
  23. package/dist/services.global-setup.d.ts +43 -0
  24. package/dist/services.global-setup.js +1 -0
  25. package/dist/services.js +597 -0
  26. package/dist/shared-appium.adapter.js +428 -0
  27. package/dist/shared-artifacts.js +34 -0
  28. package/dist/shared-chapter.js +7 -0
  29. package/dist/shared-container.port.d.ts +31 -0
  30. package/dist/shared-derived-projects.js +73 -0
  31. package/dist/shared-device.port.d.ts +189 -0
  32. package/dist/shared-exchange.js +447 -0
  33. package/dist/shared-in-place.js +172 -0
  34. package/dist/shared-index.services.d.ts +346 -0
  35. package/dist/shared-index.stubs.d.ts +287 -0
  36. package/dist/shared-json-shape.js +17 -0
  37. package/dist/shared-landmarks.js +71 -0
  38. package/dist/shared-ledger.js +68 -0
  39. package/dist/shared-listened-port.js +19 -0
  40. package/dist/shared-literate-plugin.js +184 -0
  41. package/dist/shared-manifest.js +1321 -0
  42. package/dist/shared-pause.js +22 -0
  43. package/dist/shared-peer.js +88 -0
  44. package/dist/shared-playwright.adapter.js +328 -0
  45. package/dist/shared-process-alive.js +13 -0
  46. package/dist/shared-process.adapter.js +74 -0
  47. package/dist/shared-record-key.js +13 -0
  48. package/dist/shared-refusal.js +35 -0
  49. package/dist/shared-resolve.js +24 -0
  50. package/dist/shared-route-coverage.global-setup.js +187 -0
  51. package/dist/shared-scenario-failure.js +198 -0
  52. package/dist/shared-serve.js +407 -0
  53. package/dist/shared-service-images.js +10 -0
  54. package/dist/shared-services.global-setup.js +174 -0
  55. package/dist/shared-session-ledger.js +305 -0
  56. package/dist/shared-setup-entry.js +12 -0
  57. package/dist/shared-source-scan.js +133 -0
  58. package/dist/shared-spec-document.js +303 -0
  59. package/dist/shared-spec-grammar.js +66 -0
  60. package/dist/shared-specifications.js +26 -0
  61. package/dist/shared-specs-modules.js +1033 -0
  62. package/dist/shared-stub-reply.js +167 -0
  63. package/dist/shared-stub-server.js +142 -0
  64. package/dist/shared-temp-root.global-setup.js +73 -0
  65. package/dist/shared-test-baseline.js +26 -0
  66. package/dist/shared-test-section.js +58 -0
  67. package/dist/shared-testcontainers.adapter.js +306 -0
  68. package/dist/shared-tokens.js +55 -0
  69. package/dist/shared-yaml-document.js +119 -0
  70. package/dist/stubs.d.ts +2 -0
  71. package/dist/stubs.js +657 -0
  72. package/dist/temp-root.global-setup.d.ts +7 -0
  73. package/dist/temp-root.global-setup.js +1 -0
  74. package/dist/vitest.d.ts +93 -164
  75. package/dist/vitest.js +382 -550
  76. package/docs/README.md +82 -36
  77. package/docs/concepts/architecture.md +118 -0
  78. package/docs/concepts/conventions.md +333 -0
  79. package/docs/guides/api-specs.md +374 -0
  80. package/docs/guides/cli-specs.md +566 -0
  81. package/docs/guides/component-specs.md +252 -0
  82. package/docs/guides/develop.md +97 -0
  83. package/docs/guides/integration-specs.md +181 -0
  84. package/docs/guides/job-specs.md +180 -0
  85. package/docs/guides/lint-specs.md +174 -0
  86. package/docs/guides/mobile-specs.md +319 -0
  87. package/docs/guides/module-tests.md +149 -0
  88. package/docs/guides/release.md +44 -0
  89. package/docs/guides/spec-documents.md +189 -0
  90. package/docs/guides/website-specs.md +413 -0
  91. package/docs/reference/assertions.md +345 -0
  92. package/docs/reference/glossary.md +20 -0
  93. package/docs/reference/linting.md +483 -0
  94. package/docs/reference/services.md +236 -0
  95. package/docs/reference/stubs.md +500 -0
  96. package/docs/reference/time-and-doubles.md +137 -0
  97. package/docs/reference/tokens.md +258 -0
  98. package/docs/reference/visitor.md +221 -0
  99. package/docs/start/getting-started.md +187 -0
  100. package/docs/start/set-up-a-project.md +228 -0
  101. package/package.json +56 -36
  102. package/schema/spec.schema.json +43 -17
  103. package/dist/appium.adapter.js +0 -489
  104. package/dist/artifacts.js +0 -32
  105. package/dist/fixtures.js +0 -110
  106. package/dist/literate.js +0 -1747
  107. package/dist/match.js +0 -804
  108. package/dist/playwright.adapter.js +0 -438
  109. package/dist/server.js +0 -160
  110. package/dist/stream.js +0 -321
  111. package/dist/substring-warning.js +0 -282
  112. package/docs/01-architecture.md +0 -95
  113. package/docs/02-developing.md +0 -351
  114. package/docs/03-testing.md +0 -495
  115. package/docs/04-operating.md +0 -101
  116. package/docs/05-module-tests.md +0 -152
  117. package/docs/06-integration.md +0 -180
  118. package/docs/07-component.md +0 -246
  119. package/docs/08-website.md +0 -404
  120. package/docs/09-mobile.md +0 -270
  121. package/docs/10-api.md +0 -264
  122. package/docs/11-jobs.md +0 -157
  123. package/docs/12-cli.md +0 -677
  124. package/docs/13-elements.md +0 -248
  125. package/docs/14-assertions.md +0 -314
  126. package/docs/15-tokens.md +0 -253
  127. package/docs/16-contracts.md +0 -438
  128. package/docs/17-services.md +0 -188
  129. package/docs/18-conventions.md +0 -332
  130. package/docs/19-linting.md +0 -512
  131. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  132. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  133. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  134. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  135. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  136. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  137. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  138. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  139. package/docs/decisions/_template.md +0 -22
  140. package/docs/reference/classes/Matcher.md +0 -120
  141. package/docs/reference/classes/ProcessHandle.md +0 -368
  142. package/docs/reference/functions/defineContract.md +0 -39
  143. package/docs/reference/functions/defineContracts.md +0 -36
  144. package/docs/reference/functions/disabled.md +0 -28
  145. package/docs/reference/functions/enabled.md +0 -20
  146. package/docs/reference/functions/focused.md +0 -26
  147. package/docs/reference/functions/mockOf.md +0 -79
  148. package/docs/reference/functions/postgres.md +0 -31
  149. package/docs/reference/functions/process.md +0 -33
  150. package/docs/reference/functions/redis.md +0 -28
  151. package/docs/reference/functions/required.md +0 -41
  152. package/docs/reference/functions/selected.md +0 -26
  153. package/docs/reference/functions/sqlite.md +0 -33
  154. package/docs/reference/functions/testId.md +0 -20
  155. package/docs/reference/functions/text.md +0 -40
  156. package/docs/reference/functions/valued.md +0 -28
  157. package/docs/reference/functions/waitUntil.md +0 -46
  158. package/docs/reference/functions/within.md +0 -29
  159. package/docs/reference/index.md +0 -196
  160. package/docs/reference/interfaces/BaseResult.md +0 -87
  161. package/docs/reference/interfaces/CallResult.md +0 -160
  162. package/docs/reference/interfaces/CaptureScope.md +0 -82
  163. package/docs/reference/interfaces/CliResult.md +0 -253
  164. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  165. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  166. package/docs/reference/interfaces/FetchResult.md +0 -180
  167. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  168. package/docs/reference/interfaces/HttpResult.md +0 -126
  169. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  170. package/docs/reference/interfaces/PageResult.md +0 -356
  171. package/docs/reference/interfaces/RenderResult.md +0 -72
  172. package/docs/reference/interfaces/RenderedText.md +0 -110
  173. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  174. package/docs/reference/interfaces/ScreenResult.md +0 -128
  175. package/docs/reference/interfaces/TableAccessor.md +0 -26
  176. package/docs/reference/interfaces/TextAccessor.md +0 -96
  177. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  178. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  179. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  180. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  181. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  182. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  183. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  184. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  185. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  186. package/docs/reference/type-aliases/CliEnv.md +0 -11
  187. package/docs/reference/type-aliases/CliHandle.md +0 -80
  188. package/docs/reference/type-aliases/CliInput.md +0 -36
  189. package/docs/reference/type-aliases/CliOutput.md +0 -46
  190. package/docs/reference/type-aliases/CliPort.md +0 -58
  191. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  192. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  193. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  194. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  195. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  196. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  197. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  198. package/docs/reference/type-aliases/Contract.md +0 -68
  199. package/docs/reference/type-aliases/ContractInput.md +0 -13
  200. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  201. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  202. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  203. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  204. package/docs/reference/type-aliases/Contracts.md +0 -48
  205. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  206. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  207. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  208. package/docs/reference/type-aliases/DevicePort.md +0 -51
  209. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  210. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  211. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  212. package/docs/reference/type-aliases/DomMount.md +0 -21
  213. package/docs/reference/type-aliases/ElementKind.md +0 -27
  214. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  215. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  216. package/docs/reference/type-aliases/ElementRef.md +0 -128
  217. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  218. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  219. package/docs/reference/type-aliases/HonoApp.md +0 -31
  220. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  221. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  222. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  223. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  224. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  225. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  226. package/docs/reference/type-aliases/Intercept.md +0 -19
  227. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  228. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  229. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  230. package/docs/reference/type-aliases/JobHandle.md +0 -34
  231. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  232. package/docs/reference/type-aliases/JobsResult.md +0 -16
  233. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  234. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  235. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  236. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  237. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  238. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  239. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  240. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  241. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  242. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  243. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  244. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  245. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  246. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  247. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  248. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  249. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  250. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  251. package/docs/reference/type-aliases/MockPort.md +0 -10
  252. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  253. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  254. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  255. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  256. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  257. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  258. package/docs/reference/type-aliases/ServerPort.md +0 -36
  259. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  260. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  261. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  262. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  263. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  264. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  265. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  266. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  267. package/docs/reference/type-aliases/SpecKind.md +0 -8
  268. package/docs/reference/type-aliases/SpecRun.md +0 -108
  269. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  270. package/docs/reference/type-aliases/SpecStream.md +0 -32
  271. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  272. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  273. package/docs/reference/type-aliases/SseEvent.md +0 -58
  274. package/docs/reference/type-aliases/StreamBody.md +0 -61
  275. package/docs/reference/type-aliases/TextFilter.md +0 -22
  276. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  277. package/docs/reference/type-aliases/Visitor.md +0 -215
  278. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  279. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  280. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  281. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  282. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  283. package/docs/reference/variables/anthropic.md +0 -19
  284. package/docs/reference/variables/banner.md +0 -10
  285. package/docs/reference/variables/button.md +0 -10
  286. package/docs/reference/variables/clock.md +0 -16
  287. package/docs/reference/variables/complementary.md +0 -10
  288. package/docs/reference/variables/component.md +0 -10
  289. package/docs/reference/variables/content.md +0 -10
  290. package/docs/reference/variables/contentinfo.md +0 -10
  291. package/docs/reference/variables/dialog.md +0 -10
  292. package/docs/reference/variables/field.md +0 -10
  293. package/docs/reference/variables/form.md +0 -10
  294. package/docs/reference/variables/heading.md +0 -10
  295. package/docs/reference/variables/http.md +0 -40
  296. package/docs/reference/variables/intercept.md +0 -11
  297. package/docs/reference/variables/link.md +0 -10
  298. package/docs/reference/variables/listitem.md +0 -10
  299. package/docs/reference/variables/main.md +0 -10
  300. package/docs/reference/variables/match.md +0 -8
  301. package/docs/reference/variables/navigation.md +0 -10
  302. package/docs/reference/variables/openai.md +0 -21
  303. package/docs/reference/variables/option.md +0 -17
  304. package/docs/reference/variables/region.md +0 -10
  305. package/docs/reference/variables/row.md +0 -10
  306. package/docs/reference/variables/search.md +0 -10
  307. package/docs/reference/variables/specification.md +0 -8
  308. package/docs/reference/variables/status.md +0 -10
  309. package/docs/reference/variables/table.md +0 -10
@@ -0,0 +1,187 @@
1
+ import { t as isRecord } from "./shared-json-shape.js";
2
+ import { n as refusal } from "./shared-refusal.js";
3
+ import { a as readLedgers, i as measure, n as clearLedgers, r as markPartial, s as shardLedgersDir, t as ROUTES_DIR, u as shouldUpdateSnapshots } from "./shared-ledger.js";
4
+ import { u as parseYamlSource } from "./shared-yaml-document.js";
5
+ import { r as testSettingsAt } from "./shared-test-section.js";
6
+ import { t as setupEntry } from "./shared-setup-entry.js";
7
+ import { n as heldUnreached, r as recordUnreached, t as ROUTES_LEDGER } from "./shared-test-baseline.js";
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ //#region src/specifications/api/routes/methodless.ts
11
+ function shapeOf(path) {
12
+ return path.replaceAll(/:[^/]+/gu, ":");
13
+ }
14
+ function methodlessDrift(table, methodless) {
15
+ const named = new Set(methodless.map((path) => shapeOf(path)));
16
+ const any = [...new Map(table.filter((route) => route.method === "ALL").map((route) => [shapeOf(route.path), route.path]))];
17
+ const taken = new Set(any.map(([shape]) => shape));
18
+ const missed = any.filter(([shape]) => !named.has(shape)).map(([, path]) => ` ALL ${path}: the app takes it on any method, and the list misses it, so add '${path}' with why the app takes it on any method`);
19
+ const stale = methodless.filter((path) => !taken.has(shapeOf(path))).map((path) => table.some((route) => shapeOf(route.path) === shapeOf(path)) ? ` ${path}: the list names it, and the app takes it on its own methods only, so remove '${path}'` : ` ${path}: the list names it, and the app's table holds no such route, so remove '${path}'`);
20
+ return [...missed, ...stale];
21
+ }
22
+ function methodlessDrifted(drift) {
23
+ return {
24
+ fix: "state in `test: { methodless }` exactly the routes the app takes on any method, each with its reason",
25
+ found: `api routes: \`test: { methodless }\` in jterrazz.config.ts drifted from the app's table:\n${drift.join("\n")}`,
26
+ read: "docs/guides/api-specs.md#a-route-taken-on-any-method",
27
+ why: "`test: { methodless }` names the routes the app takes on any method, which the checker files by their path, whatever verb a spec sends."
28
+ };
29
+ }
30
+ //#endregion
31
+ //#region src/specifications/api/routes/routes.ts
32
+ const METHODS = /* @__PURE__ */ new Set([
33
+ "delete",
34
+ "get",
35
+ "head",
36
+ "options",
37
+ "patch",
38
+ "post",
39
+ "put",
40
+ "trace"
41
+ ]);
42
+ const BRACED_PARAMETER = /^\{[^}]+\}$/u;
43
+ function isParameter(segment) {
44
+ return segment.startsWith(":") || BRACED_PARAMETER.test(segment);
45
+ }
46
+ function takesSegment(declared, sent, template) {
47
+ if (sent === void 0) return false;
48
+ if (template && isParameter(sent)) return isParameter(declared);
49
+ return isParameter(declared) ? sent.length > 0 && !template : sent === declared;
50
+ }
51
+ /** A template request is matched by its template, any other by its path; `ALL` takes any method, a `*` tail the rest. */
52
+ function reachesRoute(route, sent) {
53
+ if (route.method !== "ALL" && route.method !== sent.method) return false;
54
+ const template = sent.template !== void 0;
55
+ const declared = route.path.split("/");
56
+ const segments = ((sent.template ?? sent.path).split("?")[0] ?? "").split("/");
57
+ if (declared.at(-1) === "*") return declared.slice(0, -1).every((segment, index) => takesSegment(segment, segments[index], template));
58
+ return declared.length === segments.length && declared.every((segment, index) => takesSegment(segment, segments[index], template));
59
+ }
60
+ function unreachedRoutes(declared, reached) {
61
+ const seen = /* @__PURE__ */ new Set();
62
+ return declared.filter((route) => {
63
+ const key = `${route.method} ${route.path}`;
64
+ if (seen.has(key) || reached.some((sent) => reachesRoute(route, sent))) return false;
65
+ seen.add(key);
66
+ return true;
67
+ });
68
+ }
69
+ function openApiRoutes(text, json) {
70
+ const parsed = json ? JSON.parse(text) : parseYamlSource(text).document.toJSON();
71
+ const paths = isRecord(parsed) ? parsed.paths : void 0;
72
+ if (!isRecord(paths)) return [];
73
+ return Object.entries(paths).flatMap(([path, item]) => isRecord(item) ? Object.keys(item).filter((method) => METHODS.has(method)).map((method) => ({
74
+ method: method.toUpperCase(),
75
+ path
76
+ })) : []);
77
+ }
78
+ async function declaredRoutes(root) {
79
+ const { routes } = await testSettingsAt(root);
80
+ if (routes === void 0) return;
81
+ return openApiRoutes(readFileSync(join(root, routes), "utf8"), routes.endsWith(".json"));
82
+ }
83
+ async function declaredMethodless(root) {
84
+ const { methodless } = await testSettingsAt(root);
85
+ return [...methodless];
86
+ }
87
+ //#endregion
88
+ //#region src/specifications/api/routes/route-judge.ts
89
+ const UNKNOWN_TABLE = {
90
+ fix: "hand `server` the Hono app itself (its `app.routes` is the table), or declare the routes as an OpenAPI document: `test: { routes: './openapi.yaml' }` in jterrazz.config.ts",
91
+ found: "api routes: an app specification.api() started exposes no route table",
92
+ read: "docs/guides/api-specs.md#an-app-that-is-not-hono",
93
+ why: "A route is judged reached against the table the app declares."
94
+ };
95
+ function spelled(route) {
96
+ return `${route.method} ${route.path}`;
97
+ }
98
+ const SHARD_UNJUDGED = "api routes: not judged, since a shard's ledgers are a part of the run's and the route ratchet is judged only over every shard's ledgers (docs/guides/api-specs.md)";
99
+ const PARTIAL_UNJUDGED = "api routes: not judged, since a partial run (--changed, a file filter or -t) reaches only some routes";
100
+ async function judgeRoutes(root, directory, run) {
101
+ return await judgeLedgers(root, readLedgers(directory), run);
102
+ }
103
+ function unjudgedRun({ failed, scope, update }) {
104
+ if (scope === "shard") return { line: SHARD_UNJUDGED };
105
+ if (scope === "partial") return { line: PARTIAL_UNJUDGED };
106
+ if (failed === 0) return;
107
+ const why = `${failed} file(s) of the run failed, and a failed file may not have sent its requests`;
108
+ return update ? { failure: {
109
+ fix: "make the run green, then record with TEST_UPDATE=1",
110
+ found: `api routes: not recorded: ${why}`,
111
+ read: "docs/guides/api-specs.md#when-a-run-is-not-judged",
112
+ why: "The route ratchet is recorded only from a run that is green and whole."
113
+ } } : { line: `api routes: not judged: ${why}` };
114
+ }
115
+ function ratchetMoved(missing, held, named) {
116
+ return missing > held ? {
117
+ fix: "send each from a spec, or record them with TEST_UPDATE=1",
118
+ found: `api routes: ${missing} declared route(s) no test reached, and the ratchet holds ${held}:\n${named}`,
119
+ read: "docs/guides/api-specs.md#the-ratchet",
120
+ why: "A route no test reached fails the run, beyond the count the ratchet holds."
121
+ } : {
122
+ fix: "the debt fell: record it with TEST_UPDATE=1",
123
+ found: `api routes: ${missing} declared route(s) no test reached, below the ${held} the ratchet holds`,
124
+ read: "docs/guides/api-specs.md#the-ratchet",
125
+ why: "The ratchet is exact in both directions."
126
+ };
127
+ }
128
+ async function judgeLedgers(root, ledgers, { failed, scope, update }) {
129
+ if (ledgers.length === 0) return {};
130
+ const unjudged = unjudgedRun({
131
+ failed,
132
+ scope,
133
+ update
134
+ });
135
+ if (unjudged !== void 0) return unjudged;
136
+ const run = measure(ledgers);
137
+ const table = await declaredRoutes(root) ?? run.declared;
138
+ if (table === void 0) return { failure: UNKNOWN_TABLE };
139
+ const drift = run.declared === void 0 ? [] : methodlessDrift(run.declared, await declaredMethodless(root));
140
+ if (drift.length > 0) return { failure: methodlessDrifted(drift) };
141
+ const declared = [...new Map(table.map((route) => [spelled(route), route])).values()];
142
+ const missing = unreachedRoutes(declared, run.reached);
143
+ const named = missing.map((route) => ` ${spelled(route)}`).join("\n");
144
+ if (update) {
145
+ recordUnreached(root, missing.length);
146
+ return { line: `api routes: ${missing.length} unreached recorded under ${ROUTES_LEDGER}` };
147
+ }
148
+ const held = heldUnreached(root);
149
+ if (missing.length !== held) return { failure: ratchetMoved(missing.length, held, named) };
150
+ return { line: missing.length === 0 ? `api routes: all ${declared.length} declared routes reached` : `api routes: ${missing.length} unreached, held by ${ROUTES_LEDGER}:\n${named}` };
151
+ }
152
+ //#endregion
153
+ //#region src/specifications/api/routes/route-coverage.global-setup.ts
154
+ async function scopeOf(project) {
155
+ if (project.vitest.config.shard !== void 0) return "shard";
156
+ if (project.vitest.config.testNamePattern !== void 0) return "partial";
157
+ const ran = new Set(project.vitest.state.getFiles().filter((file) => file.projectName === project.name).map((file) => file.filepath));
158
+ const { testFiles } = await project.globTestFiles();
159
+ return testFiles.every((file) => ran.has(file)) ? "whole" : "partial";
160
+ }
161
+ function routeCoverageSetup() {
162
+ return setupEntry(import.meta.dirname, "route-coverage");
163
+ }
164
+ function setupRoutes(project) {
165
+ const { root } = project.config;
166
+ const shard = project.vitest.config.shard;
167
+ const directory = join(root, shard === void 0 ? ROUTES_DIR : shardLedgersDir(shard));
168
+ clearLedgers(directory);
169
+ if (shard !== void 0 && project.vitest.config.testNamePattern !== void 0) markPartial(directory);
170
+ project.provide("routesLedger", directory);
171
+ return async () => {
172
+ const { failure, line } = await judgeRoutes(root, directory, {
173
+ failed: project.vitest.state.getFailedFilepaths().length,
174
+ scope: await scopeOf(project),
175
+ update: shouldUpdateSnapshots()
176
+ });
177
+ if (failure !== void 0) throw new Error(refusal(failure));
178
+ if (line !== void 0) process.stdout.write(`${line}\n`);
179
+ };
180
+ }
181
+ function setup(project) {
182
+ return setupRoutes(project);
183
+ }
184
+ //#endregion
185
+ export { setup as n, setupRoutes as r, routeCoverageSetup as t };
186
+
187
+ //# sourceMappingURL=shared-route-coverage.global-setup.js.map
@@ -0,0 +1,198 @@
1
+ import { n as refusal$1 } from "./shared-refusal.js";
2
+ import { lessonParts, refusalBlock } from "@jterrazz/typescript/rules";
3
+ //#region src/visitor/targets/as-written.ts
4
+ const FUNCTION_NAME_OF_KIND = {
5
+ banner: "banner",
6
+ button: "button",
7
+ complementary: "complementary",
8
+ contentinfo: "contentinfo",
9
+ dialog: "dialog",
10
+ field: "field",
11
+ form: "form",
12
+ heading: "heading",
13
+ link: "link",
14
+ listitem: "listitem",
15
+ main: "main",
16
+ navigation: "navigation",
17
+ option: "option",
18
+ region: "region",
19
+ row: "row",
20
+ search: "search",
21
+ status: "status",
22
+ table: "table",
23
+ testId: "testId",
24
+ text: "content"
25
+ };
26
+ /** Spoken in the vocabulary the author wrote, never playwright's: an option appears only when the author stated one. */
27
+ function asWritten(target) {
28
+ const bare = asWrittenUnscoped(target);
29
+ return target.scope ? `within(${asWritten(target.scope)}, ${bare})` : bare;
30
+ }
31
+ function asWrittenUnscoped(target) {
32
+ const args = [];
33
+ if (target.name !== void 0) args.push(JSON.stringify(target.name));
34
+ if (target.exact !== void 0) args.push(`{ exact: ${String(target.exact)} }`);
35
+ return `${FUNCTION_NAME_OF_KIND[target.kind]}(${args.join(", ")})`;
36
+ }
37
+ //#endregion
38
+ //#region src/visitor/matching/landmark-of-tag.ts
39
+ const LANDMARK_ELEMENTS = {
40
+ banner: "header",
41
+ complementary: "aside",
42
+ contentinfo: "footer",
43
+ form: "form",
44
+ main: "main",
45
+ navigation: "nav",
46
+ region: "section",
47
+ search: "search"
48
+ };
49
+ const LANDMARK_SELECTOR = Object.entries(LANDMARK_ELEMENTS).flatMap(([kind, tag]) => [tag, `[role="${kind}"]`]).join(", ");
50
+ function isLandmarkKind(value) {
51
+ return Object.hasOwn(LANDMARK_ELEMENTS, value);
52
+ }
53
+ function landmarkOfTagOrRole(value) {
54
+ const kind = Object.entries(LANDMARK_ELEMENTS).find(([, tag]) => tag === value)?.[0] ?? value;
55
+ return isLandmarkKind(kind) ? kind : void 0;
56
+ }
57
+ //#endregion
58
+ //#region src/visitor/matching/ambiguous.ts
59
+ function landmarkOf(match) {
60
+ const kind = landmarkOfTagOrRole(match.context ?? "");
61
+ if (kind !== "region") return kind === void 0 ? void 0 : `${FUNCTION_NAME_OF_KIND[kind]}()`;
62
+ return match.contextName === void 0 ? void 0 : `${FUNCTION_NAME_OF_KIND.region}(${JSON.stringify(match.contextName)})`;
63
+ }
64
+ function formatMatch(match, index) {
65
+ const attribute = match.detail === void 0 ? "" : ` ${quoteDetail(match)}`;
66
+ const matchedOnLabel = match.accessibleName === void 0 || match.accessibleName === "" ? "" : ` named ${JSON.stringify(match.accessibleName)}`;
67
+ const context = match.context === void 0 || match.context === "" ? "" : ` in <${match.context}>`;
68
+ return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${matchedOnLabel}${context}`;
69
+ }
70
+ function quoteDetail(match) {
71
+ return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
72
+ }
73
+ function landmarkCounts(matches) {
74
+ const counts = /* @__PURE__ */ new Map();
75
+ for (const match of matches) {
76
+ const landmark = landmarkOf(match);
77
+ if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
78
+ }
79
+ return [...counts].toSorted(([, a], [, b]) => a - b);
80
+ }
81
+ function formatFixes(target, matches) {
82
+ const fixes = [];
83
+ const landmarksThatCutTheSet = landmarkCounts(matches).filter(([, count]) => count < matches.length);
84
+ if (!target.scope) {
85
+ const [first, ...rest] = landmarksThatCutTheSet;
86
+ const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
87
+ fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${asWrittenUnscoped(target)})` : `scope it within(${first[0]}, ${asWrittenUnscoped(target)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
88
+ }
89
+ if (target.exact === false) {
90
+ const remaining = matches.filter((match) => match.text === target.name).length;
91
+ if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${asWrittenUnscoped({
92
+ ...target,
93
+ exact: true
94
+ })} [leaves ${remaining} of ${matches.length}]`);
95
+ }
96
+ fixes.push("other element a heading(), button() or field() may name one thing where this does not");
97
+ return fixes;
98
+ }
99
+ function ambiguityError(options) {
100
+ const { target, fixes, lines, where } = options;
101
+ return new Error(refusal$1("target-matches-one-element", {
102
+ fix: ["designate one of them:", ...fixes.map((fix) => ` • ${fix}`)].join("\n"),
103
+ found: [`Ambiguous element: ${asWritten(target)} matched ${lines.length} elements on ${where}:`, ...lines].join("\n")
104
+ }));
105
+ }
106
+ function ambiguousTargetError(options) {
107
+ const { target, matches, url } = options;
108
+ return ambiguityError({
109
+ target,
110
+ fixes: formatFixes(target, matches),
111
+ lines: matches.map((match, index) => formatMatch(match, index)),
112
+ where: url
113
+ });
114
+ }
115
+ function shortened(text) {
116
+ return text.replaceAll(/\s+/gu, " ").trim().slice(0, 80);
117
+ }
118
+ const CANDIDATE_LIMITS = {
119
+ landmarks: LANDMARK_SELECTOR,
120
+ limit: 10,
121
+ textLength: 80
122
+ };
123
+ /** Serialized into the page by playwright: it reads nothing from module scope. */
124
+ function candidatesOf(nodes, { landmarks, limit, textLength }) {
125
+ const landmarkOf = (node) => {
126
+ const landmark = node.parentElement?.closest(landmarks);
127
+ if (landmark === null || landmark === void 0) return {
128
+ context: void 0,
129
+ contextName: void 0
130
+ };
131
+ const labelledBy = (landmark.getAttribute("aria-labelledby") ?? "").split(/\s+/u).filter((id) => id !== "").map((id) => document.querySelector(`#${CSS.escape(id)}`)?.textContent ?? "").join(" ");
132
+ const name = (landmark.getAttribute("aria-label") ?? labelledBy).replaceAll(/\s+/gu, " ").trim();
133
+ return {
134
+ context: landmark.getAttribute("role") ?? landmark.tagName.toLowerCase(),
135
+ contextName: name === "" ? void 0 : name
136
+ };
137
+ };
138
+ return nodes.slice(0, limit).map((node) => {
139
+ const detail = node.getAttribute("href") ?? node.getAttribute("name");
140
+ const text = (node.textContent ?? "").replaceAll(/\s+/gu, " ").trim().slice(0, textLength);
141
+ const label = node.getAttribute("aria-label")?.trim();
142
+ const { context, contextName } = landmarkOf(node);
143
+ return {
144
+ accessibleName: label === void 0 || label === "" || label === text ? void 0 : label,
145
+ context,
146
+ contextName,
147
+ detail: detail ?? void 0,
148
+ tag: node.tagName.toLowerCase(),
149
+ text
150
+ };
151
+ });
152
+ }
153
+ //#endregion
154
+ //#region src/chain/refusals/scenario-failure.ts
155
+ const SURFACE_OF = {
156
+ open: "screen",
157
+ visit: "page"
158
+ };
159
+ const CHAPTER_ANCHOR = /^docs\/[^#]+\.md#.+$/u;
160
+ const isAnchor = (read) => CHAPTER_ANCHOR.test(read);
161
+ function finishedLesson(error) {
162
+ if (!(error instanceof Error)) return;
163
+ const { fix = "", found = "", read, why = "" } = lessonParts(error.message) ?? {};
164
+ const anchor = String(read);
165
+ return [
166
+ fix,
167
+ found,
168
+ why
169
+ ].includes("") || !isAnchor(anchor) ? void 0 : {
170
+ fix,
171
+ found,
172
+ read: anchor,
173
+ why
174
+ };
175
+ }
176
+ function scenarioFailure(error, evidence, verb) {
177
+ const suffix = evidence === null ? "" : `\nEvidence: ${evidence}`;
178
+ const lesson = finishedLesson(error);
179
+ if (error instanceof Error && lesson !== void 0) {
180
+ error.message = refusalBlock({
181
+ ...lesson,
182
+ found: `${lesson.found}${suffix}`
183
+ });
184
+ return error;
185
+ }
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ const surface = SURFACE_OF[verb];
188
+ return new Error(refusal$1({
189
+ fix: `correct the step that failed, or the ${surface} it drives`,
190
+ found: `the ${verb} scenario failed:\n ${message.replaceAll("\n", "\n ")}${suffix}`,
191
+ read: verb === "visit" ? "docs/guides/website-specs.md#visit-scenarios-the-when" : "docs/guides/mobile-specs.md#open-scenarios-the-when",
192
+ why: `${verb === "visit" ? "A" : "An"} ${verb} scenario drives the ${surface} step by step, and the first step that fails ends it.`
193
+ }), { cause: error });
194
+ }
195
+ //#endregion
196
+ export { ambiguityError as a, asWrittenUnscoped as c, shortened as i, CANDIDATE_LIMITS as n, ambiguousTargetError as o, candidatesOf as r, asWritten as s, scenarioFailure as t };
197
+
198
+ //# sourceMappingURL=shared-scenario-failure.js.map