@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
@@ -0,0 +1,374 @@
1
+ # API specs (`specification.api`)
2
+
3
+ `specification.api()` tests an HTTP API through real requests. Your app runs **in this process**, built by the `server` factory from the real services the runner started, so a request reaches it without a socket, and a stub, a clock and a golden all mean something because the app shares this process's world.
4
+
5
+ Use it when the subject under test is an HTTP surface. For background pipelines use [job](job-specs.md); for binaries use [cli](cli-specs.md).
6
+
7
+ ## What it specifies
8
+
9
+ An api spec answers one question: **given this state and this request, what does the HTTP app answer, and what does it leave behind?** The subject is the assembled app met through its own entry (the handler chain, the middleware, the serialisation, the database writes), and the request is a complete HTTP exchange, written as a file a person can read. A single handler function called directly is a module test's ([Module tests](module-tests.md)); a repository against a real database with no HTTP in sight is an integration spec's ([Integration specs](integration-specs.md)).
10
+
11
+ ### The app runs in THIS process
12
+
13
+ | Aspect | What it means |
14
+ | ------------------ | ------------------------------------------------------------------------------------------------------ |
15
+ | Your app | In-process, built by `server(services)`; no container, no socket |
16
+ | Real services | Containers for `postgres()` and `redis()`, a file copy for `sqlite()`, a child process for `process()` |
17
+ | What it proves | Application logic against real databases, caches and processes |
18
+ | Parallel isolation | Per-worker schema / db-index / file copy (rule `state-isolated-per-worker`) |
19
+
20
+ Because the app shares this process, three things the framework offers are real here and nowhere else: `.intercept()` (msw runs in-process), `.clock()` (it pins the `Date` the app reads), and a golden of the response the app actually built. What the SHIPPED artefact does (its Dockerfile, its wiring, its networking) is a deployment probe, and this specification does not claim it.
21
+
22
+ A `postgres()` service runs `docker/<service>/init.sql` when it starts, under the kebab-case of its record key; `sqlite()` takes its schema from its own `init` or `prismaSchema` option instead ([Services](../reference/services.md#dockerserviceinitsql)).
23
+
24
+ ## The constructor
25
+
26
+ ```typescript
27
+ // specs/api/api.specification.ts
28
+ import { afterAll } from 'vitest';
29
+ import { specification } from '@jterrazz/test';
30
+ import { postgres, redis } from '@jterrazz/test/services';
31
+ import { compose } from '../../src/composition/compose.js';
32
+ import type { App } from '../../src/composition/compose.js';
33
+ import { testConfig } from './__setup__/config.js';
34
+ import { idleMailer } from './__setup__/mailer.js';
35
+
36
+ let app: App | undefined;
37
+
38
+ export const { api, cleanup } = await specification.api({
39
+ services: {
40
+ db: postgres(), // → reported as "db", init from docker/db/
41
+ analyticsDb: postgres(), // → "analytics-db", init from docker/analytics-db/
42
+ cache: redis(), // → "cache"
43
+ },
44
+ // testConfig points the app's configuration at each started service
45
+ server: (services) => {
46
+ app = compose(testConfig(services), { mailer: idleMailer });
47
+ return app.server;
48
+ },
49
+ // root: absent, so auto-discovery (see below)
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await app?.[Symbol.asyncDispose]();
54
+ await cleanup();
55
+ });
56
+ ```
57
+
58
+ ### How the runner builds the app
59
+
60
+ `server` builds the app the way its process entry does: through the composition root, `compose(config, overrides)`. The runner hands it a configuration pointing at the started services, and, by name, the ports a spec must not reach for real (an agent, a mailer, a clock-driven worker), each a typed member of `Partial<Ports>`, so a wrong double fails the type check rather than a run. It hands the specification the part a request meets, the server, and keeps the app: the specification stops the services it started, never the app built on them, so the runner's `afterAll` disposes the app (the database connections and the workers it built), then stops any double it handed in that runs, which the app's dispose leaves to its caller, before `cleanup` stops the services under it. No container is looked up and nothing is cast: what `compose()` is and why the hexagonal map lets only the entry and the runners import it are `@jterrazz/typescript`'s [The composition root](https://github.com/jterrazz/package-typescript/blob/main/docs/concepts/the-composition-root.md).
61
+
62
+ ### Options
63
+
64
+ | Option | Required | Description |
65
+ | ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
66
+ | `services` | yes (if the app needs infra) | Named record of service factories (`postgres()`, `redis()`, `sqlite()`). Keys are your test vocabulary; see [services](../reference/services.md) |
67
+ | `server` | yes | `(services) => app`: receives the started services record, fully typed (rule `server-sees-declared-services`) |
68
+ | `root` | no | Override for root resolution, reserved for cases where the convention is not enough (rule `no-redundant-root`) |
69
+
70
+ ### Root resolution
71
+
72
+ Without `root`, the framework finds the package being tested by walking up from the specification file. The walk, the tree it reads and when a `root` is redundant are [Services § Root auto-discovery](../reference/services.md#root-auto-discovery)'s.
73
+
74
+ ## The chain
75
+
76
+ ### Setups (chainable)
77
+
78
+ | Setup | Description |
79
+ | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `.seed(file('./__seeds__/file.sql'))` | Load that `.sql` file, named by its path from the spec file, beside the test that reads it, or at the level of the tests that share it (rule `ground-shared-or-inline`) |
81
+ | `.seed(file('./__seeds__/file.sql'), { database })` | Target a database by its record key ([Services § The `database:` rule](../reference/services.md#the-database-rule)) |
82
+ | `.headers({ 'Name': 'value' })` | Set request headers; repeated calls merge |
83
+ | `.intercept(stub)` | Mock an outgoing HTTP call with a declared [stub](../reference/stubs.md) |
84
+ | `.intercept(trigger, response)` | Inline intercept for one-off cases |
85
+ | `.clock('2026-03-04T09:30:00Z')` | Pin the app's `Date` for this chain, released when the action resolves ([Time and doubles](../reference/time-and-doubles.md#the-clock)) |
86
+ | `api.with({ clock, headers })` | A view of the specification whose every chain starts with what the whole file shares ([Conventions](../concepts/conventions.md#what-a-file-shares)) |
87
+
88
+ Stubs are **strict** (rule `network-strict-after-first-stub`): once a chain declares one, an outgoing request no stub answers fails the spec ([Stubs § Strict by construction](../reference/stubs.md#strict-by-construction)). `.intercept()` and `.clock()` both work because the app runs in THIS process: msw intercepts its outgoing requests, and the pinned `Date` is the one it reads.
89
+
90
+ ```typescript
91
+ describe('language', () => {
92
+ test('serves french content', async () => {
93
+ // Given - headers inline
94
+ const result = await api.headers({ 'Accept-Language': 'fr' }).get('/welcome');
95
+
96
+ // Then - the whole response
97
+ expect(result.response).toMatch(`
98
+ HTTP/1.1 200 OK
99
+ content-type: application/json
100
+
101
+ {"message": "Bienvenue"}
102
+ `);
103
+ });
104
+ });
105
+ ```
106
+
107
+ #### Isolation between specs (rules `one-terminal-action-per-chain`, `sequence-expressed-by-seeding`)
108
+
109
+ Databases are **reset at the start of every chain**. A spec never depends on a previous spec, and there is no "flow" mode: sequential scenarios are expressed through seeds:
110
+
111
+ ```typescript
112
+ describe('isolation', () => {
113
+ test('starts clean between specs', async () => {
114
+ // Given - nothing (every chain resets the databases: one spec = ONE action)
115
+ const result = await api.get('/orders');
116
+
117
+ // Then
118
+ await expect(result.table('orders', { database: 'db' })).toBeEmpty();
119
+ });
120
+ });
121
+ ```
122
+
123
+ ### Actions (terminal)
124
+
125
+ Exactly one per chain (rule `one-terminal-action-per-chain`). Each executes the spec and resolves to the result.
126
+
127
+ | Action | Description |
128
+ | -------------------------------------------- | ------------------------------------------------------------------------------------ |
129
+ | ``.request(`POST /users ...`)`` | Execute the complete request written in the test, `.http` text, dedented |
130
+ | `.request(file('./__requests__/file.http'))` | Execute the complete request that `.http` file describes, when several tests send it |
131
+ | `.get(path, { params }?)` | Inline GET, for simple cases where a file would be excessive |
132
+ | `.post(path, body?, { params }?)` | Inline POST |
133
+ | `.put(path, body?, { params }?)` | Inline PUT |
134
+ | `.delete(path, { params }?)` | Inline DELETE |
135
+
136
+ The `body?` of `.post()` / `.put()` is a plain object, JSON-serialized with a `Content-Type: application/json` default header, or a body file from `__requests__/`, `.post(path, file('./__requests__/order.json'))`: a `.json` is parsed and sent as that value, any other extension is sent as its text. A complete request (method, headers and body) goes through `.request(file('./__requests__/file.http'))`, whose body section is sent **raw**: its surrounding whitespace is trimmed, but everything between is preserved byte-for-byte (interior double spaces, indentation, and non-JSON text are intact).
137
+
138
+ A route with parameters is written as the app declares it, its values beside it: `.get('/indicators/:symbol/chart', { params: { symbol: 'btc' } })`. The template types the params (every `:name` it holds takes a value, and no other name does: rule `route-written-as-template`), and the chain sends the path filled, each value URI-encoded, the query kept. The template is what the test's reader, the subject reader (`one-subject-per-spec`, `spec-in-subject-folder`) and route coverage read, so none of them has to guess that `btc` is a value. A path with no parameter takes no options, or `{ params: {} }` to state that it is the route itself.
139
+
140
+ There is no label argument: the vitest test name is the spec's only description (rule `no-chain-label`).
141
+
142
+ ```typescript
143
+ test('returns 404 with a useful body', async () => {
144
+ // Given - empty database
145
+ const result = await api.get('/users/999');
146
+
147
+ // Then - the whole response, written in place
148
+ expect(result.response).toMatch(`
149
+ HTTP/1.1 404 Not Found
150
+
151
+ {"error": "User 999 not found"}
152
+ `);
153
+ });
154
+ ```
155
+
156
+ ### `.http` request files: full format
157
+
158
+ Requests live in `__requests__/`, one file per request, extension `.http`; the body files a verb sends live beside them, each named with its extension (rule `request-file-extension-known`). A request file is the **complete** request: method + path on the first line, then headers, then a blank line, then the body.
159
+
160
+ ```http
161
+ ### __requests__/create-user.http
162
+ POST /users
163
+ Content-Type: application/json
164
+ Accept-Language: fr
165
+
166
+ { "name": "Alice" }
167
+ ```
168
+
169
+ Format rules:
170
+
171
+ - First line: `METHOD /path`; the file must start with it (rule `http-first-line-follows-its-grammar`). The path is relative to the app under test.
172
+ - Header lines: `Name: value`, one per line, immediately after the request line.
173
+ - Blank line, then the body (optional; a GET usually has none).
174
+ - Executed with `api.request(file('./__requests__/create-user.http'))`. The argument is the file's path from the spec file, spelled in full; the type refuses a bare name (rule `disk-named-by-relative-path`).
175
+
176
+ Expected responses live in `__expected__/`, like every other golden, same extension, and start with a status line (rule `expected-headers-are-a-subset`):
177
+
178
+ ```http
179
+ ### __expected__/user-created.http
180
+ HTTP/1.1 201 Created
181
+ Content-Type: application/json
182
+ Location: /users/{{uuid#user}}
183
+
184
+ { "id": "{{uuid#user}}", "name": "Alice" }
185
+ ```
186
+
187
+ - First line: `HTTP/1.1 <status>`, mandatory.
188
+ - Headers are matched as a **subset**: listed headers must match, unlisted headers are unconstrained (rule `expected-headers-are-a-subset`). A header name compares case-blind, as HTTP reads it, so `Content-Type` and `content-type` are one header; its value compares as written.
189
+ - Body and headers both accept `{{token}}` placeholders, including `#ref` captures: `{{uuid#user}}` above must be the _same_ UUID in the `Location` header and the body. See [tokens](../reference/tokens.md).
190
+ - The body is read by its `content-type` (the file's own, or the actual response's when the file states none), with its surrounding whitespace trimmed on both sides. A text type (`text/*`, `+xml`, `application/xml`, `application/x-ndjson`, ...) is the text it is, even a `42` or a `true`: an HTML page, an event stream, a plain message. A binary type (`audio/*`, `font/*`, `image/*`, `video/*`, and under `application/` exactly `gzip`, `octet-stream`, `pdf`, `protobuf`, `vnd.google.protobuf`, `wasm`, `x-gzip`, `x-protobuf`, `x-tar` and `zip`) is not read: its body is `null`, so its `.http` states the status and the headers and no body. Any other type (a JSON type, one the lists above do not name such as `application/graphql` or `multipart/*`, or no type at all) is the JSON value its text parses to, or its text when it does not parse.
191
+
192
+ ## The result
193
+
194
+ The result of an API action exposes read-only accessors (rule `result-accessor-read-only`); all assertions go through `expect()`:
195
+
196
+ | Member | Type | Description |
197
+ | --------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
198
+ | `result.status` | `number` | HTTP status code; assert with native `expect(...).toBe(...)` |
199
+ | `result.response` | response subject | Full response (status + headers + body), compared whole, `toMatch(<.http text>)` or `toMatch(file(...))` |
200
+ | `result.response.body` | body | The JSON value, or the text of a text body or one that is not JSON; `null` when empty or binary; for native assertions |
201
+ | `result.table(name, opts?)` | table subject | Database table, compared whole, `toMatch(<rows>)` / `toBeEmpty()` (async, `await expect`) |
202
+
203
+ `expect(result.response).toMatch(file('./__expected__/user-created.http'))` reads the path it is given, like every other golden matcher (rule `golden-read-from-its-path`): nothing resolves against a folder the line does not name. The full matcher reference is in [assertions](../reference/assertions.md).
204
+
205
+ Beyond the result, the `specification.api()` handle destructures to `{ api, cleanup, docker }`. The `docker(containerId)` reader lazily runs `docker inspect` and returns a `ContainerAccessor` for an arbitrary container id, usable with `await expect(docker(id)).toBeRunning()` and the sync read accessors (`.exists`, `.status`, `.file(path)`, logs). An unknown id yields `exists: false` instead of throwing. (`specification.job()` has no `docker` member; jobs never spawn containers.)
206
+
207
+ ## Unique here
208
+
209
+ ### Full example: a multi-database order flow
210
+
211
+ ```http
212
+ ### __requests__/new-order.http
213
+ POST /orders
214
+ Content-Type: application/json
215
+
216
+ { "sku": "KB-42", "quantity": 1 }
217
+ ```
218
+
219
+ ```http
220
+ ### __expected__/order-created.http
221
+ HTTP/1.1 201 Created
222
+ Content-Type: application/json
223
+ Location: /orders/{{uuid#order}}
224
+
225
+ {
226
+ "id": "{{uuid#order}}",
227
+ "status": "pending",
228
+ "total": "{{number}}",
229
+ "createdAt": "{{iso8601}}",
230
+ "paymentIntent": { "orderId": "{{uuid#order}}", "expiresAt": "{{iso8601}}" }
231
+ }
232
+ ```
233
+
234
+ ```typescript
235
+ // specs/api/orders/create/analytics.spec.ts: POST /orders, what it writes where
236
+ import { describe, expect, test } from 'vitest';
237
+ import { file, match } from '@jterrazz/test';
238
+ import { api } from '../../api.specification.js';
239
+
240
+ describe('analytics', () => {
241
+ test('ingests an order event into analytics', async () => {
242
+ // Given - catalog in the main database, analytics empty
243
+ const result = await api
244
+ .seed(file('./__seeds__/catalog.sql'), { database: 'db' })
245
+ .request(file('./__requests__/new-order.http'));
246
+
247
+ // Then - one row per database, targeted by record key
248
+ await expect(result.table('orders', { database: 'db' })).toMatch([
249
+ ['{{uuid}}', 'pending', '{{number}}', '{{iso8601}}'],
250
+ ]);
251
+ await expect(result.table('events', { database: 'analyticsDb' })).toMatch([
252
+ [1, '{{uuid}}', 'order_created'],
253
+ ]);
254
+ });
255
+
256
+ test('returns the created order with consistent ids', async () => {
257
+ // Given
258
+ const result = await api
259
+ .seed(file('./__seeds__/catalog.sql'), { database: 'db' })
260
+ .request(file('./__requests__/new-order.http'));
261
+
262
+ // Then - {{uuid#order}} appears 3 times (Location header included), all equal
263
+ expect(result.response).toMatch(file('./__expected__/order-created.http'));
264
+ });
265
+
266
+ test('links the analytics event to the created order', async () => {
267
+ // Given
268
+ const result = await api
269
+ .seed(file('./__seeds__/catalog.sql'), { database: 'db' })
270
+ .request(file('./__requests__/new-order.http'));
271
+
272
+ // Then - same id on both sides, without ever knowing its value
273
+ await expect(result.table('orders', { database: 'db' })).toMatch([
274
+ [match.ref('order'), 'pending', '{{number}}', '{{iso8601}}'],
275
+ ]);
276
+ await expect(result.table('events', { database: 'analyticsDb' })).toMatch([
277
+ [1, match.ref('order'), 'order_created'],
278
+ ]);
279
+ });
280
+ });
281
+ ```
282
+
283
+ > This project declares two databases, so every `.seed()` and `.table()` names one ([Services § The `database:` rule](../reference/services.md#the-database-rule)).
284
+
285
+ ### A case, written whole
286
+
287
+ A case is a TypeScript spec whose oracle is the WHOLE result, written where the test reads it: the given is the chain's setups (a seed is always a `.sql` file named with `file()`, beside the test when it alone reads it), the action is the request, and the response is compared whole to its `.http` text in place, tokens allowed ([Assertions](../reference/assertions.md#one-verb-compares-a-whole-result-and-its-argument-says-where-the-expectation-lives)):
288
+
289
+ ```typescript
290
+ test('the users seeded are the users listed, in the order they were inserted', async () => {
291
+ // Given - two users, from the seed beside the test
292
+ const result = await api.seed(file('./__seeds__/alice-and-bob.sql')).get('/users');
293
+
294
+ // Then - the whole response, and the table it read
295
+ expect(result.response).toMatch(`
296
+ HTTP/1.1 200 OK
297
+ content-type: application/json
298
+
299
+ {"users": [{"name": "Alice", "email": "alice@test.com"}, {"name": "Bob", "email": "bob@test.com"}]}
300
+ `);
301
+ await expect(result.table('users')).toMatch([
302
+ [1, 'Alice', 'alice@test.com'],
303
+ [2, 'Bob', 'bob@test.com'],
304
+ ]);
305
+ });
306
+ ```
307
+
308
+ A journey (a request carrying what an earlier response returned) is code: read the value from the first result, send it in the next chain. `TEST_UPDATE=1` writes an empty `toMatch('')` with what came back ([Tokens](../reference/tokens.md#a-value-written-in-place-is-rewritten-in-the-test)).
309
+
310
+ ### Route coverage
311
+
312
+ Line coverage says which lines of the app ran; it cannot say which routes no case asked for. The api project measures it at run time: every request a test sends is recorded, and after the last test the run names each route the app declares that no request reached. The measure is the `api()` project's global setup; a project written by hand measures nothing.
313
+
314
+ ```text
315
+ api routes: all 12 declared routes reached
316
+ ```
317
+
318
+ #### What is measured
319
+
320
+ - **The table is the app's own** (Hono's `app.routes`, read from the app each `specification.api()` started), so it is the table that served the tests, never a copy that can drift from it.
321
+ - **A middleware is left out.** An entry whose handler takes `next` is one, by Hono's own rule. A route `app.all()` declares is judged like any other, reached by a request of any method.
322
+ - **A path parameter** (`:id`) takes any one segment, a `*` tail the rest of the path.
323
+ - **A request written as a template is matched by its template**: a parameter where the route declares one, each literal segment as itself, so `.get('/users/:id', { params: { id: 'me' } })` reaches `GET /users/:id` and never a `GET /users/me` the filled path also fits.
324
+
325
+ #### The ratchet
326
+
327
+ A route no test reached fails the run, each one named. The count is a ratchet, held under `.jterrazz/baseline.json › test › routes/unreached`: `TEST_UPDATE=1` records where the run stands, and the checker's own recordings keep that key. The entry is exact in both directions: a run that reaches more routes than it held fails too, since the debt fell, until `TEST_UPDATE=1` records the lower count.
328
+
329
+ #### When a run is not judged
330
+
331
+ A run is judged, and `TEST_UPDATE=1` records, only when it is green and whole. Otherwise it prints a line and records nothing:
332
+
333
+ | The run | Prints | Why |
334
+ | ---------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
335
+ | a file of it failed | `api routes: not judged`, with the count of failed files | a failed file may not have sent its requests; `TEST_UPDATE=1` refuses and keeps the entry |
336
+ | a shard (`vitest --shard`) | `api routes: not judged` | its requests are a part of the run's; it writes its ledgers to `.artifacts/vitest/routes/shard-<index>-<count>/` |
337
+ | a merge (`vitest --merge-reports`) | nothing | a merge runs no global setup; this package's own split judges its routes after the merge, over every shard's ledgers ([Test § What CI runs](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#what-ci-runs)) |
338
+ | partial: some test files (`vitest --changed`, a file filter) or tests (`-t`) | `api routes: not judged, since a partial run ...` | it reached only some routes; a shard that `-t` filters marks its ledger folder partial, so its split is not judged at the merge |
339
+
340
+ #### An app that is not Hono
341
+
342
+ A `server` that returns anything but the Hono app itself exposes no table the specification reads, and fails the run, since a measure against no table would read as every route reached. It states its routes instead, as an OpenAPI document (JSON or YAML) named in `jterrazz.config.ts`:
343
+
344
+ ```typescript
345
+ // jterrazz.config.ts
346
+ import { defineConfig } from '@jterrazz/typescript/config';
347
+
348
+ export default defineConfig({ kind: 'api', platform: 'node', test: { routes: './openapi.yaml' } });
349
+ ```
350
+
351
+ Sending a request needs one more thing such an app may lack: `server` must return an object whose `request(path, init)` answers a Fetch `Response`, which is Hono's own method. Express and Fastify have none, so the project writes that wrapper, for instance one that starts the app on a free port once and forwards each call with `fetch`. Route coverage has no off switch: a project that cannot name its routes cannot run an api spec.
352
+
353
+ #### A route taken on any method
354
+
355
+ A route the app takes on any method (`app.all('/mcp', ...)`) is one the member names in `jterrazz.config.ts`, `test: { methodless: { '/mcp': 'the MCP transport answers GET and POST' } }`, so the checker files its specs under its path whatever verb they send ([Conventions § The subject, and its standard name](../concepts/conventions.md#the-subject-and-its-standard-name)). The run holds that list to the app's own table, and fails naming each mismatch, under `TEST_UPDATE=1` too, since the list is written by hand and nothing records it: the checker files the specs without running the app, so it reads the paths from the settings file, each with why the app takes it on any method:
356
+
357
+ - a route the table takes on any method that the list misses;
358
+ - a path the list names that the table takes on its own methods only, or holds nowhere.
359
+
360
+ The list is judged whenever the app exposes a route table of its own, an OpenAPI document declared or not; a `server` that is not the Hono app exposes none, and leaves the list unjudged.
361
+
362
+ The app runs in THIS process, always: there is no second mode, no container binding and no `TEST_MODE`. Why compose mode left, and what was weighed against keeping it: [ADR-007](https://github.com/jterrazz/package-test/blob/main/docs/decisions/007-compose-mode-leaves-the-package.md).
363
+
364
+ ## Pitfalls
365
+
366
+ - **Getting `database:` wrong for the number of databases.** See [Services § The `database:` rule](../reference/services.md#the-database-rule).
367
+ - **Expecting unlisted response headers to be constrained.** Response `__expected__/*.http` header matching is subset-only; if a header must be _absent_, that is not expressible in the file; assert on it in code.
368
+ - **Chaining two actions** (`api.get(...).get(...)`) or reusing state across tests. One chain = one terminal action; databases reset per chain (rules `one-terminal-action-per-chain`, `sequence-expressed-by-seeding`).
369
+ - **Forgetting that `.intercept()` is strict.** After the first `.intercept()`, an outgoing request no stub answers fails the spec ([Stubs § Strict by construction](../reference/stubs.md#strict-by-construction)).
370
+ - **Putting the request body in the test file when it has any substance.** Requests of more than a line or two belong in `__requests__/*.http`; inline `.post()` is for trivial cases.
371
+
372
+ ## Related
373
+
374
+ [Job specs](job-specs.md) · [Assertions](../reference/assertions.md) · [Tokens](../reference/tokens.md) · [Stubs](../reference/stubs.md) · [Services](../reference/services.md)