@jterrazz/test 16.1.0 → 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 -2867
  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 +4551 -2612
  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 -3002
  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 +6073 -5616
  16. package/dist/oxlint.d.cts +43 -7212
  17. package/dist/oxlint.d.ts +43 -7212
  18. package/dist/oxlint.js +6065 -5609
  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 +374 -507
  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 -349
  104. package/dist/server.js +0 -160
  105. package/dist/stream.js +0 -321
  106. package/dist/substring-warning.js +0 -244
  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 -242
  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 -305
  125. package/docs/19-linting.md +0 -510
  126. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  127. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  128. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  129. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  130. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  131. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  132. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  133. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  134. package/docs/decisions/_template.md +0 -22
  135. package/docs/reference/classes/Matcher.md +0 -120
  136. package/docs/reference/classes/ProcessHandle.md +0 -368
  137. package/docs/reference/functions/defineContract.md +0 -39
  138. package/docs/reference/functions/defineContracts.md +0 -36
  139. package/docs/reference/functions/disabled.md +0 -28
  140. package/docs/reference/functions/enabled.md +0 -20
  141. package/docs/reference/functions/focused.md +0 -26
  142. package/docs/reference/functions/mockOf.md +0 -79
  143. package/docs/reference/functions/postgres.md +0 -31
  144. package/docs/reference/functions/process.md +0 -33
  145. package/docs/reference/functions/redis.md +0 -28
  146. package/docs/reference/functions/required.md +0 -41
  147. package/docs/reference/functions/selected.md +0 -26
  148. package/docs/reference/functions/sqlite.md +0 -33
  149. package/docs/reference/functions/testId.md +0 -20
  150. package/docs/reference/functions/text.md +0 -40
  151. package/docs/reference/functions/valued.md +0 -28
  152. package/docs/reference/functions/waitUntil.md +0 -46
  153. package/docs/reference/functions/within.md +0 -29
  154. package/docs/reference/index.md +0 -196
  155. package/docs/reference/interfaces/BaseResult.md +0 -87
  156. package/docs/reference/interfaces/CallResult.md +0 -160
  157. package/docs/reference/interfaces/CaptureScope.md +0 -82
  158. package/docs/reference/interfaces/CliResult.md +0 -253
  159. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  160. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  161. package/docs/reference/interfaces/FetchResult.md +0 -180
  162. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  163. package/docs/reference/interfaces/HttpResult.md +0 -126
  164. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  165. package/docs/reference/interfaces/PageResult.md +0 -356
  166. package/docs/reference/interfaces/RenderResult.md +0 -72
  167. package/docs/reference/interfaces/RenderedText.md +0 -110
  168. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  169. package/docs/reference/interfaces/ScreenResult.md +0 -128
  170. package/docs/reference/interfaces/TableAccessor.md +0 -26
  171. package/docs/reference/interfaces/TextAccessor.md +0 -96
  172. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  173. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  174. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  175. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  176. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  177. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  178. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  179. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  180. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  181. package/docs/reference/type-aliases/CliEnv.md +0 -11
  182. package/docs/reference/type-aliases/CliHandle.md +0 -80
  183. package/docs/reference/type-aliases/CliInput.md +0 -36
  184. package/docs/reference/type-aliases/CliOutput.md +0 -46
  185. package/docs/reference/type-aliases/CliPort.md +0 -58
  186. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  187. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  188. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  189. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  190. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  191. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  192. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  193. package/docs/reference/type-aliases/Contract.md +0 -68
  194. package/docs/reference/type-aliases/ContractInput.md +0 -13
  195. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  196. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  197. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  198. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  199. package/docs/reference/type-aliases/Contracts.md +0 -48
  200. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  201. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  202. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  203. package/docs/reference/type-aliases/DevicePort.md +0 -51
  204. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  205. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  206. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  207. package/docs/reference/type-aliases/DomMount.md +0 -21
  208. package/docs/reference/type-aliases/ElementKind.md +0 -27
  209. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  210. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  211. package/docs/reference/type-aliases/ElementRef.md +0 -128
  212. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  213. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  214. package/docs/reference/type-aliases/HonoApp.md +0 -31
  215. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  216. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  217. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  218. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  219. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  220. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  221. package/docs/reference/type-aliases/Intercept.md +0 -19
  222. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  223. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  224. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  225. package/docs/reference/type-aliases/JobHandle.md +0 -34
  226. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  227. package/docs/reference/type-aliases/JobsResult.md +0 -16
  228. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  229. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  230. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  231. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  232. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  233. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  234. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  235. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  236. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  237. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  238. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  239. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  240. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  241. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  242. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  243. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  244. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  245. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  246. package/docs/reference/type-aliases/MockPort.md +0 -10
  247. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  248. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  249. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  250. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  251. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  252. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  253. package/docs/reference/type-aliases/ServerPort.md +0 -36
  254. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  255. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  256. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  257. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  258. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  259. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  260. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  261. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  262. package/docs/reference/type-aliases/SpecKind.md +0 -8
  263. package/docs/reference/type-aliases/SpecRun.md +0 -108
  264. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  265. package/docs/reference/type-aliases/SpecStream.md +0 -32
  266. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  267. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  268. package/docs/reference/type-aliases/SseEvent.md +0 -58
  269. package/docs/reference/type-aliases/StreamBody.md +0 -61
  270. package/docs/reference/type-aliases/TextFilter.md +0 -22
  271. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  272. package/docs/reference/type-aliases/Visitor.md +0 -215
  273. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  274. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  275. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  276. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  277. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  278. package/docs/reference/variables/anthropic.md +0 -19
  279. package/docs/reference/variables/banner.md +0 -10
  280. package/docs/reference/variables/button.md +0 -10
  281. package/docs/reference/variables/clock.md +0 -16
  282. package/docs/reference/variables/complementary.md +0 -10
  283. package/docs/reference/variables/component.md +0 -10
  284. package/docs/reference/variables/content.md +0 -10
  285. package/docs/reference/variables/contentinfo.md +0 -10
  286. package/docs/reference/variables/dialog.md +0 -10
  287. package/docs/reference/variables/field.md +0 -10
  288. package/docs/reference/variables/form.md +0 -10
  289. package/docs/reference/variables/heading.md +0 -10
  290. package/docs/reference/variables/http.md +0 -40
  291. package/docs/reference/variables/intercept.md +0 -11
  292. package/docs/reference/variables/link.md +0 -10
  293. package/docs/reference/variables/listitem.md +0 -10
  294. package/docs/reference/variables/main.md +0 -10
  295. package/docs/reference/variables/match.md +0 -8
  296. package/docs/reference/variables/navigation.md +0 -10
  297. package/docs/reference/variables/openai.md +0 -21
  298. package/docs/reference/variables/option.md +0 -17
  299. package/docs/reference/variables/region.md +0 -10
  300. package/docs/reference/variables/row.md +0 -10
  301. package/docs/reference/variables/search.md +0 -10
  302. package/docs/reference/variables/specification.md +0 -8
  303. package/docs/reference/variables/status.md +0 -10
  304. package/docs/reference/variables/table.md +0 -10
