@jterrazz/test 16.1.1 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +2 -2980
  3. package/dist/browser/services.js +2 -0
  4. package/dist/browser/stubs.js +2 -0
  5. package/dist/checker.d.ts +6 -1
  6. package/dist/checker.js +4534 -2628
  7. package/dist/config.d.ts +40 -0
  8. package/dist/config.js +309 -0
  9. package/dist/in-place.global-setup.d.ts +11 -0
  10. package/dist/in-place.global-setup.js +21 -0
  11. package/dist/index.d.ts +1653 -3004
  12. package/dist/index.js +5226 -3845
  13. package/dist/mobile-session.global-setup.d.ts +14 -0
  14. package/dist/mobile-session.global-setup.js +31 -0
  15. package/dist/oxlint.cjs +6069 -5634
  16. package/dist/oxlint.d.cts +43 -7212
  17. package/dist/oxlint.d.ts +43 -7212
  18. package/dist/oxlint.js +6127 -5693
  19. package/dist/route-coverage.global-setup.d.ts +42 -0
  20. package/dist/route-coverage.global-setup.js +2 -0
  21. package/dist/services.d.ts +3 -0
  22. package/dist/services.global-setup.d.ts +43 -0
  23. package/dist/services.global-setup.js +2 -0
  24. package/dist/services.js +597 -0
  25. package/dist/shared-appium.adapter.js +428 -0
  26. package/dist/shared-artifacts.js +32 -0
  27. package/dist/shared-chapter.js +7 -0
  28. package/dist/shared-container.port.d.ts +31 -0
  29. package/dist/shared-derived-projects.js +58 -0
  30. package/dist/shared-device.port.d.ts +189 -0
  31. package/dist/shared-exchange.js +447 -0
  32. package/dist/shared-in-place.js +172 -0
  33. package/dist/shared-index.browser.js +3191 -0
  34. package/dist/shared-index.services.d.ts +346 -0
  35. package/dist/shared-index.stubs.d.ts +287 -0
  36. package/dist/shared-json-shape.js +17 -0
  37. package/dist/shared-landmarks.js +71 -0
  38. package/dist/shared-ledger.js +68 -0
  39. package/dist/shared-literate-plugin.js +183 -0
  40. package/dist/shared-manifest.js +1321 -0
  41. package/dist/shared-pause.js +11 -0
  42. package/dist/shared-peer.js +88 -0
  43. package/dist/shared-playwright.adapter.js +330 -0
  44. package/dist/shared-process.adapter.js +74 -0
  45. package/dist/shared-refusal.js +35 -0
  46. package/dist/shared-resolve.js +24 -0
  47. package/dist/shared-route-coverage.global-setup.js +187 -0
  48. package/dist/shared-scenario-failure.js +198 -0
  49. package/dist/shared-serve.js +365 -0
  50. package/dist/shared-service-images.js +10 -0
  51. package/dist/shared-services.global-setup.js +179 -0
  52. package/dist/shared-session-ledger.js +301 -0
  53. package/dist/shared-setup-entry.js +12 -0
  54. package/dist/shared-source-scan.js +133 -0
  55. package/dist/shared-spec-document.js +365 -0
  56. package/dist/shared-specifications.js +26 -0
  57. package/dist/shared-stream-body.js +130 -0
  58. package/dist/shared-stub-server.js +190 -0
  59. package/dist/shared-temp-root.global-setup.js +80 -0
  60. package/dist/shared-test-baseline.js +26 -0
  61. package/dist/shared-test-section.js +58 -0
  62. package/dist/shared-testcontainers.adapter.js +203 -0
  63. package/dist/shared-tokens.js +55 -0
  64. package/dist/shared-yaml-document.js +119 -0
  65. package/dist/stubs.d.ts +2 -0
  66. package/dist/stubs.js +657 -0
  67. package/dist/temp-root.global-setup.d.ts +7 -0
  68. package/dist/temp-root.global-setup.js +2 -0
  69. package/dist/vitest.d.ts +97 -163
  70. package/dist/vitest.js +373 -525
  71. package/docs/README.md +82 -36
  72. package/docs/concepts/architecture.md +118 -0
  73. package/docs/concepts/conventions.md +333 -0
  74. package/docs/guides/api-specs.md +374 -0
  75. package/docs/guides/cli-specs.md +564 -0
  76. package/docs/guides/component-specs.md +252 -0
  77. package/docs/guides/develop.md +94 -0
  78. package/docs/guides/integration-specs.md +181 -0
  79. package/docs/guides/job-specs.md +180 -0
  80. package/docs/guides/lint-specs.md +174 -0
  81. package/docs/guides/mobile-specs.md +319 -0
  82. package/docs/guides/module-tests.md +149 -0
  83. package/docs/guides/release.md +43 -0
  84. package/docs/guides/spec-documents.md +189 -0
  85. package/docs/guides/website-specs.md +413 -0
  86. package/docs/reference/assertions.md +345 -0
  87. package/docs/reference/glossary.md +20 -0
  88. package/docs/reference/linting.md +483 -0
  89. package/docs/reference/services.md +236 -0
  90. package/docs/reference/stubs.md +498 -0
  91. package/docs/reference/time-and-doubles.md +137 -0
  92. package/docs/reference/tokens.md +258 -0
  93. package/docs/reference/visitor.md +221 -0
  94. package/docs/start/getting-started.md +187 -0
  95. package/docs/start/set-up-a-project.md +214 -0
  96. package/package.json +50 -30
  97. package/schema/spec.schema.json +43 -17
  98. package/dist/appium.adapter.js +0 -489
  99. package/dist/artifacts.js +0 -32
  100. package/dist/fixtures.js +0 -110
  101. package/dist/literate.js +0 -1747
  102. package/dist/match.js +0 -804
  103. package/dist/playwright.adapter.js +0 -438
  104. package/dist/server.js +0 -160
  105. package/dist/stream.js +0 -321
  106. package/dist/substring-warning.js +0 -282
  107. package/docs/01-architecture.md +0 -95
  108. package/docs/02-developing.md +0 -351
  109. package/docs/03-testing.md +0 -495
  110. package/docs/04-operating.md +0 -101
  111. package/docs/05-module-tests.md +0 -152
  112. package/docs/06-integration.md +0 -180
  113. package/docs/07-component.md +0 -246
  114. package/docs/08-website.md +0 -404
  115. package/docs/09-mobile.md +0 -270
  116. package/docs/10-api.md +0 -264
  117. package/docs/11-jobs.md +0 -157
  118. package/docs/12-cli.md +0 -677
  119. package/docs/13-elements.md +0 -248
  120. package/docs/14-assertions.md +0 -314
  121. package/docs/15-tokens.md +0 -253
  122. package/docs/16-contracts.md +0 -438
  123. package/docs/17-services.md +0 -188
  124. package/docs/18-conventions.md +0 -332
  125. package/docs/19-linting.md +0 -512
  126. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  127. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  128. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  129. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  130. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  131. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  132. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  133. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  134. package/docs/decisions/_template.md +0 -22
  135. package/docs/reference/classes/Matcher.md +0 -120
  136. package/docs/reference/classes/ProcessHandle.md +0 -368
  137. package/docs/reference/functions/defineContract.md +0 -39
  138. package/docs/reference/functions/defineContracts.md +0 -36
  139. package/docs/reference/functions/disabled.md +0 -28
  140. package/docs/reference/functions/enabled.md +0 -20
  141. package/docs/reference/functions/focused.md +0 -26
  142. package/docs/reference/functions/mockOf.md +0 -79
  143. package/docs/reference/functions/postgres.md +0 -31
  144. package/docs/reference/functions/process.md +0 -33
  145. package/docs/reference/functions/redis.md +0 -28
  146. package/docs/reference/functions/required.md +0 -41
  147. package/docs/reference/functions/selected.md +0 -26
  148. package/docs/reference/functions/sqlite.md +0 -33
  149. package/docs/reference/functions/testId.md +0 -20
  150. package/docs/reference/functions/text.md +0 -40
  151. package/docs/reference/functions/valued.md +0 -28
  152. package/docs/reference/functions/waitUntil.md +0 -46
  153. package/docs/reference/functions/within.md +0 -29
  154. package/docs/reference/index.md +0 -196
  155. package/docs/reference/interfaces/BaseResult.md +0 -87
  156. package/docs/reference/interfaces/CallResult.md +0 -160
  157. package/docs/reference/interfaces/CaptureScope.md +0 -82
  158. package/docs/reference/interfaces/CliResult.md +0 -253
  159. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  160. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  161. package/docs/reference/interfaces/FetchResult.md +0 -180
  162. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  163. package/docs/reference/interfaces/HttpResult.md +0 -126
  164. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  165. package/docs/reference/interfaces/PageResult.md +0 -356
  166. package/docs/reference/interfaces/RenderResult.md +0 -72
  167. package/docs/reference/interfaces/RenderedText.md +0 -110
  168. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  169. package/docs/reference/interfaces/ScreenResult.md +0 -128
  170. package/docs/reference/interfaces/TableAccessor.md +0 -26
  171. package/docs/reference/interfaces/TextAccessor.md +0 -96
  172. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  173. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  174. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  175. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  176. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  177. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  178. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  179. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  180. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  181. package/docs/reference/type-aliases/CliEnv.md +0 -11
  182. package/docs/reference/type-aliases/CliHandle.md +0 -80
  183. package/docs/reference/type-aliases/CliInput.md +0 -36
  184. package/docs/reference/type-aliases/CliOutput.md +0 -46
  185. package/docs/reference/type-aliases/CliPort.md +0 -58
  186. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  187. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  188. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  189. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  190. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  191. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  192. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  193. package/docs/reference/type-aliases/Contract.md +0 -68
  194. package/docs/reference/type-aliases/ContractInput.md +0 -13
  195. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  196. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  197. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  198. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  199. package/docs/reference/type-aliases/Contracts.md +0 -48
  200. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  201. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  202. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  203. package/docs/reference/type-aliases/DevicePort.md +0 -51
  204. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  205. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  206. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  207. package/docs/reference/type-aliases/DomMount.md +0 -21
  208. package/docs/reference/type-aliases/ElementKind.md +0 -27
  209. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  210. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  211. package/docs/reference/type-aliases/ElementRef.md +0 -128
  212. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  213. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  214. package/docs/reference/type-aliases/HonoApp.md +0 -31
  215. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  216. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  217. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  218. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  219. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  220. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  221. package/docs/reference/type-aliases/Intercept.md +0 -19
  222. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  223. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  224. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  225. package/docs/reference/type-aliases/JobHandle.md +0 -34
  226. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  227. package/docs/reference/type-aliases/JobsResult.md +0 -16
  228. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  229. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  230. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  231. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  232. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  233. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  234. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  235. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  236. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  237. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  238. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  239. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  240. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  241. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  242. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  243. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  244. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  245. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  246. package/docs/reference/type-aliases/MockPort.md +0 -10
  247. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  248. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  249. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  250. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  251. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  252. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  253. package/docs/reference/type-aliases/ServerPort.md +0 -36
  254. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  255. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  256. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  257. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  258. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  259. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  260. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  261. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  262. package/docs/reference/type-aliases/SpecKind.md +0 -8
  263. package/docs/reference/type-aliases/SpecRun.md +0 -108
  264. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  265. package/docs/reference/type-aliases/SpecStream.md +0 -32
  266. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  267. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  268. package/docs/reference/type-aliases/SseEvent.md +0 -58
  269. package/docs/reference/type-aliases/StreamBody.md +0 -61
  270. package/docs/reference/type-aliases/TextFilter.md +0 -22
  271. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  272. package/docs/reference/type-aliases/Visitor.md +0 -215
  273. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  274. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  275. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  276. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  277. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  278. package/docs/reference/variables/anthropic.md +0 -19
  279. package/docs/reference/variables/banner.md +0 -10
  280. package/docs/reference/variables/button.md +0 -10
  281. package/docs/reference/variables/clock.md +0 -16
  282. package/docs/reference/variables/complementary.md +0 -10
  283. package/docs/reference/variables/component.md +0 -10
  284. package/docs/reference/variables/content.md +0 -10
  285. package/docs/reference/variables/contentinfo.md +0 -10
  286. package/docs/reference/variables/dialog.md +0 -10
  287. package/docs/reference/variables/field.md +0 -10
  288. package/docs/reference/variables/form.md +0 -10
  289. package/docs/reference/variables/heading.md +0 -10
  290. package/docs/reference/variables/http.md +0 -40
  291. package/docs/reference/variables/intercept.md +0 -11
  292. package/docs/reference/variables/link.md +0 -10
  293. package/docs/reference/variables/listitem.md +0 -10
  294. package/docs/reference/variables/main.md +0 -10
  295. package/docs/reference/variables/match.md +0 -8
  296. package/docs/reference/variables/navigation.md +0 -10
  297. package/docs/reference/variables/openai.md +0 -21
  298. package/docs/reference/variables/option.md +0 -17
  299. package/docs/reference/variables/region.md +0 -10
  300. package/docs/reference/variables/row.md +0 -10
  301. package/docs/reference/variables/search.md +0 -10
  302. package/docs/reference/variables/specification.md +0 -8
  303. package/docs/reference/variables/status.md +0 -10
  304. package/docs/reference/variables/table.md +0 -10
