@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,167 @@
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
+ async function nextTask() {
113
+ const { port1, port2 } = new MessageChannel();
114
+ await new Promise((resolve) => {
115
+ port1.onmessage = () => {
116
+ port1.close();
117
+ resolve();
118
+ };
119
+ port2.postMessage(null);
120
+ });
121
+ }
122
+ /** One serialisation for all three deliveries (msw server, msw worker, `node:http` stub). */
123
+ function toReadableStream(body) {
124
+ const encoder = new TextEncoder();
125
+ let index = 0;
126
+ return new ReadableStream({ async pull(controller) {
127
+ const chunk = body.chunks[index];
128
+ if (chunk === void 0) {
129
+ controller.close();
130
+ return;
131
+ }
132
+ if (index > 0) {
133
+ await pause(body.delayBetweenChunks);
134
+ await nextTask();
135
+ }
136
+ index += 1;
137
+ controller.enqueue(encoder.encode(chunk));
138
+ } });
139
+ }
140
+ //#endregion
141
+ //#region src/stubs/delivery/stub-reply.ts
142
+ /** A request's body as a stub's filter reads it: the parsed JSON, the raw text when it is none, `null` when there is none. */
143
+ function requestBody(raw) {
144
+ if (raw.length === 0) return null;
145
+ try {
146
+ return JSON.parse(raw);
147
+ } catch {
148
+ return raw;
149
+ }
150
+ }
151
+ /** The reply a matched stub states for this request, once its delay has passed. */
152
+ async function replyOf(response, observed) {
153
+ const reply = typeof response === "function" ? response(observed) : response;
154
+ await pause(reply.delay ?? 0);
155
+ return reply;
156
+ }
157
+ /** The `error` of the 501 every delivery answers a request it refuses, whichever process serves it. */
158
+ function refusedReply(summary) {
159
+ return `@jterrazz/test strict stubs: ${summary}`;
160
+ }
161
+ function unmatchedSummary(method, url) {
162
+ return `unmatched request ${method} ${url}`;
163
+ }
164
+ //#endregion
165
+ export { toReadableStream as a, unmatchedSummary as i, replyOf as n, StubQueue as o, requestBody as r, refusedReply as t };
166
+
167
+ //# sourceMappingURL=shared-stub-reply.js.map
@@ -0,0 +1,142 @@
1
+ import { n as refusal } from "./shared-refusal.js";
2
+ import { n as isStreamBody } from "./shared-exchange.js";
3
+ import { a as toReadableStream, i as unmatchedSummary, n as replyOf, o as StubQueue, r as requestBody, t as refusedReply } from "./shared-stub-reply.js";
4
+ import { METHODS } from "node:http";
5
+ //#region src/stubs/delivery/msw-handlers.ts
6
+ const NEVER_CACHED = { "cache-control": "no-store" };
7
+ function toMswResponse(msw, response) {
8
+ const { body, headers, status = 200 } = response;
9
+ if (response.transport === "network-error") return msw.HttpResponse.error();
10
+ if (body === null || body === void 0) return new msw.HttpResponse(null, {
11
+ headers: {
12
+ ...NEVER_CACHED,
13
+ ...headers
14
+ },
15
+ status
16
+ });
17
+ if (isStreamBody(body)) return new msw.HttpResponse(toReadableStream(body), {
18
+ headers: {
19
+ ...NEVER_CACHED,
20
+ "content-type": body.contentType,
21
+ ...headers
22
+ },
23
+ status
24
+ });
25
+ if (typeof body === "string") return new msw.HttpResponse(body, {
26
+ headers: {
27
+ ...NEVER_CACHED,
28
+ "content-type": "text/plain; charset=utf-8",
29
+ ...headers
30
+ },
31
+ status
32
+ });
33
+ return msw.HttpResponse.json(body, {
34
+ headers: {
35
+ ...NEVER_CACHED,
36
+ ...headers
37
+ },
38
+ status
39
+ });
40
+ }
41
+ function handlerOf(msw, method) {
42
+ return {
43
+ "*": msw.http.all,
44
+ DELETE: msw.http.delete,
45
+ GET: msw.http.get,
46
+ HEAD: msw.http.head,
47
+ OPTIONS: msw.http.options,
48
+ PATCH: msw.http.patch,
49
+ POST: msw.http.post,
50
+ PUT: msw.http.put
51
+ }[method.toUpperCase()];
52
+ }
53
+ function buildStubHandlers(msw, stubs, bypass) {
54
+ const queue = new StubQueue(stubs);
55
+ let violation = null;
56
+ const refuse = (error, summary) => {
57
+ violation ??= error;
58
+ return msw.HttpResponse.json({ error: refusedReply(summary) }, {
59
+ headers: NEVER_CACHED,
60
+ status: 501
61
+ });
62
+ };
63
+ const recordViolation = (method, url) => refuse(queue.unmatchedError(method, url), unmatchedSummary(method, url));
64
+ const handlers = [];
65
+ for (const route of queue.routes) for (const method of route.methods) {
66
+ const handlerFn = handlerOf(msw, method);
67
+ if (handlerFn === void 0) continue;
68
+ handlers.push(handlerFn(route.url, async ({ request }) => {
69
+ const observed = await observe(request);
70
+ const stub = queue.take(observed);
71
+ if (!stub) return recordViolation(request.method, request.url);
72
+ return toMswResponse(msw, await replyOf(stub.response, observed));
73
+ }));
74
+ }
75
+ handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
76
+ return {
77
+ handlers,
78
+ refuse,
79
+ violation: () => queue.verdict(violation)
80
+ };
81
+ }
82
+ async function observe(request) {
83
+ return {
84
+ body: requestBody(await request.clone().text()),
85
+ headers: Object.fromEntries(request.headers),
86
+ method: request.method.toUpperCase(),
87
+ url: request.url
88
+ };
89
+ }
90
+ //#endregion
91
+ //#region src/stubs/delivery/same-process/stub-server.ts
92
+ async function loadMsw() {
93
+ const [{ setupServer }, msw] = await Promise.all([import("msw/node"), import("msw")]);
94
+ return {
95
+ msw,
96
+ setupServer: () => setupServer()
97
+ };
98
+ }
99
+ let serverInstance = null;
100
+ let refuseOffTheWire = null;
101
+ function offTheWireError(request) {
102
+ return new Error(refusal("network-strict-after-first-stub", {
103
+ fix: "send a method in the capitals the HTTP parser reads (`PATCH`, not `patch`): fetch upper-cases DELETE, GET, HEAD, OPTIONS, POST and PUT, and sends any other as written",
104
+ found: `an outgoing request the network double cannot read off the wire, ${request.method} ${request.url}: its method is not one of node's \`http.METHODS\`, so no stub could answer it and it would reach the network`
105
+ }));
106
+ }
107
+ function guardOffTheWireMethods() {
108
+ const next = globalThis.fetch;
109
+ globalThis.fetch = async (input, init) => {
110
+ const request = new Request(input, init);
111
+ if (refuseOffTheWire !== null && !METHODS.includes(request.method)) return refuseOffTheWire(request);
112
+ return await next(request);
113
+ };
114
+ }
115
+ async function ensureStubServer() {
116
+ if (serverInstance !== null) return serverInstance;
117
+ const { setupServer } = await loadMsw();
118
+ const server = setupServer();
119
+ server.listen({ onUnhandledFrame: "bypass" });
120
+ guardOffTheWireMethods();
121
+ serverInstance = server;
122
+ return server;
123
+ }
124
+ /** The trailing catch-all records any request no stub accepted; the chain's pipeline rethrows it. */
125
+ async function registerStubs(stubs) {
126
+ const server = await ensureStubServer();
127
+ const { msw } = await loadMsw();
128
+ const { handlers, refuse, violation } = buildStubHandlers(msw, stubs);
129
+ server.use(...handlers);
130
+ refuseOffTheWire = (request) => refuse(offTheWireError(request), `unreadable request ${request.method} ${request.url}`);
131
+ return {
132
+ cleanup: () => {
133
+ refuseOffTheWire = null;
134
+ server.resetHandlers();
135
+ },
136
+ violation
137
+ };
138
+ }
139
+ //#endregion
140
+ export { registerStubs };
141
+
142
+ //# sourceMappingURL=shared-stub-server.js.map
@@ -0,0 +1,73 @@
1
+ import { a as EVIDENCE_DIR } from "./shared-artifacts.js";
2
+ import { t as setupEntry } from "./shared-setup-entry.js";
3
+ import { t as processAlive } from "./shared-process-alive.js";
4
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ //#region src/chain/paths/run-folders.ts
8
+ function runOf(entry) {
9
+ const pid = Number(entry.split("-")[0]);
10
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
11
+ }
12
+ /** Leaves alone every name no run gave: the parent of temp roots is shared with whatever else a user keeps there. */
13
+ function sweepOrphanRoots(parent) {
14
+ for (const entry of readdirSync(parent)) {
15
+ const pid = runOf(entry);
16
+ if (pid !== null && !processAlive(pid)) rmSync(join(parent, entry), {
17
+ force: true,
18
+ recursive: true
19
+ });
20
+ }
21
+ }
22
+ /** Keeps a living run's evidence alone: a finished run's stays until the next run starts, and nothing else is ever kept. */
23
+ function sweepFinishedEvidence(folder) {
24
+ if (!existsSync(folder)) return;
25
+ for (const entry of readdirSync(folder)) {
26
+ const pid = runOf(entry);
27
+ if (pid === null || !processAlive(pid)) rmSync(join(folder, entry), {
28
+ force: true,
29
+ recursive: true
30
+ });
31
+ }
32
+ }
33
+ //#endregion
34
+ //#region src/chain/paths/temp-root.global-setup.ts
35
+ /** Named by uid: a temp directory users share never holds a folder one of them cannot sweep. */
36
+ const TEMP_ROOTS_PARENT = `jterrazz-test-runs-${process.getuid?.() ?? "user"}`;
37
+ const TEMP_VARIABLES = [
38
+ "TMPDIR",
39
+ "TMP",
40
+ "TEMP"
41
+ ];
42
+ function setVariables(values) {
43
+ for (const [name, value] of Object.entries(values)) if (value === void 0) Reflect.deleteProperty(process.env, name);
44
+ else process.env[name] = value;
45
+ }
46
+ function tempRootSetup() {
47
+ return setupEntry(import.meta.dirname, "temp-root");
48
+ }
49
+ function joinedRoot() {
50
+ const current = tmpdir();
51
+ return basename(dirname(current)) === TEMP_ROOTS_PARENT && basename(current).startsWith(`${process.pid}-`);
52
+ }
53
+ function setup() {
54
+ if (joinedRoot()) return () => {};
55
+ const parent = join(tmpdir(), TEMP_ROOTS_PARENT);
56
+ mkdirSync(parent, { recursive: true });
57
+ sweepOrphanRoots(parent);
58
+ sweepFinishedEvidence(resolve(EVIDENCE_DIR));
59
+ const root = mkdtempSync(join(parent, `${process.pid}-`));
60
+ const previous = Object.fromEntries(TEMP_VARIABLES.map((name) => [name, process.env[name]]));
61
+ setVariables(Object.fromEntries(TEMP_VARIABLES.map((name) => [name, root])));
62
+ return () => {
63
+ setVariables(previous);
64
+ rmSync(root, {
65
+ force: true,
66
+ recursive: true
67
+ });
68
+ };
69
+ }
70
+ //#endregion
71
+ export { setup as n, tempRootSetup as r, TEMP_ROOTS_PARENT as t };
72
+
73
+ //# sourceMappingURL=shared-temp-root.global-setup.js.map
@@ -0,0 +1,26 @@
1
+ import { t as TEST_SECTION } from "./shared-test-section.js";
2
+ import { labelOf, sectionOf, writeSection } from "@jterrazz/typescript/rules";
3
+ //#region lint/kit/baseline/test-baseline.ts
4
+ const ROUTES_KEY = "routes/unreached";
5
+ const ROUTES_LEDGER = `${labelOf(TEST_SECTION)} › ${ROUTES_KEY}`;
6
+ const heldOf = (root) => sectionOf(root, "test") ?? {};
7
+ const withoutRoutes = (counts) => Object.fromEntries(Object.entries(counts).filter(([key]) => key !== ROUTES_KEY));
8
+ function writeBaseline(root, counts) {
9
+ const held = heldOf(root)[ROUTES_KEY];
10
+ writeSection(root, TEST_SECTION, held === void 0 ? { ...counts } : {
11
+ ...counts,
12
+ [ROUTES_KEY]: held
13
+ });
14
+ }
15
+ const heldUnreached = (root) => heldOf(root)[ROUTES_KEY] ?? 0;
16
+ function recordUnreached(root, count) {
17
+ const others = withoutRoutes(heldOf(root));
18
+ writeSection(root, TEST_SECTION, count === 0 ? others : {
19
+ ...others,
20
+ [ROUTES_KEY]: count
21
+ });
22
+ }
23
+ //#endregion
24
+ export { writeBaseline as i, heldUnreached as n, recordUnreached as r, ROUTES_LEDGER as t };
25
+
26
+ //# sourceMappingURL=shared-test-baseline.js.map
@@ -0,0 +1,58 @@
1
+ import { t as isRecord } from "./shared-json-shape.js";
2
+ import { readSettings, readSettingsSync, refusalBlock } from "@jterrazz/typescript/rules";
3
+ //#region lint/kit/project/test-section.ts
4
+ const TEST_SECTION = "test";
5
+ const UNREAD = Object.freeze({
6
+ collections: [],
7
+ methodless: [],
8
+ production: { allow: {} }
9
+ });
10
+ function wordsOf(value) {
11
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
12
+ }
13
+ function reasonsOf(value) {
14
+ if (!isRecord(value)) return;
15
+ const reasons = {};
16
+ for (const [name, reason] of Object.entries(value)) {
17
+ if (typeof reason !== "string" || reason.trim() === "") return;
18
+ reasons[name] = reason;
19
+ }
20
+ return reasons;
21
+ }
22
+ const BASE_PATH = /^(?:\/[a-z0-9][\w.~-]*)+$/u;
23
+ const isMountPath = (value) => BASE_PATH.test(value);
24
+ function basePathOf(value) {
25
+ if (typeof value !== "string") return;
26
+ if (!isMountPath(value)) throw new Error(refusalBlock({
27
+ fix: "write the path the routes are mounted under from the root, with no slash at its end: test: { basePath: '/api' }",
28
+ found: `jterrazz.config.ts: test.basePath ${JSON.stringify(value)} is no mount path`,
29
+ read: "docs/reference/linting.md#test-settings-section",
30
+ why: "The base path is cut from every route before its subject folder is derived, so it is read as a route prefix is written: one leading slash, lowercase segments, no trailing slash."
31
+ }));
32
+ return value;
33
+ }
34
+ /** A key of the wrong shape is read as its default, never half-used: the type check refuses it first; a base path the type lets through and no route can carry is refused here. */
35
+ function testSettingsOf(test) {
36
+ const section = isRecord(test) ? test : {};
37
+ const allow = isRecord(section.production) ? reasonsOf(section.production.allow) : void 0;
38
+ const basePath = basePathOf(section.basePath);
39
+ return {
40
+ collections: wordsOf(section.collections) ?? UNREAD.collections,
41
+ methodless: Object.keys(reasonsOf(section.methodless) ?? {}),
42
+ production: allow === void 0 ? UNREAD.production : { allow },
43
+ ...typeof section.routes === "string" ? { routes: section.routes } : {},
44
+ ...basePath === void 0 ? {} : { basePath }
45
+ };
46
+ }
47
+ /** Every pass is sync, so the file is loaded synchronously, once per process; the runtime reads the same resolution, so the run and the checker agree. A file that does not resolve is no settings: the toolchain already reports it. */
48
+ const testSettingsAtSync = (root) => testSettingsOf(readSettingsSync(root).resolved.sections[TEST_SECTION]);
49
+ /** Whether the unit's settings list this package's preset, the one whose checker judges every specs tree under it. */
50
+ const listsTestPreset = (root) => readSettingsSync(root).resolved.presets.some((preset) => preset.package === "@jterrazz/test");
51
+ async function testSettingsAt(root) {
52
+ const { resolved } = await readSettings(root);
53
+ return testSettingsOf(resolved.sections[TEST_SECTION]);
54
+ }
55
+ //#endregion
56
+ export { testSettingsAtSync as i, listsTestPreset as n, testSettingsAt as r, TEST_SECTION as t };
57
+
58
+ //# sourceMappingURL=shared-test-section.js.map