package/dist/stream.js DELETED
@@ -1,321 +0,0 @@
1
- import { t as CaptureScope } from "./match.js";
2
- import { A as placeholderPatternSource, D as hasPlaceholders, P as textEquals } from "./literate.js";
3
- //#region src/model/contracts/contract.ts
4
- /**
5
- * Declare one contract. Identity function — its value is the enforced shape
6
- * and the naming convention:
7
- *
8
- * @example
9
- * // specs/api/reports/contracts/openai/classify-article.ts
10
- * import { defineContract, openai } from '@jterrazz/test';
11
- *
12
- * export default defineContract({
13
- * request: openai.responses({ user: PROMPT, tools: ['classify'] }),
14
- * response: openai.reply({ categories: ['TECH'] }),
15
- * });
16
- *
17
- * // Dynamic — the response is computed from the observed request:
18
- * export default defineContract({
19
- * request: http.post('https://api.example.com/echo'),
20
- * response: (request) => http.json({ received: request.body }),
21
- * });
22
- */
23
- function defineContract(contract) {
24
- return contract;
25
- }
26
- /** Is this a {@link Contracts} composite (rather than a contract or a list)? */
27
- function isContracts(value) {
28
- return typeof value === "object" && value !== null && Array.isArray(value.contracts) && typeof value.with === "function";
29
- }
30
- /** Is this a single {@link Contract} (rather than a bare request half)? */
31
- function isContract(value) {
32
- return typeof value === "object" && value !== null && "request" in value && "response" in value;
33
- }
34
- /**
35
- * The route a contract claims — method + the DECLARED url source. Two
36
- * contracts share a route when they would be written on the same line of a
37
- * declaration: same method, same url pattern (`.source` for a RegExp).
38
- */
39
- function routeKeyOf(contract) {
40
- const { method, url } = contract.request;
41
- return url instanceof RegExp ? `${method} re:${url.source}` : `${method} str:${url}`;
42
- }
43
- /** Human-readable route of a contract — used in every failure message. */
44
- function describeRoute(contract) {
45
- const { method, url } = contract.request;
46
- return `${method === "*" ? "ANY" : method} ${url instanceof RegExp ? String(url) : url}`;
47
- }
48
- /** Flatten contracts, lists, and composites into one ordered list. */
49
- function flatten(items) {
50
- const flat = [];
51
- for (const item of items) if (Array.isArray(item)) flat.push(...flatten(item));
52
- else if (isContracts(item)) flat.push(...item.contracts);
53
- else flat.push(item);
54
- return flat;
55
- }
56
- /**
57
- * Compose contracts into the artifact a test imports — it's contracts all the
58
- * way down: a composite may extend contracts, lists, and other composites,
59
- * recursively, order preserved.
60
- *
61
- * `.with(...)` derives a scenario: contracts whose route the overrides claim
62
- * are removed from the base, and the overrides are prepended. Under
63
- * first-match selection a more specific override (`/articles/gone-1`) also
64
- * wins over a generic base route (`/articles/{{uuid}}`) it does not replace.
65
- *
66
- * @example
67
- * // contracts/newsroom.contracts.ts
68
- * export default defineContracts(events, articleById);
69
- * export const withArticleGone = (id: string) =>
70
- * newsroom.with(articleGone(id));
71
- */
72
- function defineContracts(...items) {
73
- const contracts = Object.freeze(flatten(items));
74
- return {
75
- contracts,
76
- with(...overrides) {
77
- const added = flatten(overrides);
78
- const claimed = new Set(added.map(routeKeyOf));
79
- const kept = contracts.filter((contract) => !claimed.has(routeKeyOf(contract)));
80
- return defineContracts([...added, ...kept]);
81
- }
82
- };
83
- }
84
- /** Normalize any accepted input into a flat contract list. */
85
- function contractsOf(input) {
86
- return flatten([input]);
87
- }
88
- //#endregion
89
- //#region src/model/contracts/types.ts
90
- /**
91
- * The marker that tells a STREAMED body from a value that happens to be an
92
- * object. A body reaches three different engines (msw's server, msw's worker,
93
- * the `node:http` stub) and each of them serialises "an object" as JSON, so a
94
- * stream declared as a plain value arrives as `{}`. The tag is what each
95
- * engine recognises before it reaches for `JSON.stringify`.
96
- */
97
- const STREAM_BODY = "@jterrazz/test:stream";
98
- /** Is this body a declared stream rather than a value to serialise? */
99
- function isStreamBody(body) {
100
- return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
101
- }
102
- //#endregion
103
- //#region src/model/timing/pause.ts
104
- /**
105
- * One answer to "wait" — the only `setTimeout` the model owns.
106
- *
107
- * Two things in the framework genuinely have to wait: the spacing between the
108
- * pieces of a declared stream, and the poll behind `waitUntil`. Everything
109
- * else that waits is a spec doing it by hand, which is what J2 refuses. Stated
110
- * once so there is one place to look when time stops behaving.
111
- */
112
- async function pause(ms) {
113
- await new Promise((resolve) => {
114
- setTimeout(resolve, ms);
115
- });
116
- }
117
- //#endregion
118
- //#region src/model/contracts/queue.ts
119
- /**
120
- * The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
121
- * Both engines consume it: the MSW integration on api/jobs chains, and the
122
- * declared stub backend on website/mobile chains.
123
- *
124
- * Selection: the FIRST contract in declaration order that matches the observed
125
- * request and is not exhausted. A contract with no `times` is unlimited (a
126
- * re-render or a retry replays it); `times: n` is spent after n serves, so an
127
- * ordered sequence is written as finite contracts before an unlimited tail:
128
- * `[error500 ×3, ok]`.
129
- *
130
- * Strictness (CONVENTIONS D7): when nothing matches — including "everything
131
- * that matches is exhausted" — the engine answers 501, records the request,
132
- * and the chain fails with {@link ContractQueue.unmatchedError}. A chain that
133
- * declared zero contracts is not guarded at all (unchanged boundary).
134
- */
135
- /** Base used to parse origin-relative observed URLs (`/events?x=1`). */
136
- const RELATIVE_BASE = "http://contract.invalid";
137
- /** Split a declared url into its base (origin+path, or path) and query halves.
138
- * Never through `new URL()`, which would percent-encode `{{token}}` braces. */
139
- function splitDeclared(url) {
140
- const separator = url.indexOf("?");
141
- if (separator === -1) return {
142
- base: url,
143
- query: new URLSearchParams()
144
- };
145
- return {
146
- base: url.slice(0, separator),
147
- query: new URLSearchParams(url.slice(separator + 1))
148
- };
149
- }
150
- /** Decode percent-escapes, falling back to the raw text on malformed input. */
151
- function safeDecode(text) {
152
- try {
153
- return decodeURIComponent(text);
154
- } catch {
155
- return text;
156
- }
157
- }
158
- /** Token-aware comparison of one declared value against an observed one. */
159
- function valueMatches(declared, observed) {
160
- return textEquals(declared, observed, new CaptureScope());
161
- }
162
- /**
163
- * Does an observed URL satisfy a declared one? A RegExp tests the full URL; a
164
- * PATH FORM (`/articles/{{uuid}}`) ignores the origin; an absolute string
165
- * compares origin+pathname. In both string forms `{{token}}` segments compare
166
- * structurally and declared query params are a SUBSET of the observed ones.
167
- */
168
- function urlMatches(declared, observed) {
169
- if (declared instanceof RegExp) return declared.test(observed);
170
- let url;
171
- try {
172
- url = new URL(observed, RELATIVE_BASE);
173
- } catch {
174
- return false;
175
- }
176
- const { base, query } = splitDeclared(declared);
177
- const observedBase = base.startsWith("/") ? url.pathname : `${url.origin}${url.pathname}`;
178
- const decodedBase = base.startsWith("/") ? safeDecode(url.pathname) : `${url.origin}${safeDecode(url.pathname)}`;
179
- if (!valueMatches(base, observedBase) && !valueMatches(base, decodedBase)) return false;
180
- for (const key of new Set(query.keys())) {
181
- const observedValues = url.searchParams.getAll(key);
182
- if (!query.getAll(key).every((value) => observedValues.some((actual) => valueMatches(value, actual)))) return false;
183
- }
184
- return true;
185
- }
186
- /**
187
- * Does an observed request satisfy a contract? Method (`*` matches any), URL,
188
- * then the contract's own `match` predicate (headers/query/body filters).
189
- */
190
- function contractMatches(contract, request) {
191
- const { match, method, url } = contract.request;
192
- if (method !== "*" && method.toUpperCase() !== request.method.toUpperCase()) return false;
193
- if (!urlMatches(url, request.url)) return false;
194
- return match ? match(request) : true;
195
- }
196
- /**
197
- * The URL pattern an out-of-process router (MSW) must register so the request
198
- * REACHES the queue. Deliberately no narrower than {@link contractMatches} —
199
- * the predicate above stays the authority on what actually matches.
200
- */
201
- function routePatternOf(declared) {
202
- if (declared instanceof RegExp) return declared;
203
- const { base } = splitDeclared(declared);
204
- if (base.startsWith("/")) return new RegExp(String.raw`^https?:\/\/[^/?#]+${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
205
- if (hasPlaceholders(base)) return new RegExp(String.raw`^${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
206
- return base;
207
- }
208
- /** How a declared contract reads in a failure message: spent, served, or silent. */
209
- function stateOf({ contract, served }) {
210
- const { times } = contract;
211
- if (times !== void 0 && served >= times) return ` (exhausted after ${times})`;
212
- return served === 0 ? "" : ` (served ${served} time(s))`;
213
- }
214
- var ContractQueue = class {
215
- entries;
216
- constructor(contracts) {
217
- this.entries = contracts.map((contract) => ({
218
- contract,
219
- served: 0
220
- }));
221
- }
222
- /** How many contracts the chain declared. */
223
- get size() {
224
- return this.entries.length;
225
- }
226
- /**
227
- * Routes to register with an out-of-process router, deduped by pattern —
228
- * two contracts on the same URL share one handler and one queue.
229
- */
230
- get routes() {
231
- const routes = /* @__PURE__ */ new Map();
232
- for (const { contract } of this.entries) {
233
- const url = routePatternOf(contract.request.url);
234
- const key = String(url);
235
- const route = routes.get(key) ?? {
236
- methods: [],
237
- url
238
- };
239
- if (!route.methods.includes(contract.request.method)) route.methods.push(contract.request.method);
240
- routes.set(key, route);
241
- }
242
- return [...routes.values()];
243
- }
244
- /** The declared routes, in order — the enumeration a 501 body carries. */
245
- declaredRoutes() {
246
- return this.entries.map(({ contract }) => describeRoute(contract));
247
- }
248
- /**
249
- * Serve the first contract in declaration order that matches and is not
250
- * exhausted, or null when nothing matches (including "everything that
251
- * matches is spent") — the strict-violation signal.
252
- */
253
- take(request) {
254
- for (const entry of this.entries) {
255
- const { times } = entry.contract;
256
- if (times !== void 0 && entry.served >= times) continue;
257
- if (!contractMatches(entry.contract, request)) continue;
258
- entry.served += 1;
259
- return entry.contract;
260
- }
261
- return null;
262
- }
263
- /**
264
- * The chain-end failure for contracts declared `required: true` that were
265
- * never requested (or not exactly `times` times), or null when every
266
- * requirement held.
267
- */
268
- requiredError() {
269
- const unmet = [];
270
- for (const { contract, served } of this.entries) {
271
- if (!contract.required) continue;
272
- const { times } = contract;
273
- if (times === void 0) {
274
- if (served === 0) unmet.push(` - ${describeRoute(contract)} — declared and required but never requested`);
275
- } else if (served !== times) unmet.push(` - ${describeRoute(contract)} — declared required with times: ${times} but was requested ${served} time(s)`);
276
- }
277
- if (unmet.length === 0) return null;
278
- return /* @__PURE__ */ new Error(`Required contract(s) never satisfied during the chain:\n${unmet.join("\n")}\nA required contract states the call MUST happen — either the code no longer makes it, or the contract no longer describes it.`);
279
- }
280
- /**
281
- * The strict failure for a request that matched no contract (CONVENTIONS
282
- * D7): method + URL of the offending request, plus every declared contract
283
- * and how often it was served.
284
- */
285
- unmatchedError(method, url) {
286
- const declared = this.entries.length === 0 ? " (no contracts declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.contract)}${stateOf(entry)}`).join("\n");
287
- return /* @__PURE__ */ new Error(`Unmatched outgoing HTTP request during spec: ${method} ${url}\nDeclared contracts:\n${declared}\nEvery outgoing request of a chain that declares contracts must match one — add a contract for it (or raise its \`times\` when it is exhausted).`);
288
- }
289
- };
290
- //#endregion
291
- //#region src/model/contracts/stream.ts
292
- /**
293
- * A declared stream, made real — ONE serialisation for all three engines.
294
- *
295
- * msw's server and msw's worker take a `ReadableStream` body verbatim
296
- * (https://mswjs.io/docs/http/mocking-responses/streaming) and the `node:http`
297
- * stub pipes one into its response, so what "arriving in pieces" MEANS —
298
- * the order, the spacing, the close after the last chunk — is stated here and
299
- * nowhere else. A second copy of it would be a second answer to "what does the
300
- * subject read", and the facets would drift.
301
- */
302
- /**
303
- * The pieces of `body`, in order, `delayBetweenChunks` milliseconds apart,
304
- * closing after the last one.
305
- */
306
- function toReadableStream(body) {
307
- const encoder = new TextEncoder();
308
- let index = 0;
309
- return new ReadableStream({ async pull(controller) {
310
- const chunk = body.chunks[index];
311
- if (chunk === void 0) {
312
- controller.close();
313
- return;
314
- }
315
- if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
316
- index += 1;
317
- controller.enqueue(encoder.encode(chunk));
318
- } });
319
- }
320
- //#endregion
321
- export { isStreamBody as a, defineContracts as c, STREAM_BODY as i, isContract as l, ContractQueue as n, contractsOf as o, pause as r, defineContract as s, toReadableStream as t, isContracts as u };
@@ -1,244 +0,0 @@
1
- //#region src/model/elements/ambiguity.ts
2
- /**
3
- * The ambiguity refusal — CONVENTIONS W3.
4
- *
5
- * A visit descriptor must designate exactly one element. Acting on "the
6
- * first match" is the failure mode this module exists to prevent: the spec
7
- * keeps passing while the visitor clicks something else, and nothing ever
8
- * reports it. So when a descriptor matches several elements the framework
9
- * refuses, and the refusal has to carry everything needed to fix it without
10
- * opening a browser — the descriptor in the caller's own vocabulary, every
11
- * candidate, and the concrete rewrites that would resolve it.
12
- *
13
- * Pure string building: it takes captured data and returns a message, so the
14
- * wording is unit-testable and the browser integration stays a thin adapter.
15
- */
16
- /** `kind` → the constructor that builds it, so errors echo the caller's source. */
17
- const CONSTRUCTORS = {
18
- banner: "banner",
19
- button: "button",
20
- complementary: "complementary",
21
- contentinfo: "contentinfo",
22
- dialog: "dialog",
23
- field: "field",
24
- form: "form",
25
- heading: "heading",
26
- link: "link",
27
- listitem: "listitem",
28
- main: "main",
29
- navigation: "navigation",
30
- option: "option",
31
- region: "region",
32
- row: "row",
33
- search: "search",
34
- status: "status",
35
- table: "table",
36
- testId: "testId",
37
- text: "content"
38
- };
39
- /** The landmark a context string maps back to, for a copy-pasteable suggestion. */
40
- const CONTEXT_LANDMARKS = {
41
- aside: "complementary()",
42
- banner: "banner()",
43
- complementary: "complementary()",
44
- contentinfo: "contentinfo()",
45
- footer: "contentinfo()",
46
- form: "form()",
47
- header: "banner()",
48
- main: "main()",
49
- nav: "navigation()",
50
- navigation: "navigation()",
51
- search: "search()"
52
- };
53
- /**
54
- * Render a descriptor as the source that would build it — `link('Articles')`,
55
- * `within(navigation(), link('Articles', { exact: false }))`. The error speaks
56
- * the vocabulary the author wrote, never playwright's — so the option appears
57
- * only when the AUTHOR stated one, whichever way.
58
- */
59
- function formatElement(element) {
60
- const bare = formatBare(element);
61
- return element.scope ? `within(${formatElement(element.scope)}, ${bare})` : bare;
62
- }
63
- function formatBare(element) {
64
- const args = [];
65
- if (element.name !== void 0) args.push(JSON.stringify(element.name));
66
- if (element.exact !== void 0) args.push(`{ exact: ${String(element.exact)} }`);
67
- return `${CONSTRUCTORS[element.kind]}(${args.join(", ")})`;
68
- }
69
- /** `1. <a href="/articles">Articles</a> in <nav>` — one evidence line per candidate. */
70
- function formatMatch(match, index) {
71
- const attribute = match.detail ? ` ${quoteDetail(match)}` : "";
72
- const named = match.accessibleName ? ` named ${JSON.stringify(match.accessibleName)}` : "";
73
- const context = match.context ? ` in <${match.context}>` : "";
74
- return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${named}${context}`;
75
- }
76
- function quoteDetail(match) {
77
- return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
78
- }
79
- /** Each landmark the candidates sit in, and how many of them it holds. */
80
- function landmarkCounts(matches) {
81
- const counts = /* @__PURE__ */ new Map();
82
- for (const match of matches) {
83
- const landmark = match.context === void 0 ? void 0 : CONTEXT_LANDMARKS[match.context];
84
- if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
85
- }
86
- return [...counts].toSorted(([, a], [, b]) => a - b);
87
- }
88
- /**
89
- * The rewrites worth offering, in the order they should be tried. Scoping is
90
- * always available; `exact` only when it would actually narrow the set, and
91
- * the count it would leave is stated so a still-ambiguous suggestion never
92
- * reads as a fix.
93
- */
94
- function formatFixes(element, matches) {
95
- const fixes = [];
96
- const narrowing = landmarkCounts(matches).filter(([, count]) => count < matches.length);
97
- if (!element.scope) {
98
- const [first, ...rest] = narrowing;
99
- const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
100
- fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${formatBare(element)})` : `scope it within(${first[0]}, ${formatBare(element)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
101
- }
102
- if (element.exact === false && element.name !== void 0) {
103
- const remaining = matches.filter((match) => match.text === element.name).length;
104
- if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatBare({
105
- ...element,
106
- exact: true
107
- })} [leaves ${remaining} of ${matches.length}]`);
108
- }
109
- fixes.push("other element a heading(), button() or field() may name one thing where this does not");
110
- return fixes;
111
- }
112
- /**
113
- * The W3 refusal. A distinct class so the visit wrapper can recognize an
114
- * already-complete message and let it through instead of nesting it inside
115
- * `visit scenario failed: …`, which would print the whole thing twice.
116
- */
117
- var AmbiguousElementError = class extends Error {
118
- name = "AmbiguousElementError";
119
- };
120
- /**
121
- * Build the refusal thrown when a descriptor matches more than one element.
122
- *
123
- * @param options.element The descriptor as the caller wrote it.
124
- * @param options.matches Every candidate, in DOM order (already truncated).
125
- * @param options.url The page the visitor was on when it happened.
126
- */
127
- function describeAmbiguity(options) {
128
- const { element, matches, url } = options;
129
- const fixes = formatFixes(element, matches).map((fix) => ` • ${fix}`);
130
- return [
131
- `Ambiguous element: ${formatElement(element)} matched ${matches.length} elements on ${url}.`,
132
- "",
133
- "A spec must designate exactly one element. Acting on the first match would let",
134
- "this test keep passing while the visitor interacts with something else.",
135
- "",
136
- "Matched:",
137
- ...matches.map((match, index) => formatMatch(match, index)),
138
- "",
139
- "Disambiguate with one of:",
140
- ...fixes,
141
- "",
142
- "Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
143
- ].join("\n");
144
- }
145
- //#endregion
146
- //#region src/model/elements/substring-warning.ts
147
- /**
148
- * The transitional warning for a name that matches only as a SUBSTRING.
149
- *
150
- * Until 16.0 a descriptor's name matched any element whose accessible name
151
- * CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
152
- * test could pass for years against the wrong element without anyone noticing,
153
- * because the wrong element was usually next to the right one. Exact is the
154
- * default now.
155
- *
156
- * A lint rule cannot carry this: whether a name matched whole or in part is a
157
- * fact about a RUN, and a verdict fed by a previous run's artefact is not
158
- * deterministic. So it is a runtime warning, printed by the adapter that did
159
- * the looking, at the moment it has both answers in hand — nothing found under
160
- * exact, something found under substring.
161
- *
162
- * It is printed ONCE per descriptor per process: a scenario that clicks the
163
- * same link in ten tests has one thing to fix, not ten lines to read.
164
- */
165
- /** How long the window lasts, stated in the message itself. */
166
- const WINDOW = "through 16.x";
167
- /** Descriptors already reported in this process. */
168
- const reported = /* @__PURE__ */ new Set();
169
- /** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
170
- function identityOf(element) {
171
- const own = `${element.kind}:${element.name ?? ""}`;
172
- return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
173
- }
174
- /**
175
- * Report that `element` designated nothing as a whole name, but would have
176
- * designated something as a substring. Returns whether a line was printed,
177
- * which is what the adapters' own tests assert on.
178
- */
179
- function warnSubstringOnly(element, where, found) {
180
- const identity = identityOf(element);
181
- if (reported.has(identity)) return false;
182
- reported.add(identity);
183
- const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
184
- console.warn(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
185
- return true;
186
- }
187
- /**
188
- * The descriptor to retry with, LEVEL by level, or `undefined` when nothing
189
- * would change.
190
- *
191
- * Every level of the chain is asked the same question the target is asked —
192
- * `within(button('Proof of authorship'), content('Verified'))` against a
193
- * button named "Proof of authorship Verified" failed on the SCOPE, and a
194
- * warning that only ever widened the target printed nothing at all. The chain
195
- * is walked outside-in and rebuilt as it goes, since an inner level's answer
196
- * is meaningless under a scope that designates nothing.
197
- *
198
- * A level that states an `exact` of its own is left alone: an author who wrote
199
- * `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
200
- * was already exact.
201
- *
202
- * Asked BEFORE the action, not after it fails: a verb waits its whole
203
- * actionability budget, which is the test's own budget, so a retry afterwards
204
- * never runs — the test is already over. The probes are counts, which wait for
205
- * nothing, and the common case (the name matches whole) costs one per level.
206
- */
207
- async function widenedForWindow(element, where, probe) {
208
- const chain = [];
209
- for (let level = element; level; level = level.scope) chain.unshift(level);
210
- let rebuilt;
211
- let widened = false;
212
- for (const level of chain) {
213
- const candidate = {
214
- ...level,
215
- ...rebuilt === void 0 ? {} : { scope: rebuilt }
216
- };
217
- const loose = await widenedLevel(candidate, where, probe);
218
- widened ||= loose !== void 0;
219
- rebuilt = loose ?? candidate;
220
- }
221
- return widened ? rebuilt : void 0;
222
- }
223
- /**
224
- * The one level, widened and warned about, or `undefined` when it designates
225
- * something already, states its own `exact`, carries no name, or would
226
- * designate nothing either way.
227
- */
228
- async function widenedLevel(candidate, where, probe) {
229
- if (candidate.exact !== void 0 || candidate.name === void 0) return;
230
- try {
231
- if (await probe.count(candidate) > 0) return;
232
- const loose = {
233
- ...candidate,
234
- exact: false
235
- };
236
- if (await probe.count(loose) !== 1) return;
237
- warnSubstringOnly(candidate, where, await probe.nameOf(loose));
238
- return loose;
239
- } catch {
240
- return;
241
- }
242
- }
243
- //#endregion
244
- export { formatElement as a, describeAmbiguity as i, widenedForWindow as n, AmbiguousElementError as r, warnSubstringOnly as t };
@@ -1,95 +0,0 @@
1
- # 01 — Architecture
2
-
3
- What this package IS: one runner model behind six constructors, one chain that needs none, five source trees with declared edges, and seven channels through which its conventions are enforced — each row of the catalogue naming exactly one. The chapters that follow explain how to USE each facet; this one draws the lines they sit inside.
4
-
5
- | The shape | Held below |
6
- | -------------------------- | -------------------------------------------------------------------------------- |
7
- | The runner model | [The runner model](#the-runner-model) — constructor, handle, chain, result |
8
- | The source trees | [The five trees](#the-five-trees) — `model`, `facets`, `seams`, `runner`, `lint` |
9
- | How a convention is held | [The seven enforcement channels](#the-seven-enforcement-channels) |
10
- | What ships out of the tree | [What the tree publishes](#what-the-tree-publishes) |
11
-
12
- The rules themselves — what each family says, what a reviewer must judge — are the constitution, [18 — Conventions](18-conventions.md); their normative sentences are the generated catalogue, [19 — Linting](19-linting.md).
13
-
14
- ## The runner model
15
-
16
- Every facet is the same four steps. A **constructor** takes options and returns a **handle**; the handle opens a **chain** of zero or more setups closed by exactly one terminal action; the action executes and resolves to a **typed result**; the result is asserted through vitest's `expect()`. Nothing else is public — there is no imperative escape hatch, because a spec that can do anything proves nothing in particular.
17
-
18
- Six constructors exist and the list is closed (`src/facets/specification.ts`):
19
-
20
- | Constructor | Handle destructures to | Subject under test |
21
- | ----------------------------- | ---------------------------------- | ------------------------------------------------ |
22
- | `specification.api()` | `{ api, cleanup, docker }` | An HTTP API, built and served in this process |
23
- | `specification.jobs()` | `{ jobs, cleanup }` | A background pipeline, triggered by name |
24
- | `specification.cli()` | `{ cli, cleanup, docker }` | A command binary in a fresh temp directory |
25
- | `specification.integration()` | `{ integration, cleanup, docker }` | A module against real services, or a golden |
26
- | `specification.website()` | `{ website, cleanup, url }` | A rendered page — fetched, or driven in chromium |
27
- | `specification.mobile()` | `{ mobile, cleanup, udid }` | A native screen on an iOS simulator |
28
-
29
- The asymmetry in that column is the model, not an oversight: `jobs` never spawns a container, so it is handed no `docker`; `website` and `mobile` drive a browser and a simulator rather than an orchestrated stack, so they carry neither.
30
-
31
- **One facet has no constructor**, and the same reason explains it: a rendered component starts nothing. `component` ([07 — Component specs](07-component.md)) is a chain the package exports directly — `component.intercept(…).render(<X />)` — so there is no handle to destructure and no `cleanup` to pass to `afterAll`. What every render of a project shares (the providers, the Vite pipeline, the viewport) is the PROJECT's, stated once by `component()` in `vitest.config.ts`.
32
-
33
- A runner is created **once per suite**, in a `*.specification.ts` file, and imported by the test files beside it. That split is what makes the container lifecycle affordable — one Postgres per suite, not one per test — and it is why `afterAll(cleanup)` belongs in the specification file and nowhere else.
34
-
35
- ### The seam under the chain
36
-
37
- `SpecificationBuilder` (`src/model/chain/builder.ts`) holds the chain for every facet; each facet contributes its own setups and its own terminal actions on top. The infrastructure a chain needs is reached through **ports** — `src/model/ports/` declares eight of them (`browser`, `cli`, `container`, `database`, `device`, `isolation`, `server`, `service`) — and an integration implements one. So the chain knows "a database exists"; it never knows Postgres.
38
-
39
- Every seam is opened lazily rather than imported, and each one is an optional peer loaded by the one module that owns it: `playwright` for `.visit()`, `appium`/`webdriverio` for `.open()`, `@vitest/browser-playwright` + `vitest-browser-react` + `react` for `component.render()`, and — since 16.0 — `better-sqlite3`, `pg`, `redis` and `testcontainers` for the services a record declares. A project that tests no page, no component and no database installs none of them; `src/seams/peer.ts` owns the one message a missing peer produces, which names the facet that asked, the command, and under pnpm the `onlyBuiltDependencies` line a native binding needs.
40
-
41
- ## The five trees
42
-
43
- The source tree is five trees with declared, one-directional edges. The map is not the linter's to assume — `i1-layer-boundaries` ships inert — so this package declares its own as `FRAMEWORK_LAYERS` in `oxlint.config.ts`, and that declaration is the enforced statement of what follows.
44
-
45
- | Tree | May import | Holds |
46
- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
- | `model/` | itself, the docker/process/yaml seams, the msw seam through two named lazy imports, `runner/update`, and each facet's `*.result` module | What every facet is made of: the chain and the orchestrator, the results and their accessors, the element vocabulary, the contracts and `intercept()`, the `{{token}}` engine, the goldens, the clock, the doubles, the ports |
48
- | `facets/` | `model/`, itself, the runner's project shape, and the one seam each constructor opens | Seven folders, the SAME four files in each: `<facet>.specification.ts` (the constructor), `<facet>.chain.ts`, `<facet>.result.ts`, `<facet>.project.ts` (the vitest helper), plus what is unique to the facet |
49
- | `seams/` | its OWN external dependency, plus `model/` and the facet vocabulary an adapter projects into | One folder per dependency: `postgres`, `redis`, `sqlite`, `testcontainers`, `docker`, `hono`, `playwright`, `vitest-browser`, `appium`, `msw`, `openai`, `anthropic`, `yaml`, `process` |
50
- | `runner/` | `vitest`, `vite`, `@vitest/browser-playwright`, `model/`, `facets/` | The CONFIG side of the runner coupling: the preset, update mode, the `literate()` plugin, the shape a node facet's project takes, and the project index |
51
- | `lint/` | itself, plus a short list of PURE `model/` modules | The tool-facing channel: the oxlint plugin, the conventions checker, the catalogue manifest and generator |
52
-
53
- **Every facet folder carries the same four files, whether or not it owns all four.** The setups and terminal actions of the six node facets are ONE builder, because a second copy of "declare a contract, seed a table, run the thing" is how two facets drift — so `api.chain.ts` and its five siblings are a named re-export of that builder rather than an implementation, and `jobs.result.ts` re-exports the shape the builder states. The shape is uniform so that a reader who has opened one facet has opened all seven; the re-export says, in the folder itself, which of the four a facet does not own.
54
-
55
- Three of those edges carry their reason in the declaration itself. `model/` reaches `seams/docker`, `seams/process` and the yaml parser because none of the three has a dependency of its own to leak, and it reaches each facet's `*.result` module because the ONE builder constructs all seven results — a value edge, stated rather than hidden. `lint/` reaches exactly the pure modules the runner also uses — the token list, the ground names, the root walk, the `<case>.spec.yaml` parser — so that the file the lint accepts is the file the runner runs, from ONE parser. And the runner is named in exactly TWO places outside a test, one per side of it: `runner/` for the config side — the preset, the project shape, the plugin — and `seams/vitest-browser/` for the page side, where the locators, the visitor, the golden commands and the page's own `vitest` primitives live. Swapping the runner would be a rewrite of those two folders and of nothing else; `model/` states what a render IS and never which runner performs it, which is why the layer map exempts that seam from the prod-import ban (F2) by name.
56
-
57
- The table's May-import column is a reading of `FRAMEWORK_LAYERS`, never a second statement of it: the declaration in `oxlint.config.ts` is what the linter enforces and what a change edits.
58
-
59
- There are TWO composition roots and neither names a layer. `src/index.ts` is node's: it wires the container integrations into the registry seam and re-exports the public surface. `src/browser/index.ts` is the page's, and `src/surface.ts` is what they agree on — see [What the tree publishes](#what-the-tree-publishes).
60
-
61
- ## The seven enforcement channels
62
-
63
- A convention this package states is held by a **channel**, and every row of the catalogue names exactly one of seven. The guiding aim is that most enforcement is programmatic, not review-borne.
64
-
65
- - **statique** — the `jterrazz/*` oxlint plugin: one file per rule under `src/lint/rules/model/` or `src/lint/rules/<facet>/` — the tree the rule reaches, which is also the `facet` field of its manifest row — AST analysis, one diagnostic per file.
66
- - **upstream** — a convention an oxlint vitest-plugin rule already holds, set as an OPTION from the `testing` fragment rather than duplicated as a rule of ours ([ADR-005](decisions/005-the-doubles-ladder-is-closed.md)).
67
- - **checker** — passes of the `jterrazz-test-check` binary reading what an AST cannot: the `{{token}}` grammar of `_requests/` and `_expected/` fixtures, the `<case>.spec.yaml` document family, cross-file analyses that cross a `*.specification.ts` with its tests, and the member pass that judges a package rather than a tree.
68
- - **runtime** — the framework refuses incorrect usage as it executes, where static analysis abstains (a non-literal argument) or cannot reach (the network, a container lifecycle).
69
- - **type** — what the compiler refuses with no rule at all, proven by `src/type-channel.test-d.ts`.
70
- - **meta** — what the framework's own suite holds about itself: the catalogue's freshness, its completeness, the reach of its rules — [03 — Testing](03-testing.md) owns the channel.
71
- - **process** — the judgement no single channel settles: asset-driven grouping, golden-file discipline, retro-propagation.
72
-
73
- The channel a rule sits on is not prose here: it is a field of `src/lint/manifest.ts`, the single source of truth from which the catalogue in [19 — Linting](19-linting.md) is generated. A rule and its normative sentence live together in the code, so the two cannot drift.
74
-
75
- ## What the tree publishes
76
-
77
- The bundle is built by `tsdown` into `dist/` and its shape follows how each entry is consumed. `index` and `vitest`, plus the `checker`/`catalog` CLIs, are ESM-only — vitest is ESM-only and the CLIs are invoked as `node dist/*.js`. The `oxlint` plugin ships dual, because oxlint loads it from a consumer project that may itself be CommonJS.
78
-
79
- **There are FOUR entries and no more**: `@jterrazz/test` for everything a spec uses, `@jterrazz/test/vitest` for what `vitest.config.ts` needs, `@jterrazz/test/oxlint` for the lint plugin, `@jterrazz/test/schema` for an editor validating a `<case>.spec.yaml`. That is rule F1 rather than a convention of taste, and the exemption is derived from the manifest's own `exports` map (`src/lint/package-exports.ts`), so a subpath is exempt the moment it is published and stops being exempt the moment it is withdrawn.
80
-
81
- **The root entry has two runtimes.** A component test renders in a real browser, and the node entry cannot load there: it wires `pg`, `better-sqlite3`, `testcontainers` and `node:fs` at import. So `exports["."]` carries a `browser` condition resolving to `dist/browser/index.js` — the same public surface, built for a page, where every node-only name (the six constructors, the services, the docker accessors, the results that walk a disk) is a stub that throws where it was called, naming the facet and saying it runs under node. The split is the RUNTIME's and never the facets': one specifier, so no spec has to remember which entry it may import.
82
-
83
- `types` is stated once, and it is the node build's — so the two runtimes are ONE type surface and a name cannot exist on one side only. What both agree on lives in `src/surface.ts`, and the `package-exports` meta-test compares the two builds' exported names on every run.
84
-
85
- Two committed projections leave the code and land in the corpus: the API reference under `docs/reference/` (typedoc, through `typescript docs`) and the rule catalogue spliced into [19 — Linting](19-linting.md) and `skills/jterrazz-test/references/rules.md`. A third, `schema/spec.schema.json`, is generated from the document grammar's own constants. Both `docs/` and `schema/` ship in the tarball — the reference included, since the corpus links into it — so a consumer reads the manual where the messages send them, inside the install ([04 — Operating](04-operating.md#what-leaves-the-tree)). All three are regenerated by one gesture and sync-checked — see [02 — Developing](02-developing.md).
86
-
87
- ## Pitfalls
88
-
89
- - **Reaching for a dependency from `model/`.** A new external package belongs in a folder of `seams/` that imports it and nothing else; `model/` importing it directly is the one boundary this package cannot afford to blur, and `i1-layer-boundaries` refuses it.
90
- - **Adding a seventh constructor.** The six are the closed vocabulary the conventions, the linter and the documentation are all shaped around. A new SUBJECT to specify is a design decision, and it earns a record in [`decisions/`](decisions/) before it earns a constructor — as `integration` did in [ADR-004](decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md).
91
- - **Writing a rule's normative sentence into a chapter.** It belongs in `src/lint/manifest.ts` beside the implementation; a chapter that restates it is the second copy the whole design exists to prevent.
92
-
93
- ## Related
94
-
95
- [02 — Developing](02-developing.md) · [03 — Testing](03-testing.md) · [18 — Conventions](18-conventions.md) · [19 — Linting](19-linting.md)