package/docs/09-mobile.md DELETED
@@ -1,270 +0,0 @@
1
- # 09 — Mobile specs (`specification.mobile`)
2
-
3
- `specification.mobile()` tests a native app on the iOS simulator — its screens (the accessibility tree a user's assistive tech sees) and its flows (tap, fill, see) — through a real XCUITest session driven by appium. It resolves and boots the simulator itself, and starts the appium server from the caller project.
4
-
5
- Use it when the subject under test is an installed native app. For a browser-rendered page use [website](08-website.md); for a JSON/HTTP API surface use [api](10-api.md).
6
-
7
- ## What it specifies
8
-
9
- A mobile spec answers one question: **given this backend and this visitor, what does the native app draw on a simulator, and what does the screen read like to someone who cannot see it?** The subject is the installed app met through a deep link, driven by the accessibility tree the platform publishes. A React Native component rendered off a device is jest's for now, not this facet's ([07](07-component.md) states the boundary and the spike that would move it).
10
-
11
- ## The constructor
12
-
13
- ```typescript
14
- // specs/mobile/mobile.specification.ts
15
- import { specification } from '@jterrazz/test';
16
- import { afterAll } from 'vitest';
17
-
18
- export const { cleanup, mobile } = await specification.mobile({
19
- app: { bundleId: 'com.jterrazz.fakenews' },
20
- device: { name: 'iPhone 17', os: '26.5' },
21
- });
22
-
23
- afterAll(cleanup);
24
- ```
25
-
26
- ### Options
27
-
28
- | Option | Description |
29
- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | `app` | `{ bundleId }` — the installed app under test. Every `.open()` terminates and relaunches it |
31
- | `device` | `{ name, os?, udid? }` — the simulator to run on, resolved through `xcrun simctl` and booted when shut down |
32
- | `backend` | `{ port? }` — start a declared stub backend; the handle gains `backendUrl` — see [Declared backend](#declared-backend) |
33
- | `root` | **Project-root override** (rule A9): where the `appium` binary is resolved from (`node_modules/.bin`). Auto-discovered from the calling file when absent |
34
- | `services` | A named record started with the runner and stopped with it — a `process()` for the bundler the app loads from, a database a dev build reads |
35
- | `timeouts` | `{ action?, launch? }` in ms — how long this runner waits. Each field falls back to the framework's default; see [Declared timeouts](#declared-timeouts) |
36
-
37
- `device` itself takes:
38
-
39
- | `device` field | Description |
40
- | -------------- | ---------------------------------------------------------------------------------------------- |
41
- | `name` | Simulator name, matched exactly (`'iPhone 17'`) |
42
- | `os` | OS narrowing when the name exists on several runtimes — `'26.5'` or `'iOS 26.5'`, case-relaxed |
43
- | `udid` | Explicit UDID — skips name/os resolution entirely |
44
-
45
- Resolution refuses rather than guesses: zero matches and several matches both fail with the full `simctl` device listing, so the fix never needs an Xcode round-trip. A shut-down simulator is booted (`simctl boot` + `bootstatus`); an already-booted one is reused as-is.
46
-
47
- **`services` is where Metro goes.** A dev build loads its JavaScript from a bundler, and until now that bundler was a `beforeAll` the repository maintained, with an `afterAll` it sometimes forgot. It is a `process()` like any other external process the framework owns, started before the app is ever launched and stopped with the specification ([17 — Services](17-services.md#process--the-one-shape-an-external-process-takes)):
48
-
49
- ```typescript
50
- import { process, specification } from '@jterrazz/test';
51
-
52
- export const { cleanup, mobile, udid } = await specification.mobile({
53
- app: { bundleId: 'com.example.app' },
54
- device: { name: 'iPhone 17' },
55
- services: {
56
- metro: process({ command: 'expo start', ready: /Metro waiting on .*:(\d+)/u }),
57
- },
58
- });
59
- ```
60
-
61
- The appium server is spawned from the caller project's `node_modules/.bin/appium` on a free port and polled on `/status` until ready. On teardown the driver session ends and the server process group is terminated (SIGTERM, escalating to SIGKILL after a 2 s grace) — the same escalation as the [website](08-website.md) serve adapter.
62
-
63
- The handle destructures to `{ mobile, cleanup, udid }` (rule A3) — `udid` is the resolved simulator, handy for shelling out to `simctl` in a debugging session. With the `backend` option it additionally carries `backendUrl`.
64
-
65
- ### Declared timeouts
66
-
67
- Every verb auto-waits — there is no sleep and no conditional helper. `timeouts` states how long, for the two waits a project legitimately needs to move:
68
-
69
- | `timeouts` field | The wait it budgets | Default |
70
- | ---------------- | ------------------------------------------------------------------------------------------- | --------- |
71
- | `action` | one visible match, on every verb — the whole `see`/`tap`/`fill` budget, polled every 500 ms | `30_000` |
72
- | `launch` | WebDriverAgent to build and launch, on a run's first session | `240_000` |
73
-
74
- The defaults suit a **release** bundle: 30 s absorbs a cold app boot, the same figure playwright chose for its actionability timeout. A project driving a **dev** build pays its bundler's cold boot on top — a Metro cold start to first content measures 31–32 s, over the default — and states that wait once, on the runner, rather than sleeping inside its scenarios:
75
-
76
- ```typescript
77
- // specs/mobile/mobile.specification.ts
78
- export const { cleanup, mobile } = await specification.mobile({
79
- app: { bundleId: 'com.jterrazz.fakenews' },
80
- device: { name: 'iPhone 17', os: '26.5' },
81
- // Dev-mode Metro cold boot measured at 31-32s — over the 30s default.
82
- // 45s covers the cold path with margin without hiding a real hang.
83
- timeouts: { action: 45_000 },
84
- });
85
-
86
- afterAll(cleanup);
87
- ```
88
-
89
- Raise it to the slowest **honest** path and no further: the timeout is what turns a hang into a legible refusal naming the element and what was on screen instead.
90
-
91
- ## The chain
92
-
93
- ### One terminal action: `.open(deepLink?, scenario?)`
94
-
95
- `.open()` is terminal and deterministic: it **terminates and relaunches the app** (fresh state every spec), applies the deep link when given, runs the scenario, then captures the final screen — the projected accessibility tree plus the visible texts:
96
-
97
- ```typescript
98
- test('shows the events feed behind its deep link', async () => {
99
- // Given - the events screen
100
- const result = await mobile.open('news://events');
101
-
102
- // Then - one golden covers the whole screen
103
- expect(result.screen).toMatch('events.screen.json');
104
- });
105
- ```
106
-
107
- Without a deep link, the app opens on its launch screen. There is **one driver session per runner**, created lazily on the first `.open()` and reused — the expensive part is WebDriverAgent startup, not the app relaunch.
108
-
109
- ### Open scenarios — the When
110
-
111
- A scenario is the interaction that happens **before** the capture — the open's When. The capture always reflects the **final** screen state, after the scenario ran:
112
-
113
- ```typescript
114
- test('bookmarks an event from its detail screen', async () => {
115
- // Given - a visitor on the events feed
116
- const result = await mobile.open('news://events', async (visitor) => {
117
- // When - they open the Fauci event and bookmark it
118
- await visitor.tap(button('Enquête Fauci COVID-19'));
119
- await visitor.see(content('12 rapports publiés'));
120
- await visitor.tap(button('Bookmark'));
121
- });
122
-
123
- // Then - the capture reflects the screen after the interaction
124
- expect(result.content).toContain('Bookmarked');
125
- });
126
- ```
127
-
128
- **No `expect()` inside a scenario (rule W1).** A scenario is pure interaction — assertions live in the Then, on the returned result.
129
-
130
- The element vocabulary is [13 — Elements](13-elements.md)'s, the same one the website and component facets name things with — there is no CSS, no XPath and no raw predicate surface. What a device does with it is this chapter's, and it is three things.
131
-
132
- **The verbs are three.** `tap(element)` is the device's word for a click, `fill(element, value)` fills a text field, and `see(element)` is the only synchronization primitive. There is no `gone()` here: the adapter cannot answer honestly for absence on an XCUITest tree. Every verb polls until at least one visible match exists (default 30 s, sized for a cold app boot), then enforces the verb's cardinality; there are no sleeps anywhere in the framework. Actionability includes **scroll-into-view**: when a descriptor matches nothing visible but an off-screen element exists, the adapter scrolls it into view once (directly by element id) and keeps polling — elements buried in very heavy screens (a full-page WebView) may still be out of reach, so prefer a deep link then.
133
-
134
- **Four descriptors reach a screen** — `button(name)` (accessible label, `XCUIElementTypeButton`), `field(label)` (accessible name or value, text and secure-text fields), `content(text)` (any element whose label or value IS the text — the whole-name default of [13 — Elements](13-elements.md#a-name-designates-the-accessible-name-whole), compiled to `==`; `{ exact: false }` compiles to `CONTAINS`) and `testId(id)` (the accessibility identifier, rule W2's escape hatch, with the line saying what the element lacks). The link, heading, dialog, table and option roles, the landmarks and the state modifiers are not part of a device's tree: passing one to a mobile verb refuses at runtime with a message naming the boundary.
135
-
136
- **W3 is relaxed for `see()` alone.** `tap` and `fill` refuse an ambiguous descriptor and print the candidates with their accessibility identifiers; `see()` does not — [13 — Elements § `see()` and `gone()`](13-elements.md#see-and-gone-are-the-synchronization) says why, once.
137
-
138
- A mobile scope is any descriptor, most often a `testId()` on the containing view, because iOS has no landmarks:
139
-
140
- ```typescript
141
- await visitor.tap(within(testId('event-list'), button('Bookmark')));
142
- ```
143
-
144
- `see()` resolving through render work is the point: a deep link into a Metro cold bundle, a network round-trip, an animation — the poll absorbs them all without a single sleep.
145
-
146
- ## The result
147
-
148
- | Member | Type | Description |
149
- | ---------------- | -------------- | ---------------------------------------------------------------------------- |
150
- | `result.screen` | `JsonAccessor` | The projected accessibility tree — the one-golden-per-screen surface |
151
- | `result.content` | `TextAccessor` | The visible texts, one per line in reading order — the scalpel for one probe |
152
-
153
- ### The `screen` golden — one per screen
154
-
155
- `result.screen` is the **stable, assertion-friendly projection** of the XCUITest page source. The raw source is deep and noisy — React Native wraps every view in layers of unlabeled `XCUIElementTypeOther` nodes — so the projection collapses it: wrapper nodes with no label, no value and no identifier are dropped (their children hoisted), an element repeated as its own child appears once, type names lose the `XCUIElementType` prefix, and a value or identifier merely echoing the label is omitted:
156
-
157
- ```json
158
- {
159
- "type": "Application",
160
- "label": "SigNews",
161
- "children": [
162
- { "type": "StaticText", "label": "Signals" },
163
- { "type": "Button", "label": "Enquête Fauci COVID-19" },
164
- { "type": "TextField", "label": "Email", "value": "a@b.test", "identifier": "email-field" }
165
- ]
166
- }
167
- ```
168
-
169
- It is the one golden a screen needs:
170
-
171
- ```typescript
172
- test('renders the events feed', async () => {
173
- // Given - the events screen
174
- const result = await mobile.open('news://events');
175
-
176
- // Then - the whole projected tree matches one golden
177
- expect(result.screen).toMatch('events.screen.json');
178
- });
179
- ```
180
-
181
- The tree describes the **whole mounted hierarchy**, including rows below the fold; `result.content` carries only what is visible. Volatile parts (dates, counters) are covered by the usual `{{token}}` grammar ([15 — Tokens](15-tokens.md)); generate with `TEST_UPDATE=1`.
182
-
183
- ## Unique here
184
-
185
- ### Declared backend
186
-
187
- A native app usually talks to an API. The `backend` option starts a small **stub backend** (plain `node:http`, no extra dependency) with the runner; what it serves is declared per chain, as [contracts](16-contracts.md) — the feature's `contracts/` facade, exactly the form `api`/`jobs` use:
188
-
189
- ```typescript
190
- // specs/mobile/mobile.specification.ts
191
- export const { backendUrl, cleanup, mobile } = await specification.mobile({
192
- app: { bundleId: 'com.jterrazz.fakenews' },
193
- backend: { port: 4820 },
194
- device: { name: 'iPhone 17', os: '26.5' },
195
- });
196
-
197
- afterAll(cleanup);
198
- ```
199
-
200
- ```typescript
201
- // specs/mobile/events/feed.spec.ts
202
- import newsroom from './contracts/newsroom.contracts.js';
203
-
204
- test('renders the events feed from the declared backend', async () => {
205
- // Given - the backend under contract for this chain
206
- const result = await mobile.intercept(newsroom).open('news://events');
207
-
208
- // Then - the screen rendered what the stub declared
209
- expect(result.screen).toMatch('events.screen.json');
210
- });
211
- ```
212
-
213
- | `backend` field | Description |
214
- | --------------- | --------------------------------------------------------------------------------- |
215
- | `port` | Fixed port — pins a stable stub URL across runs. Default: a free OS-assigned port |
216
-
217
- **The ownership boundary.** The framework owns the simulator and the appium server — it does NOT own the JS bundler: Metro belongs to the caller's repo, exactly like `next build` belongs to a website's. So nothing is injected anywhere; the handle exposes `backendUrl` and **the caller wires it into its own bundler env** (e.g. `EXPO_PUBLIC_API_URL=<backendUrl> npx expo start`). This is why `port` exists: Metro inlines `EXPO_PUBLIC_*` values at bundle-serve time, and a stable port lets a warm Metro survive between runs instead of re-bundling against a fresh URL.
218
-
219
- The stub behaves exactly as on the website facet ([08 — Website specs](08-website.md#declared-backend)): it **resets between chains** (one chain = one terminal action); selection is the shared queue (first non-exhausted match wins, no `times` = unlimited); a request matching no declared contract is answered **501 and recorded**, and the `.open()` then **throws** an error enumerating every unmatched request (method, path, count) — screenshots and other failure evidence are captured first, as always. A chain with zero contracts leaves the stub unguarded.
220
-
221
- ### Evidence on failure
222
-
223
- When a scenario throws — an element never appears, a `see()` times out — the error carries a screenshot of the state the scenario died in, referenced by its temp path in the error message. A timeout additionally includes a compact excerpt of the labels currently on screen, so the failure reads without relaunching anything. The original error is never masked.
224
-
225
- ### Requirements — what the machine must have
226
-
227
- Provisioning the simulator environment is not this package's job. The framework needs, in this order:
228
-
229
- 1. **Xcode + an iOS simulator** — `xcrun simctl list devices` must show the device named in `device`.
230
- 2. **The app installed on that simulator** — `.open()` launches by bundle id; it does not build or install. Install with your build tooling (`xcrun simctl install <udid> <path>.app`, Expo/EAS, xcodebuild).
231
- 3. **appium + webdriverio + the XCUITest driver** in the caller project — both are optional peer dependencies:
232
-
233
- ```bash
234
- npm install -D appium webdriverio && npx appium driver install xcuitest
235
- ```
236
-
237
- Calling `specification.mobile()` (or `.open()`) without them throws exactly that guidance — there is nothing else to search for.
238
-
239
- ### Folder layout
240
-
241
- ```
242
- specs/mobile/
243
- ├── mobile.specification.ts # runner at the facet ROOT (rule C1)
244
- └── <domain>/
245
- ├── <aspect>.spec.ts
246
- ├── _expected/ # ALL expected fixtures, FLAT (*.screen.json, …)
247
- └── contracts/ # what the declared backend serves — with the `backend` option
248
- ├── newsroom.contracts.ts
249
- └── http/…
250
- ```
251
-
252
- No `_seeds/` or `_requests/` — `specification.mobile()` has no `services` option and no request-file format; `.open()` calls are inline, and the golden is always `_expected/<name>`.
253
-
254
- ## Pitfalls
255
-
256
- - **Using `expect()` inside a scenario callback.** Forbidden (rule W1) — the scenario is the When; assertions belong on the result the `.open()` promise resolves to.
257
- - **The first session builds WebDriverAgent (~40 s, once per simulator).** Subsequent sessions on a warm simulator are seconds. Budget the first run; do not "fix" it with retries.
258
- - **Passing a landmark (`main()`, `navigation()`) to a mobile verb.** They are website concepts — iOS has no ARIA regions; the runtime refusal points at `within(testId('…'), …)` as the scoping tool.
259
- - **Reaching for `testId()` as the default locator.** It exists as an escape hatch (rule W2 warns) — prefer `button`/`field`/`content`, the vocabulary the accessibility tree exposes to a real user.
260
- - **Expecting `.open()` to preserve app state between specs.** It never does — every open terminates and relaunches the app; a flow that spans screens belongs in ONE scenario.
261
- - **Asserting on off-screen content with `result.content`.** `content` carries only _visible_ texts; the mounted-but-offscreen rows live in `result.screen`. Use the golden for the whole list, `content` for what the user currently sees.
262
- - **Calling `specification.mobile()` without appium installed.** The error names the exact fix — `npm install -D appium webdriverio && npx appium driver install xcuitest` — there is no silent fallback.
263
- - **Pointing `device.name` at an ambiguous simulator.** When the same name exists on several OS runtimes the constructor refuses with the listing — narrow with `os:` (or pin `udid:`), do not delete simulators to make it pass.
264
- - **Calling `.intercept()` without the `backend` option.** It throws immediately, naming the fix: add `backend: { … }` to the `specification.mobile()` options — a chain's `.http` exchanges need a stub to serve them.
265
- - **Sleeping around a slow boot instead of declaring it.** A dev bundle that boots past the 30 s default makes every verb refuse; the answer is `timeouts: { action: … }` on the runner, once — an arbitrary wait inside a scenario is untyped, per-spec, and hides the next regression.
266
- - **Expecting the framework to point the app at the stub.** It never touches the bundler — that is the caller's territory. Wire `backendUrl` into your own bundler env (`EXPO_PUBLIC_API_URL`), and pin `port` so a warm Metro keeps its inlined URL.
267
-
268
- ## Related
269
-
270
- [02 — Developing](02-developing.md) · [14 — Assertions](14-assertions.md) · [15 — Tokens](15-tokens.md) · [08 — Website specs](08-website.md)
package/docs/10-api.md DELETED
@@ -1,264 +0,0 @@
1
- # 10 — 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 services the runner started in real containers — so a request reaches it without a socket, and a contract, 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 [jobs](11-jobs.md); for binaries use [cli](12-cli.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 ([05](05-module-tests.md)); a repository against a real database with no HTTP in sight is an integration spec's ([06](06-integration.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
- | Services | Real containers, started by testcontainers from each handle's own defaults |
17
- | What it proves | Application logic against real databases, caches and processes |
18
- | Parallel isolation | Per-worker schema / db-index / file copy (rule G2) |
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 facet does not claim it.
21
-
22
- `docker/<service>/init.sql` runs when the corresponding service starts, under the kebab-case of its record key. See [services](17-services.md).
23
-
24
- ## The constructor
25
-
26
- ```typescript
27
- // specs/api/api.specification.ts
28
- import { afterAll } from 'vitest';
29
- import { specification, postgres, redis } from '@jterrazz/test';
30
- import { createApp } from '../../src/app.js';
31
-
32
- export const { api, cleanup } = await specification.api({
33
- services: {
34
- db: postgres(), // → reported as "db", init from docker/db/
35
- analyticsDb: postgres(), // → "analytics-db", init from docker/analytics-db/
36
- cache: redis(), // → "cache"
37
- },
38
- server: ({ db, analyticsDb, cache }) =>
39
- createApp({
40
- databaseUrl: db.connectionString,
41
- analyticsDatabaseUrl: analyticsDb.connectionString,
42
- redisUrl: cache.connectionString,
43
- }),
44
- // root: absent → auto-discovery (see below)
45
- });
46
-
47
- afterAll(cleanup);
48
- ```
49
-
50
- ### Options
51
-
52
- | Option | Required | Description |
53
- | ---------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
54
- | `services` | yes (if the app needs infra) | Named record of service factories (`postgres()`, `redis()`, `sqlite()`). Keys are your test vocabulary — see [services](17-services.md) |
55
- | `server` | yes | `(services) => app` — receives the started services record, fully typed (rule A8) |
56
- | `root` | no | Override for root resolution — reserved for cases where the convention is not enough (rule A9) |
57
-
58
- ### Root resolution (rule A9)
59
-
60
- Without `root`, the framework walks **up from the specification file** to the **nearest** directory carrying `package.json` — the package being tested, not the repository around it. Passing a `root` that points at the directory the walk would have found anyway is redundant (future lint warning).
61
-
62
- ## The chain
63
-
64
- ### Setups (chainable)
65
-
66
- | Setup | Description |
67
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
68
- | `.seed('file.sql')` | Load `_seeds/file.sql` into the database |
69
- | `.seed('file.sql', { database })` | Target a database by its record key — **mandatory with ≥ 2 databases, forbidden with 1** (rule A7) |
70
- | `.headers({ 'Name': 'value' })` | Set request headers; repeated calls merge |
71
- | `.intercept(contract)` | Mock an outgoing HTTP call with a declared [contract](16-contracts.md) |
72
- | `.intercept(trigger, response)` | Inline intercept for one-off cases |
73
- | `.clock('2026-03-04T09:30:00Z')` | Pin the app's `Date` for this chain, released when the action resolves ([18](18-conventions.md#time--one-primitive-two-depths)) |
74
-
75
- Contracts are **strict** (rule D7): once a chain declares one, every outgoing request must match a declared, non-exhausted contract or the spec fails with an explicit "Unmatched outgoing HTTP request" error (see [contracts](16-contracts.md#strict-by-construction-rule-d7)). `.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.
76
-
77
- ```typescript
78
- test('serves french content', async () => {
79
- // Given - headers inline
80
- const result = await api.headers({ 'Accept-Language': 'fr' }).get('/welcome');
81
-
82
- // Then
83
- expect(result.response.body).toEqual({ message: 'Bienvenue' });
84
- });
85
- ```
86
-
87
- #### Isolation between specs (rules B1, B7)
88
-
89
- 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:
90
-
91
- ```typescript
92
- test('starts clean between specs', async () => {
93
- // Given - nothing (every chain resets the databases: one spec = ONE action)
94
- const result = await api.get('/orders');
95
-
96
- // Then
97
- await expect(result.table('orders', { database: 'db' })).toBeEmpty();
98
- });
99
- ```
100
-
101
- ### Actions (terminal)
102
-
103
- Exactly one per chain (rule B1/B2). Each executes the spec and resolves to the result.
104
-
105
- | Action | Description |
106
- | ----------------------- | ------------------------------------------------------------- |
107
- | `.request('file.http')` | Execute the complete request from `_requests/file.http` |
108
- | `.get(path)` | Inline GET — for simple cases where a file would be excessive |
109
- | `.post(path, body?)` | Inline POST |
110
- | `.put(path, body?)` | Inline PUT |
111
- | `.delete(path)` | Inline DELETE |
112
-
113
- The inline `body?` of `.post()` / `.put()` is a plain object, JSON-serialized with a `Content-Type: application/json` default header. There is no filename form inline — file-based requests always go through `.request('file.http')`, whose body section is sent **raw**: the surrounding blank lines are trimmed, but everything between is preserved byte-for-byte (interior double spaces, indentation, and non-JSON text are intact).
114
-
115
- There is no `.run()` and no label argument: the vitest test name is the spec's only description (rule B3).
116
-
117
- ```typescript
118
- test('returns 404 with a useful body', async () => {
119
- // Given - empty database
120
- const result = await api.get('/users/999');
121
-
122
- // Then - inline assertions when a fixture file would be excessive
123
- expect(result.status).toBe(404);
124
- expect(result.response.body).toEqual({ error: 'User 999 not found' });
125
- });
126
- ```
127
-
128
- ### `.http` request files — full format
129
-
130
- Requests live in `_requests/`, one file per request, extension `.http` (rule C2). A request file is the **complete** request: method + path on the first line, then headers, then a blank line, then the body.
131
-
132
- ```http
133
- ### _requests/create-user.http
134
- POST /users
135
- Content-Type: application/json
136
- Accept-Language: fr
137
-
138
- { "name": "Alice" }
139
- ```
140
-
141
- Format rules:
142
-
143
- - First line: `METHOD /path` — the file must start with it (rule C2). The path is relative to the app under test.
144
- - Header lines: `Name: value`, one per line, immediately after the request line.
145
- - Blank line, then the body (optional — a GET usually has none).
146
- - Executed with `api.request('create-user.http')` — the argument is the file name inside the feature's `_requests/` folder.
147
-
148
- Expected responses live in `_expected/`, like every other expected fixture, same extension, and start with a status line (rule C3):
149
-
150
- ```http
151
- ### _expected/user-created.http
152
- HTTP/1.1 201 Created
153
- Content-Type: application/json
154
- Location: /users/{{uuid#user}}
155
-
156
- { "id": "{{uuid#user}}", "name": "Alice" }
157
- ```
158
-
159
- - First line: `HTTP/1.1 <status>` — mandatory.
160
- - Headers are matched as a **subset**: listed headers must match, unlisted headers are unconstrained (rule C3).
161
- - 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](15-tokens.md).
162
-
163
- ## The result
164
-
165
- The result of an API action exposes read-only accessors (rule D1); all assertions go through `expect()`:
166
-
167
- | Member | Type | Description |
168
- | --------------------------- | ---------------- | -------------------------------------------------------------------------------- |
169
- | `result.status` | `number` | HTTP status code — assert with native `expect(...).toBe(...)` |
170
- | `result.response` | response subject | Full response (status + headers + body) — subject for `toMatch` |
171
- | `result.response.body` | parsed body | Raw body for native assertions (`toEqual`, `toMatchObject`) |
172
- | `result.table(name, opts?)` | table subject | Database table — subject for `toMatchRows` / `toBeEmpty` (async, `await expect`) |
173
-
174
- `expect(result.response).toMatch('user-created.http')` resolves against `_expected/`, like every other subject (rule D3) — there is no per-subject resolution. The full matcher reference is in [assertions](14-assertions.md).
175
-
176
- 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.jobs()` has no `docker` member — jobs never spawn containers.)
177
-
178
- ## Unique here
179
-
180
- ### Full example — a multi-database order flow
181
-
182
- ```http
183
- ### _requests/new-order.http
184
- POST /orders
185
- Content-Type: application/json
186
-
187
- { "sku": "KB-42", "quantity": 1 }
188
- ```
189
-
190
- ```http
191
- ### _expected/order-created.http
192
- HTTP/1.1 201 Created
193
- Content-Type: application/json
194
- Location: /orders/{{uuid#order}}
195
-
196
- {
197
- "id": "{{uuid#order}}",
198
- "status": "pending",
199
- "total": "{{number}}",
200
- "createdAt": "{{iso8601}}",
201
- "paymentIntent": { "orderId": "{{uuid#order}}", "expiresAt": "{{iso8601}}" }
202
- }
203
- ```
204
-
205
- ```typescript
206
- // specs/api/orders/orders.spec.ts
207
- import { expect, test } from 'vitest';
208
- import { match } from '@jterrazz/test';
209
- import { api } from '../api.specification.js';
210
-
211
- test('ingests an order event into analytics', async () => {
212
- // Given - catalog in the main database, analytics empty
213
- const result = await api.seed('catalog.sql', { database: 'db' }).request('new-order.http');
214
-
215
- // Then - one row per database, targeted by record key
216
- await expect(result.table('orders', { database: 'db' })).toMatchRows({
217
- columns: ['status'],
218
- rows: [['pending']],
219
- });
220
- await expect(result.table('events', { database: 'analyticsDb' })).toMatchRows({
221
- columns: ['type'],
222
- rows: [['order_created']],
223
- });
224
- });
225
-
226
- test('returns the created order with consistent ids', async () => {
227
- // Given
228
- const result = await api.seed('catalog.sql', { database: 'db' }).request('new-order.http');
229
-
230
- // Then - {{uuid#order}} appears 3 times (Location header included) → all equal
231
- expect(result.response).toMatch('order-created.http');
232
- });
233
-
234
- test('links the analytics event to the created order', async () => {
235
- // Given
236
- const result = await api.seed('catalog.sql', { database: 'db' }).request('new-order.http');
237
-
238
- // Then - same id on both sides, without ever knowing its value
239
- await expect(result.table('orders', { database: 'db' })).toMatchRows({
240
- columns: ['id', 'status'],
241
- rows: [[match.ref('order'), 'pending']],
242
- });
243
- await expect(result.table('events', { database: 'analyticsDb' })).toMatchRows({
244
- columns: ['order_id', 'type'],
245
- rows: [[match.ref('order'), 'order_created']],
246
- });
247
- });
248
- ```
249
-
250
- > This project declares two databases, so `database:` is mandatory on every `.seed()` and `.table()` (rule A7). With a single database you would write `api.seed('catalog.sql')` and `result.table('orders')` — and adding `database:` would be forbidden as redundant.
251
-
252
- 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](decisions/007-compose-mode-leaves-the-package.md).
253
-
254
- ## Pitfalls
255
-
256
- - **Omitting `database:` with ≥ 2 databases, or passing it with 1** — both are convention violations (rule A7).
257
- - **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.
258
- - **Chaining two actions** (`api.get(...).get(...)`) or reusing state across tests. One chain = one terminal action; databases reset per chain (rules B1, B7).
259
- - **Forgetting that `.intercept()` is strict.** After the first `.intercept()`, an unmatched or queue-exhausted outgoing request fails the spec (rule D7).
260
- - **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.
261
-
262
- ## Related
263
-
264
- [11 — Jobs specs](11-jobs.md) · [14 — Assertions](14-assertions.md) · [15 — Tokens](15-tokens.md) · [16 — Contracts](16-contracts.md) · [17 — Services](17-services.md)