@jterrazz/test 15.3.0 → 16.1.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 (222) hide show
  1. package/README.md +120 -427
  2. package/dist/appium.adapter.js +71 -15
  3. package/dist/artifacts.js +25 -1735
  4. package/dist/browser/index.js +304 -233
  5. package/dist/checker.d.ts +1 -1
  6. package/dist/checker.js +1103 -97
  7. package/dist/fixtures.js +110 -0
  8. package/dist/index.d.ts +956 -971
  9. package/dist/index.js +2159 -2281
  10. package/dist/literate.js +1747 -0
  11. package/dist/match.js +625 -84
  12. package/dist/oxlint.cjs +3585 -1480
  13. package/dist/oxlint.d.cts +60 -48
  14. package/dist/oxlint.d.ts +60 -48
  15. package/dist/oxlint.js +3585 -1480
  16. package/dist/playwright.adapter.js +51 -8
  17. package/dist/{intercept.js → server.js} +2 -2
  18. package/dist/stream.js +6 -6
  19. package/dist/{ambiguity.js → substring-warning.js} +106 -7
  20. package/dist/vitest.d.ts +67 -48
  21. package/dist/vitest.js +229 -93
  22. package/docs/01-architecture.md +95 -0
  23. package/docs/02-developing.md +351 -0
  24. package/docs/03-testing.md +495 -0
  25. package/docs/04-operating.md +101 -0
  26. package/docs/05-module-tests.md +152 -0
  27. package/docs/06-integration.md +180 -0
  28. package/docs/07-component.md +246 -0
  29. package/docs/08-website.md +404 -0
  30. package/docs/09-mobile.md +270 -0
  31. package/docs/10-api.md +264 -0
  32. package/docs/11-jobs.md +157 -0
  33. package/docs/12-cli.md +677 -0
  34. package/docs/13-elements.md +242 -0
  35. package/docs/14-assertions.md +314 -0
  36. package/docs/15-tokens.md +253 -0
  37. package/docs/16-contracts.md +438 -0
  38. package/docs/17-services.md +188 -0
  39. package/docs/18-conventions.md +305 -0
  40. package/docs/19-linting.md +510 -0
  41. package/docs/README.md +36 -0
  42. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  43. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  44. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  45. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  46. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  47. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  48. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  49. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  50. package/docs/decisions/_template.md +22 -0
  51. package/docs/reference/classes/Matcher.md +120 -0
  52. package/docs/reference/classes/ProcessHandle.md +368 -0
  53. package/docs/reference/functions/defineContract.md +39 -0
  54. package/docs/reference/functions/defineContracts.md +36 -0
  55. package/docs/reference/functions/disabled.md +28 -0
  56. package/docs/reference/functions/enabled.md +20 -0
  57. package/docs/reference/functions/focused.md +26 -0
  58. package/docs/reference/functions/mockOf.md +79 -0
  59. package/docs/reference/functions/postgres.md +31 -0
  60. package/docs/reference/functions/process.md +33 -0
  61. package/docs/reference/functions/redis.md +28 -0
  62. package/docs/reference/functions/required.md +41 -0
  63. package/docs/reference/functions/selected.md +26 -0
  64. package/docs/reference/functions/sqlite.md +33 -0
  65. package/docs/reference/functions/testId.md +20 -0
  66. package/docs/reference/functions/text.md +40 -0
  67. package/docs/reference/functions/valued.md +28 -0
  68. package/docs/reference/functions/waitUntil.md +46 -0
  69. package/docs/reference/functions/within.md +29 -0
  70. package/docs/reference/index.md +196 -0
  71. package/docs/reference/interfaces/BaseResult.md +87 -0
  72. package/docs/reference/interfaces/CallResult.md +160 -0
  73. package/docs/reference/interfaces/CaptureScope.md +82 -0
  74. package/docs/reference/interfaces/CliResult.md +253 -0
  75. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  76. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  77. package/docs/reference/interfaces/FetchResult.md +180 -0
  78. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  79. package/docs/reference/interfaces/HttpResult.md +126 -0
  80. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  81. package/docs/reference/interfaces/PageResult.md +356 -0
  82. package/docs/reference/interfaces/RenderResult.md +72 -0
  83. package/docs/reference/interfaces/RenderedText.md +110 -0
  84. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  85. package/docs/reference/interfaces/ScreenResult.md +128 -0
  86. package/docs/reference/interfaces/TableAccessor.md +26 -0
  87. package/docs/reference/interfaces/TextAccessor.md +96 -0
  88. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  89. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  90. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  91. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  92. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  93. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  94. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  95. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  96. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  97. package/docs/reference/type-aliases/CliEnv.md +11 -0
  98. package/docs/reference/type-aliases/CliHandle.md +80 -0
  99. package/docs/reference/type-aliases/CliInput.md +36 -0
  100. package/docs/reference/type-aliases/CliOutput.md +46 -0
  101. package/docs/reference/type-aliases/CliPort.md +58 -0
  102. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  103. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  104. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  105. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  106. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  107. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  108. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  109. package/docs/reference/type-aliases/Contract.md +68 -0
  110. package/docs/reference/type-aliases/ContractInput.md +13 -0
  111. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  112. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  113. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  114. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  115. package/docs/reference/type-aliases/Contracts.md +48 -0
  116. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  117. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  118. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  119. package/docs/reference/type-aliases/DevicePort.md +51 -0
  120. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  121. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  122. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  123. package/docs/reference/type-aliases/DomMount.md +21 -0
  124. package/docs/reference/type-aliases/ElementKind.md +27 -0
  125. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  126. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  127. package/docs/reference/type-aliases/ElementRef.md +128 -0
  128. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  129. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  130. package/docs/reference/type-aliases/HonoApp.md +31 -0
  131. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  132. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  133. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  134. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  135. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  136. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  137. package/docs/reference/type-aliases/Intercept.md +19 -0
  138. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  139. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  140. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  141. package/docs/reference/type-aliases/JobHandle.md +34 -0
  142. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  143. package/docs/reference/type-aliases/JobsResult.md +16 -0
  144. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  145. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  146. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  147. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  148. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  149. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  150. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  151. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  152. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  153. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  154. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  155. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  156. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  157. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  158. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  159. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  160. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  161. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  162. package/docs/reference/type-aliases/MockPort.md +10 -0
  163. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  164. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  165. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  166. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  167. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  168. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  169. package/docs/reference/type-aliases/ServerPort.md +36 -0
  170. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  171. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  172. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  173. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  174. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  175. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  176. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  177. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  178. package/docs/reference/type-aliases/SpecKind.md +8 -0
  179. package/docs/reference/type-aliases/SpecRun.md +108 -0
  180. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  181. package/docs/reference/type-aliases/SpecStream.md +32 -0
  182. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  183. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  184. package/docs/reference/type-aliases/SseEvent.md +58 -0
  185. package/docs/reference/type-aliases/StreamBody.md +61 -0
  186. package/docs/reference/type-aliases/TextFilter.md +22 -0
  187. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  188. package/docs/reference/type-aliases/Visitor.md +215 -0
  189. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  190. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  191. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  192. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  193. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  194. package/docs/reference/variables/anthropic.md +19 -0
  195. package/docs/reference/variables/banner.md +10 -0
  196. package/docs/reference/variables/button.md +10 -0
  197. package/docs/reference/variables/clock.md +16 -0
  198. package/docs/reference/variables/complementary.md +10 -0
  199. package/docs/reference/variables/component.md +10 -0
  200. package/docs/reference/variables/content.md +10 -0
  201. package/docs/reference/variables/contentinfo.md +10 -0
  202. package/docs/reference/variables/dialog.md +10 -0
  203. package/docs/reference/variables/field.md +10 -0
  204. package/docs/reference/variables/form.md +10 -0
  205. package/docs/reference/variables/heading.md +10 -0
  206. package/docs/reference/variables/http.md +40 -0
  207. package/docs/reference/variables/intercept.md +11 -0
  208. package/docs/reference/variables/link.md +10 -0
  209. package/docs/reference/variables/listitem.md +10 -0
  210. package/docs/reference/variables/main.md +10 -0
  211. package/docs/reference/variables/match.md +8 -0
  212. package/docs/reference/variables/navigation.md +10 -0
  213. package/docs/reference/variables/openai.md +21 -0
  214. package/docs/reference/variables/option.md +17 -0
  215. package/docs/reference/variables/region.md +10 -0
  216. package/docs/reference/variables/row.md +10 -0
  217. package/docs/reference/variables/search.md +10 -0
  218. package/docs/reference/variables/specification.md +8 -0
  219. package/docs/reference/variables/status.md +10 -0
  220. package/docs/reference/variables/table.md +10 -0
  221. package/package.json +32 -11
  222. package/dist/spec-document.js +0 -651
package/README.md CHANGED
@@ -1,504 +1,197 @@
1
1
  # @jterrazz/test
2
2
 
3
- Declarative testing framework for APIs, jobs, CLIs, websites, and mobile apps. Five constructors — `specification.api()`, `specification.jobs()`, `specification.cli()`, `specification.website()`, `specification.mobile()` — and specs that read as sentences: given → action → assertions. The vitest test name is the spec's description; all assertions go through `expect()` with auto-registered, subject-typed matchers.
3
+ Declarative testing framework for modules, APIs, jobs, CLIs, rendered components, websites and native apps. Six constructors — `specification.api()`, `specification.jobs()`, `specification.cli()`, `specification.integration()`, `specification.website()`, `specification.mobile()` — plus the `component` chain, which starts nothing and needs none, and the module test, which needs neither.
4
+
5
+ A spec reads as a sentence: given → action → assertions. The vitest test name is its only description, every assertion goes through `expect()` with subject-typed matchers, and the infrastructure a spec declares — Postgres, Redis, SQLite, a child process, a real Chromium, an iOS simulator — is started, isolated per worker and cleaned up for you.
4
6
 
5
7
  ```bash
6
8
  npm install -D @jterrazz/test vitest
7
9
  ```
8
10
 
9
- Everything a spec needs imports from `@jterrazz/test`. The exceptions are the two TOOL subpaths, which no spec ever imports: `@jterrazz/test/oxlint` (the zero-runtime lint plugin and its config fragment, loading no test runtime) and `@jterrazz/test/vitest` (what `vitest.config.ts` imports — the `defineSpecConfig()` preset and the `literate()` plugin).
11
+ Everything a spec needs imports from `@jterrazz/test`; the entries beside it are tools no spec imports, and they are listed once — [docs/01 § What the tree publishes](docs/01-architecture.md#what-the-tree-publishes).
10
12
 
11
- ## Quick start
13
+ ## One kind of test, one example
12
14
 
13
- ### API testing (HTTP)
15
+ Which kind a test is, is the first question — [the fork](docs/18-conventions.md#the-fork--which-kind-of-test-this-is) asks it in nine lines and the answer fixes the folder, the constructor and the project.
14
16
 
15
- ```typescript
16
- // specs/api/api.specification.ts
17
- import { afterAll } from 'vitest';
18
- import { postgres, specification } from '@jterrazz/test';
19
- import { createApp } from '../../src/app.js';
20
-
21
- export const { api, cleanup } = await specification.api({
22
- services: { db: postgres() }, // → compose service "db"
23
- server: ({ db }) => createApp({ databaseUrl: db.connectionString }),
24
- });
25
-
26
- afterAll(cleanup);
27
- ```
17
+ **A module** — beside its code, nothing started ([docs/05](docs/05-module-tests.md)):
28
18
 
29
19
  ```typescript
30
- // specs/api/users/users.test.ts
20
+ // src/domain/ranking.test.ts
21
+ import { clock } from '@jterrazz/test';
31
22
  import { expect, test } from 'vitest';
32
- import { api } from '../api.specification.js';
33
-
34
- test('creates a user', async () => {
35
- // Given - the complete request from _requests/create-user.http
36
- const result = await api.request('create-user.http');
37
-
38
- // Then - status + headers + body from _expected/user-created.http; row in db
39
- expect(result.response).toMatch('user-created.http');
40
- await expect(result.table('users')).toMatchRows({
41
- columns: ['name'],
42
- rows: [['Alice']],
43
- });
44
- });
45
- ```
46
-
47
- ### CLI testing
48
-
49
- ```typescript
50
- // specs/cli/cli.specification.ts
51
- import { resolve } from 'node:path';
52
- import { afterAll } from 'vitest';
53
- import { specification } from '@jterrazz/test';
54
-
55
- export const { cli, cleanup } = await specification.cli(
56
- resolve(import.meta.dirname, '../../bin/my-cli.sh'),
57
- );
58
23
 
59
- afterAll(cleanup);
60
- ```
24
+ import { rank } from './ranking.js';
61
25
 
62
- ```typescript
63
- // specs/cli/build/build.test.ts
64
- import { expect, test } from 'vitest';
65
- import { cli } from '../cli.specification.js';
26
+ test('ranks a fresher post above an older one of equal score', () => {
27
+ // Given - two posts of equal score, one written this morning
28
+ using _ = clock.at('2026-03-04T09:30:00Z');
66
29
 
67
- test('builds the project', async () => {
68
- // Given - sample app project spread into the cwd
69
- const result = await cli.fixture('$FIXTURES/sample-app/').exec('build');
70
-
71
- // Then - ESM output, no CJS
72
- expect(result.exitCode).toBe(0);
73
- expect(result.stdout).toContain('Build completed');
74
- expect(result.file('dist/index.js').exists).toBe(true);
75
- expect(result.file('dist/index.cjs').exists).toBe(false);
30
+ // Then - the fresher one leads
31
+ expect(rank(posts).map((post) => post.id)).toStrictEqual(['fresh', 'stale']);
76
32
  });
77
33
  ```
78
34
 
79
- A CLI session can also BE the spec file. A `<case>.spec.yaml` document states one scenario — its ground, then its runs — and executes either as a test file of its own (the `literate()` vite plugin) or through `cli.run('case.spec.yaml')`:
80
-
81
- ```yaml
82
- description: refuses to build without a manifest
83
- runs:
84
- - command: build
85
- exit: 1
86
- stderr: |
87
- Error: no my-cli.yaml in the current directory
88
- ```
89
-
90
- Same engine as the chain, same `{{token}}` grammar, same `TEST_UPDATE=1` — which rewrites each run's exit code and streams, and nothing else. A JSON Schema ships at `@jterrazz/test/schema` so an editor validates as you type. Full grammar: [docs/07-cli.md](docs/07-cli.md#spec-documents--casespecyaml).
91
-
92
- ### Website testing (browser)
35
+ **An integration spec** — a module against the real thing, or against a golden ([docs/06](docs/06-integration.md)):
93
36
 
94
37
  ```typescript
95
- // specs/website/website.specification.ts
96
- import { specification } from '@jterrazz/test';
97
- import { afterAll } from 'vitest';
98
-
99
- export const { cleanup, website } = await specification.website({
100
- server: { command: 'node specs/_fixtures/website-app/server.mjs', ready: '/' },
101
- });
102
-
103
- afterAll(cleanup);
104
- ```
105
-
106
- ```typescript
107
- // specs/website/visit/head.test.ts
38
+ // specs/integration/posts/find.spec.ts
108
39
  import { expect, test } from 'vitest';
109
- import { website } from '../website.specification.js';
110
40
 
111
- test('captures the full head surface of a rendered page', async () => {
112
- // Given - the fixture homepage
113
- const result = await website.visit('/');
41
+ import { integration } from '../integration.specification.js';
114
42
 
115
- // Then - one golden covers title, canonical, alternates, and metas
116
- expect(result.status).toBe(200);
117
- expect(result.head).toMatch('home.head.json');
118
- });
119
- ```
43
+ test('finds the post the migration wrote', async () => {
44
+ // Given - a seeded database
45
+ const result = await integration.seed('posts.sql').call(({ db }) => findPost(db, 'p-1'));
120
46
 
121
- ### Mobile testing (iOS simulator)
122
-
123
- ```typescript
124
- // specs/mobile/mobile.specification.ts
125
- import { specification } from '@jterrazz/test';
126
- import { afterAll } from 'vitest';
127
-
128
- export const { cleanup, mobile } = await specification.mobile({
129
- app: { bundleId: 'com.jterrazz.fakenews' },
130
- device: { name: 'iPhone 17', os: '26.5' },
47
+ // Then - the row comes back as the repository shapes it
48
+ expect(result.value).toMatch('found.json');
131
49
  });
132
-
133
- afterAll(cleanup);
134
50
  ```
135
51
 
136
- ```typescript
137
- // specs/mobile/events/feed.test.ts
138
- import { expect, test } from 'vitest';
139
- import { mobile } from '../mobile.specification.js';
140
-
141
- test('shows the events feed behind its deep link', async () => {
142
- // Given - the events screen
143
- const result = await mobile.open('news://events');
144
-
145
- // Then - one golden covers the whole projected accessibility tree
146
- expect(result.screen).toMatch('events.screen.json');
147
- });
148
- ```
149
-
150
- Actions are **terminal**: `.request()`, `.get()`, `.trigger()`, `.exec()`, `.call()`, `.fetch()`, `.visit()`, `.open()` execute the spec and resolve to a precisely typed result. There is no `.run()`, no label, and no `.spawn()`.
151
-
152
- ## The six constructors
153
-
154
- One constructor per tested interface, each returning a record destructured with its canonical name:
155
-
156
- | Constructor | Returns | Terminal actions |
157
- | ---------------------------------- | ---------------------------------------- | ------------------------------------------------------------ |
158
- | `specification.api(options)` | `{ api, cleanup, docker, orchestrator }` | `.request(file)`, `.get()`, `.post()`, `.put()`, `.delete()` |
159
- | `specification.jobs(options)` | `{ jobs, cleanup, orchestrator }` | `.trigger(name)` |
160
- | `specification.cli(bin, options)` | `{ cli, cleanup, docker, orchestrator }` | `.exec(args, { waitFor?, timeout? }?)` |
161
- | `specification.integration(opts?)` | `{ integration, cleanup }` | `.call((services) => …)` |
162
- | `specification.website(options)` | `{ website, cleanup, url }` | `.fetch(path)`, `.visit(path, scenario?)` |
163
- | `specification.mobile(options)` | `{ mobile, cleanup, udid }` | `.open(deepLink?, scenario?)` |
164
-
165
- A rendered component has no constructor: it starts nothing, so it is reached through the `component` chain directly (docs/16).
166
-
167
- ### `specification.api({ services, server, mode?, root? })`
168
-
169
- One definition, two execution modes — the switch lives in `vitest.config.ts`, never in the specification file:
170
-
171
- - **`node`** (default): starts the declared services via testcontainers and runs the app in-process (Hono). Fastest feedback loop.
172
- - **`compose`**: runs `docker compose up` on `docker/compose.test.yaml` and sends real HTTP to the app service (`server` is ignored). Proves the shipped artifact.
52
+ **A component** — a rendered unit in a real browser, beside its code ([docs/07](docs/07-component.md)):
173
53
 
174
- Resolution: `options.mode` > `TEST_MODE` env var > `'node'`. Only `.api()` has a mode.
175
-
176
- ```typescript
177
- // vitest.config.ts — the mode switch lives HERE
178
- import { defineSpecConfig } from '@jterrazz/test/vitest';
179
-
180
- export default defineSpecConfig({
181
- test: {
182
- projects: [
183
- { test: { name: 'http', include: ['specs/api/**/*.test.ts'] } },
184
- {
185
- test: {
186
- name: 'http-stack',
187
- include: ['specs/api/**/*.test.ts'],
188
- env: { TEST_MODE: 'compose' },
189
- },
190
- },
191
- ],
192
- },
193
- });
194
- ```
195
-
196
- `services` is a named record. Keys type the `server` factory parameters, name databases for `.seed()`/`.table()` (`{ database: 'analyticsDb' }`), and drive the compose binding — a handle with no `composeService` option links to the compose service named exactly like its key, else the kebab-case conversion of the key (`analyticsDb` → `analytics-db`). If both names exist the binding is ambiguous and throws; `composeService` is the escape hatch for non-derivable names.
54
+ ```tsx
55
+ // src/presentation/post-table.test.tsx
56
+ import { component, content } from '@jterrazz/test';
57
+ import { expect, test } from 'vitest';
197
58
 
198
- ### `specification.jobs({ services, jobs, root? })`
59
+ import { PostTable } from './post-table.js';
199
60
 
200
- Background jobs run in-process by definition — no HTTP server, no mode:
61
+ test('says how much of the collection the table is showing', async () => {
62
+ // Given - a page of one row answering for a collection of two hundred
63
+ const result = await component.intercept(listing).render(<PostTable />, async (visitor) => {
64
+ await visitor.see(content('Showing 1 of 200 posts'));
65
+ });
201
66
 
202
- ```typescript
203
- export const { jobs, cleanup } = await specification.jobs({
204
- services: { db: postgres() },
205
- jobs: ({ db }) => [nightlyReport(db)], // (services) => JobHandle[], or a static array
67
+ // Then - the table is qualified by what it is not showing
68
+ await expect(result.tree).toMatch('one-of-two-hundred.aria.yaml');
206
69
  });
207
-
208
- // In a test:
209
- const result = await jobs.seed('pending.sql').trigger('nightly-report');
210
70
  ```
211
71
 
212
- A `JobHandle` is `{ name: string; execute: () => Promise<void> }`.
213
-
214
- ### `specification.cli(bin, { root?, services?, docker?, transform?, env?, serve? })`
215
-
216
- Runs a command binary against fixture projects in fresh temp directories. `env` (named environment sets) and `serve` (named servers) are the registries a [`<case>.spec.yaml`](docs/07-cli.md#spec-documents--casespecyaml) names by word. With `services`, connection URLs are injected into the child env automatically: `<KEY>_URL` per record key (CONSTANT_CASE at camelCase boundaries — `analyticsDb` → `ANALYTICS_DB_URL`), plus `DATABASE_URL` (exactly one SQL database) and `REDIS_URL` (exactly one redis). `.env()` overrides; `null` unsets.
72
+ **A website** — the served page, driven in a browser ([docs/08](docs/08-website.md)):
217
73
 
218
74
  ```typescript
219
- export const { cli, cleanup } = await specification.cli('my-migrate-tool', {
220
- services: { db: postgres() },
221
- });
222
-
223
- // DATABASE_URL / DB_URL are already in the child env:
224
- const result = await cli.seed('legacy-schema.sql').exec('up');
225
- ```
226
-
227
- ### `specification.website({ server?, url?, backend?, external?, root? })`
228
-
229
- Tests a rendered website: `.fetch(path)` for a raw HTTP exchange (redirects never followed), `.visit(path, scenario?)` for a page rendered in a real chromium. Exactly one of `server` (start the site locally — a free port injected as `PORT`, polled on `ready`) or `url` (target a running site) is required. `backend: { env, port? }` (server mode only) additionally starts a declared stub backend and injects its URL into the server child under `env`; each chain declares what it serves with `.intercept(contracts)`, the same contracts form `api`/`jobs` use.
75
+ // specs/website/subscribe/subscribe.spec.ts
76
+ import { button, content, field } from '@jterrazz/test';
77
+ import { expect, test } from 'vitest';
230
78
 
231
- ```typescript
232
- export const { website, cleanup } = await specification.website({
233
- server: { command: 'node specs/_fixtures/website-app/server.mjs', ready: '/' },
234
- });
79
+ import { website } from '../website.specification.js';
235
80
 
236
- // Raw exchange — status + headers, redirects surface as 3xx
237
- const redirect = await website.fetch('/old');
81
+ test('subscribes through the form and captures the final state', async () => {
82
+ // Given - a visitor on the homepage
83
+ const result = await website.visit('/', async (visitor) => {
84
+ await visitor.fill(field('Email'), 'visitor@site.test');
85
+ await visitor.click(button('Subscribe'));
86
+ await visitor.see(content('Thanks for subscribing'));
87
+ });
238
88
 
239
- // Rendered page, optionally driven by a scenario (the When)
240
- const page = await website.visit('/', async (visitor) => {
241
- await visitor.click(link('Articles'));
89
+ // Then - the page says so, and the console is clean
90
+ expect(result.content).toContain('Thanks for subscribing');
91
+ await expect(result.errors).toBeEmpty();
242
92
  });
243
93
  ```
244
94
 
245
- The handle destructures to `{ website, cleanup, url }` — no `docker`, no `orchestrator`. `.visit()` needs playwright (`npm install -D playwright && npx playwright install chromium`) — an optional peer dependency, only loaded when a spec actually renders a page. Full reference: [docs/14-website.md](docs/14-website.md).
246
-
247
- ### `specification.mobile({ app, device, backend?, root? })`
248
-
249
- Tests a native app on the iOS simulator through a real XCUITest session (appium): `.open(deepLink?, scenario?)` terminates and relaunches the app (deterministic fresh state), applies the deep link, runs the scenario, and captures the final screen — the projected accessibility tree plus the visible texts. The simulator is resolved by `device: { name, os?, udid? }` via `xcrun simctl` (refusing on zero or several matches) and booted when shut down; the appium server is spawned from the caller project on a free port.
95
+ **A mobile screen** — the installed app on a simulator ([docs/09](docs/09-mobile.md)):
250
96
 
251
97
  ```typescript
252
- export const { mobile, cleanup, udid } = await specification.mobile({
253
- app: { bundleId: 'com.jterrazz.fakenews' },
254
- device: { name: 'iPhone 17', os: '26.5' },
255
- });
256
-
257
- // A screen behind its deep link, driven by a scenario (the When)
258
- const result = await mobile.open('news://events', async (visitor) => {
259
- await visitor.tap(button('Enquête Fauci COVID-19'));
260
- await visitor.see(content('rapports'));
261
- });
262
- ```
263
-
264
- The handle destructures to `{ mobile, cleanup, udid }` (plus `backendUrl` with `backend: { port? }` — a declared stub backend whose URL the CALLER wires into its own bundler env; the framework never touches Metro). The element vocabulary is the website facet's, unchanged — `button`, `field`, `content`, `testId`, `within` — landmarks excepted (an iOS screen has no ARIA regions; they refuse at runtime). Requires the app installed on the simulator plus the optional peers: `npm install -D appium webdriverio && npx appium driver install xcuitest`. Full reference: [docs/15-mobile.md](docs/15-mobile.md).
265
-
266
- ### Root auto-discovery
267
-
268
- When `root` is absent, the framework walks up from the specification file to the **nearest** directory carrying `package.json` or `docker/compose.test.yaml`. In a workspace that is the member being tested, not the repository root above it. Pass `root` only when the convention does not fit. `root` is strictly the **project root** (compose detection + local-bin resolution, or the cwd of a `specification.website()` server command) — it is not a fixtures root; `.fixture()` resolves its own paths.
269
-
270
- ## vitest config — `defineSpecConfig()`
98
+ // specs/mobile/events/bookmark.spec.ts
99
+ import { button, content } from '@jterrazz/test';
100
+ import { expect, test } from 'vitest';
271
101
 
272
- `@jterrazz/test/vitest` is the subpath `vitest.config.ts` imports, never a spec. Beside `literate()` it exports the shared preset: what you pass is a plain vite/vitest config merged **over** its defaults, and `literate:` adds the plugin.
102
+ import { mobile } from '../mobile.specification.js';
273
103
 
274
- ```typescript
275
- import { defineSpecConfig } from '@jterrazz/test/vitest';
104
+ test('bookmarks an event from its detail screen', async () => {
105
+ // Given - a visitor on the events feed
106
+ const result = await mobile.open('news://events', async (visitor) => {
107
+ await visitor.tap(button('Fauci inquiry'));
108
+ await visitor.see(content('reports'));
109
+ await visitor.tap(button('Bookmark'));
110
+ });
276
111
 
277
- export default defineSpecConfig({
278
- literate: { specification: './specs/cli/cli.specification.ts' },
279
- test: { include: ['specs/**/*.test.ts'] },
112
+ // Then - the capture reflects the screen after the interaction
113
+ expect(result.content).toContain('Bookmarked');
280
114
  });
281
115
  ```
282
116
 
283
- | Sets | To |
284
- | ---------------------------------- | ------------------------------------------------------ |
285
- | `cacheDir` | `.artifacts/vitest` |
286
- | `test.coverage.reportsDirectory` | `.artifacts/vitest/coverage` (bring your own provider) |
287
- | `test.testTimeout` / `hookTimeout` | `30_000` |
288
- | `test.exclude` | vitest's defaults **+** `**/_fixtures/**` |
289
-
290
- Nothing else: `fileParallelism`, `reporters`, `environment` and every `include` stay yours. Inline `projects` inherit the same defaults (vitest gives a project nothing from the root), arrays are concatenated rather than replaced, and scalars you state win. Full walkthrough, including migrating a hand-rolled config: [docs/02-developing.md](docs/02-developing.md#vitest-config-the-preset).
291
-
292
- Every artefact a run produces lands under `.artifacts/<tool>/` — one `.gitignore` line, one `rm -rf`. Per-run scratch (a CLI spec's temp cwd, a browser profile) stays in the OS temp dir.
293
-
294
- ## Builder API
295
-
296
- ### Setup (chainable)
297
-
298
- | Method | Facets | Description |
299
- | --------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
300
- | `.seed("file.sql", { database? })` | all | Load SQL from `_seeds/` — `database` is the record key (mandatory with ≥ 2 databases, forbidden with 1) |
301
- | `.fixture("file")` | cli | Copy the feature-local `_fixtures/file` into the working directory |
302
- | `.fixture("$FIXTURES/name/")` | cli | Spread the shared `specs/_fixtures/<name>/` project into the cwd (trailing `/` = contents; layers) |
303
- | `.env({ KEY: "value" })` | cli | Set env vars on the child (`null` unsets, `$WORKDIR` expands, calls merge) |
304
- | `.headers({ "Accept-Language": "fr" })` | api, website | Set HTTP request headers (merge on top of `.http` file headers, or on the browser context) |
305
- | `.intercept(contracts)` | all but cli | Declare the world: a `defineContracts(...)` composite — MSW on api/jobs, the stub backend on website/mobile |
306
- | `.intercept(contract)` / `([a, b])` | all but cli | A single contract, or an ordered list |
307
- | `.intercept(request, response)` | all but cli | Inline pair, for one-off plumbing |
308
- | `.clock("2026-03-04T09:30:00Z")` | api, jobs, integration, website | Pin the calendar for this chain — the page's on website, this process's elsewhere |
309
-
310
- ### Actions (terminal)
311
-
312
- | Method | Facet | Resolves to | Description |
313
- | ------------------------------------------ | ------- | -------------- | ---------------------------------------------------------------------------------------- |
314
- | `.request("create-user.http")` | api | `HttpResult` | Send the COMPLETE request from `_requests/<file>` (method, path, headers, raw body) |
315
- | `.get(path)` / `.delete(path)` | api | `HttpResult` | Inline requests for simple cases |
316
- | `.post(path, body?)` / `.put(path, body?)` | api | `HttpResult` | Inline body: plain object, JSON-serialized |
317
- | `.trigger("name")` | jobs | `BaseResult` | Execute a registered job |
318
- | `.exec("args")` | cli | `CliResult` | Run the command |
319
- | `.exec(["build", "start"])` | cli | `CliResult` | Sequence in the same cwd; stops on first non-zero exit |
320
- | `.exec("dev", { waitFor, timeout? })` | cli | `CliResult` | Long-running: resolves at the pattern, killed at `timeout` (default 10 s) |
321
- | `.run("case.spec.yaml")` | cli | `CliResult` | Run a `<case>.spec.yaml` — its ground and EVERY run asserted; the last one returns |
322
- | `.fetch(path)` | website | `FetchResult` | One raw HTTP exchange — redirects surface as 3xx, never followed |
323
- | `.visit(path, scenario?)` | website | `PageResult` | Render the page in a shared chromium; with a scenario, the capture is the final state |
324
- | `.open(deepLink?, scenario?)` | mobile | `ScreenResult` | Relaunch the app fresh on the simulator; with a scenario, the capture is the final state |
325
-
326
- One chain = one terminal action; databases reset at the start of every chain. Every cli spec runs in a fresh, empty temp directory.
327
-
328
- ## Assertions — everything through `expect()`
329
-
330
- Accessors are **read-only**; the framework registers subject-typed matchers on vitest's `expect` automatically. `await` is required exactly where IO happens (tables, trees, containers).
117
+ **An API** — the assembled app, met through a complete HTTP exchange ([docs/10](docs/10-api.md)):
331
118
 
332
119
  ```typescript
333
- // HTTP
334
- expect(result.status).toBe(201);
335
- expect(result.response).toMatch('user-created.http'); // _expected/<name> — status + header subset + body
336
- expect(result.response.body).toEqual({ error: 'User 999 not found' });
337
-
338
- // Tables (async — queries the database)
339
- await expect(result.table('orders', { database: 'db' })).toMatchRows({
340
- columns: ['id', 'status', 'created_at'],
341
- rows: [[match.uuid(), 'pending', match.iso8601()]],
342
- });
343
- await expect(result.table('orders', { database: 'db' })).toBeEmpty();
344
-
345
- // Streams (ANSI stripped by default; .text stays raw)
346
- expect(result.stdout).toContain('Build completed');
347
- expect(result.stdout).toMatch('help.txt'); // _expected/help.txt — {{token}}-aware
348
- expect(result.json).toMatch('config.json'); // _expected/config.json
349
- expect(result.json.value).toMatchObject({ name: 'shoply' });
350
-
351
- // Files & trees
352
- expect(result.file('my-shop/shoply.yaml').content).toContain('name: my-shop');
353
- await expect(result.directory('my-shop')).toMatch('shop-scaffold'); // _expected/shop-scaffold/
354
- await expect(result.filesystem).toMatch('upgraded-shop'); // whole cwd
355
-
356
- // Containers (docker-aware cli)
357
- await expect(result.container('alpha')).toBeRunning();
358
- ```
359
-
360
- `toMatch` always resolves against `_expected/<name>` — every subject, no exceptions (only `.request()` reads `_requests/`). The folder is flat: a slash in the name creates a subfolder; the extension is part of the name and required, except for tree snapshots which are directories.
361
-
362
- **Updating snapshots:** `TEST_UPDATE=1` or `vitest -u`. Update mode writes **tokens**, not values — segments covered by an existing placeholder are preserved, and `{{workdir}}` is substituted automatically.
363
-
364
- ## Dynamic values — one `{{token}}` grammar
365
-
366
- The same vocabulary works in `_expected/*.http` (body AND headers), `_expected/*.json`, text snapshots, and tree-snapshot file contents — and in code via `match.*`:
367
-
368
- `uuid` `ulid` `iso8601` `date` `time` `duration` `number` `int` `float` `semver` `sha` `hex` `base64` `port` `ip` `url` `email` `path` `workdir` `string` `any`
369
-
370
- Each token is capturable via `{{type#ref}}`: the first occurrence captures, later occurrences must be equal (scope: one spec). Code-side: `match.ref('order')`, `match.ref('intent', { not: 'order' })`, `match.regex(/…/)`.
371
-
372
- ```http
373
- ### _expected/order-created.http
374
- HTTP/1.1 201 Created
375
- Content-Type: application/json
376
- Location: /orders/{{uuid#order}}
377
-
378
- {
379
- "id": "{{uuid#order}}",
380
- "total": "{{number}}",
381
- "createdAt": "{{iso8601}}"
382
- }
383
- ```
384
-
385
- See [docs/09-tokens.md](docs/09-tokens.md) for the canonical accepted form of every token.
120
+ // specs/api/users/create-user.spec.ts
121
+ import { expect, test } from 'vitest';
386
122
 
387
- ## Contracts
123
+ import { api } from '../api.specification.js';
388
124
 
389
- Everything the outside world replies is a **contract** — a request to match and a response to serve, declared together. A feature owns a `contracts/` folder: a public `<name>.contracts.ts` facade (default export = the world, named exports = its scenarios) over internal `<provider>/<name>.ts` units, `provider ∈ { http, openai, anthropic }`.
125
+ test('creates a user and returns its location', async () => {
126
+ // Given - an empty users table
127
+ const result = await api.seed('empty.sql').request('create-user.http');
390
128
 
391
- ```typescript
392
- // contracts/openai/classify-product.ts
393
- import { defineContract, openai } from '@jterrazz/test';
394
-
395
- export default defineContract({
396
- request: openai.responses({ user: /Product Classification/, tools: ['classify'] }),
397
- response: openai.reply({ category: 'ELECTRONICS', confidence: 0.97 }),
129
+ // Then - the response matches the golden, tokens and all
130
+ expect(result.response).toMatch('user-created.http');
131
+ await expect(result.table('users')).toMatchRows({
132
+ columns: ['name'],
133
+ rows: [['Alice']],
134
+ });
398
135
  });
399
136
  ```
400
137
 
401
- ```typescript
402
- // contracts/pipeline.contracts.ts
403
- import { defineContracts, http } from '@jterrazz/test';
404
-
405
- import classifyProduct from './openai/classify-product.js';
406
- import exchangeRates from './http/exchange-rates.js';
407
-
408
- const pipeline = defineContracts(classifyProduct, exchangeRates);
409
-
410
- export default pipeline;
411
-
412
- export const withRatesDown = () =>
413
- pipeline.with({ request: http.get('/rates'), response: http.error(503) });
414
- ```
138
+ **A job** — what a name triggers, in-process ([docs/11](docs/11-jobs.md)):
415
139
 
416
140
  ```typescript
417
- const result = await jobs.intercept(pipeline).trigger('nightly-report');
418
- ```
419
-
420
- Selection is first-match, one queue for every facet: `times` bounds how often a contract serves (omitted = unlimited, so retries and re-renders replay it), `required: true` fails the chain if it was never requested. Provider string filters are **exact** — the loose forms are explicit (`RegExp`, `match.includes('…')`). Failure simulation: `openai.error(429)`, `anthropic.timeout()`, `openai.malformed('not json')`. MSW ships as a direct dependency — no separate install. Full chapter: [docs/10-contracts.md](docs/10-contracts.md).
141
+ // specs/jobs/digest/daily-digest.spec.ts
142
+ import { expect, test } from 'vitest';
421
143
 
422
- ## Docker-aware CLIs
144
+ import { jobs } from '../jobs.specification.js';
423
145
 
424
- For CLIs that spawn containers, declare `docker: { envVar, nameLabel, testRunLabel }`. The runner injects a unique test-run id into the child env; the tested binary must label its containers with `testRunLabel=<id>`. Results expose lazy `.container(name)` accessors — and must be bound with `await using` so leaked containers are force-removed at scope exit:
146
+ test('writes one digest row per active subscriber', async () => {
147
+ // Given - two active subscribers and one cancelled
148
+ const result = await jobs.seed('subscribers.sql').trigger('daily-digest');
425
149
 
426
- ```typescript
427
- test('deploy spawns a labelled container', async () => {
428
- // Given
429
- await using result = await cli.fixture('$FIXTURES/two-shops/').exec('deploy alpha');
430
-
431
- // Then - property reads are sync; only the matcher is async
432
- const shop = result.container('alpha');
433
- expect(shop.exists).toBe(true);
434
- await expect(shop).toBeRunning();
435
- expect(shop.file('/app/shoply.yaml').content).toContain('name: alpha');
150
+ // Then - only the active ones were written
151
+ await expect(result.table('digests')).toMatchRows({
152
+ columns: ['to'],
153
+ rows: [['a@site.test'], ['b@site.test']],
154
+ });
436
155
  });
437
156
  ```
438
157
 
439
- ## Service factories
440
-
441
- | Factory | Options | Connection string |
442
- | ------------ | -------------------------------- | ------------------------------------- |
443
- | `postgres()` | `composeService`, `image`, `env` | `postgresql://user:pass@host:port/db` |
444
- | `redis()` | `composeService`, `image` | `redis://host:port` |
445
- | `sqlite()` | `init` or `prismaSchema` | `file:/…/….sqlite` |
446
-
447
- `docker/compose.test.yaml` is the single source of truth for test infrastructure; `docker/<service>/init.sql` runs when the corresponding service starts. Parallel isolation is automatic per vitest worker: postgres clones a schema, redis assigns a database index, sqlite copies the template file, compose mode gets a dedicated project.
448
-
449
- `sqlite()` caches its schema template inside the project — `.artifacts/vitest/sqlite/template-<key>.sqlite` — so two checkouts never share one, and workers racing for a cold cache wait for the one that is building rather than all building at once. Details: [docs/11-services.md](docs/11-services.md#where-the-template-lives).
158
+ **A CLI** — the built binary, usually as a document ([docs/12](docs/12-cli.md)):
450
159
 
451
- ## Also exported
452
-
453
- Everything below comes from the package root (rule F1) — except the project helpers, which are `@jterrazz/test/vitest`'s. One line each; the chapter owns the detail.
454
-
455
- | Export | Is | Chapter |
456
- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
457
- | `clock.at(iso)` · `.run(iso?)` · `.advance(ms)` | The one time primitive — a `using` scope that pins `Date`, replacing `vi.useFakeTimers()` and `mockOfDate` | [docs/12](docs/12-conventions.md#time--one-primitive-two-depths) |
458
- | `intercept(…)` | The same network double with no chain: `await using _ = await intercept(…)` in a module test | [docs/10](docs/10-contracts.md#intercept--the-same-double-with-no-chain) |
459
- | `process({ command, ready?, … })` | The one shape an external process takes — a dev server, an API, a bundler, owned by the runner | [docs/11](docs/11-services.md#process--the-one-shape-an-external-process-takes) |
460
- | `http.stream(chunks, init?)` · `http.sse(events)` | A chunked or server-sent response body a contract replies with | [docs/10](docs/10-contracts.md) |
461
- | `mockOf<T>()` · `mockOf<T>({ deep: false })` | Deep double of a port (a member never stubbed answers with another double), or the flat one | [docs/12](docs/12-conventions.md#the-doubles-ladder) |
462
- | `mockOfDate` | Freeze/reset the global `Date` via `.set(date)` / `.reset()` — `clock` is what replaces it | [docs/12](docs/12-conventions.md#time--one-primitive-two-depths) |
463
- | `required(value, why)` | The value, or a failure naming what was missing and why it mattered — instead of `!` | [docs/08](docs/08-assertions.md#two-helpers-that-are-not-matchers) |
464
- | `waitUntil(predicate, { timeout?, interval?, why? })` | Wait on a CONDITION, never a duration, on a real-time budget a pinned clock cannot starve | [docs/08](docs/08-assertions.md#two-helpers-that-are-not-matchers) |
465
- | `unit()` `component()` `api()` `jobs()` `cli()` `integration()` `website()` `mobile()` | The vitest project helpers, each taking `{ include, exclude, timeout, serial }` on top of the project it already is | [docs/02](docs/02-developing.md#vitest-config-the-preset) |
466
-
467
- ## Conventions
468
-
469
- Normative rules live in the constitution ([docs/12-conventions.md](docs/12-conventions.md)); the generated per-rule catalogue is [docs/13-linting.md](docs/13-linting.md). A facet (`specs/<facet>/`) carries its runner(s) at its root and holds domain folders; the folder follows the assets:
470
-
471
- ```
472
- specs/<facet>/ # api | jobs | cli | integration | website | mobile
473
- ├── <facet>.specification.ts # runner(s) at the facet ROOT (rule C1)
474
- └── <domain>/ # a product command/area — 1..n test files
475
- ├── <aspect>.test.ts
476
- ├── _seeds/ # *.sql ONLY — database state
477
- ├── _requests/ # *.http — inputs: COMPLETE request (method, path, headers, body)
478
- ├── contracts/ # <name>.contracts.ts facade + <provider>/<name>.ts units + their .response.json / .request.ts data
479
- ├── _fixtures/ # domain-local files/dirs copied into the cwd (cli) — shared pool lives at specs/_fixtures/
480
- └── _expected/ # ALL expected fixtures, FLAT (incl. response *.http) — a slash in the name creates a subfolder
160
+ ```yaml
161
+ # specs/cli/scaffold/new-project.spec.yaml
162
+ description: scaffolds a project into an empty directory
163
+ fixture: empty-dir
164
+ runs:
165
+ - command: new my-app
166
+ exitCode: 0
167
+ stdout: scaffolded.txt
168
+ files:
169
+ my-app/package.json: package.json
481
170
  ```
482
171
 
483
- A test with its OWN asset dirs gets its own domain folder; tests without local assets group as sibling `<aspect>.test.ts` files inside a named group folder (the folder follows the assets). `.fixture(path)` is the one verb that copies into the cwd: domain-local (`_fixtures/…`) or shared (`$FIXTURES/…` → `specs/_fixtures/…`), with rsync trailing-slash semantics and layering. `.seed()` is SQL-only.
484
-
485
- Every test contains `// Given -` and `// Then -` comments (always both; `// When -` only if the action is not obvious — the chain IS the when). User-facing framework env vars: `TEST_MODE` and `TEST_UPDATE` — the only ones you set; the framework also reads vitest's `VITEST_POOL_ID` for per-worker isolation.
172
+ ## The map
486
173
 
487
- ### Convention enforcement — the shipped lint plugin
174
+ Everything is stated ONCE, in the chapter that owns it — a second copy here would be a second answer, and the one a reader meets first is the one that goes stale. The corpus map is [docs/README.md](docs/README.md).
488
175
 
489
- These conventions are not just prose: the package ships an oxlint plugin (`@jterrazz/test/oxlint`) with ~40 AST rules, plus a `jterrazz-test-check` binary (the conventions checker) that reads the data fixtures and cross-file relationships oxlint cannot. Wire the plugin into your `oxlint.config.ts` and run `jterrazz-test-check specs` in CI — the full four-channel catalogue (each rule, its channel and rationale) is generated into [docs/13-linting.md](docs/13-linting.md).
176
+ | Subject | Chapter |
177
+ | ---------------------------------------------------------------- | ---------------------------------------------------------------- |
178
+ | What the framework is, its trees, its channels, its four entries | [docs/01](docs/01-architecture.md) |
179
+ | Installing it, the preset, the project helpers | [docs/02](docs/02-developing.md#vitest-config-the-preset) |
180
+ | What a consumer must bring: the peers, Docker, the pinned pair | [docs/04](docs/04-operating.md#what-a-consumer-must-bring) |
181
+ | Each kind of test, one chapter, the same seven sections | [docs/05](docs/05-module-tests.md)–[docs/12](docs/12-cli.md) |
182
+ | Naming an element: descriptors, verbs, `within`, exact names | [docs/13](docs/13-elements.md) |
183
+ | Every matcher, by subject | [docs/14](docs/14-assertions.md) |
184
+ | The `{{token}}` grammar, `#ref` captures, update mode | [docs/15](docs/15-tokens.md) |
185
+ | Contracts, selection, provider builders, `intercept()` | [docs/16](docs/16-contracts.md) |
186
+ | Services, init scripts, per-worker isolation, `process()` | [docs/17](docs/17-services.md) |
187
+ | The conventions, and the catalogue that enforces them | [docs/18](docs/18-conventions.md), [docs/19](docs/19-linting.md) |
490
188
 
491
- ## Requirements
189
+ The conventions are not prose alone: the package ships an oxlint plugin (`@jterrazz/test/oxlint`) and a `jterrazz-test-check` binary that reads the fixtures and cross-file relationships oxlint cannot. Every diagnostic ends in a rule id and an anchor into the generated catalogue.
492
190
 
493
- - **Docker** - testcontainers for node mode, docker compose for compose mode; not needed for `sqlite()`, plain cli specs, website specs, or mobile specs
494
- - **vitest** - peer dependency
495
- - **playwright** - optional peer dependency, only needed for `.visit()`: `npm install -D playwright && npx playwright install chromium`
496
- - **appium + webdriverio** - optional peer dependencies, only needed for `specification.mobile()`: `npm install -D appium webdriverio && npx appium driver install xcuitest` — plus Xcode, a simulator, and the app installed on it
497
- - **msw** - bundled as a direct dependency (powers `.intercept()` on a chain and `intercept()` in module scope, under node and in a page); no separate install
498
- - **hono** (or any web framework) - supplied by your project for in-process apps; the adapter only needs an object with a `request()` method, so it is not a peer
191
+ Node 24 and vitest 5 are the required peers; everything else — a browser, a database driver, a simulator — is an optional peer, loaded the first time a spec needs it and named in the refusal when it is not there. What to install, and when: [docs/04](docs/04-operating.md#what-a-consumer-must-bring).
499
192
 
500
193
  ## Docs
501
194
 
502
- - Guide (chapters): [docs/README.md](docs/README.md) — getting started, API/jobs/CLI/website/mobile specs, assertions, tokens, contracts, services, conventions, linting
503
- - API reference: committed under [docs/reference/](docs/reference/) — compiled from source by `npm run docs`
504
- - Agent skill: [skills/jterrazz-test/](skills/jterrazz-test/) — mental model, per-facet references, generated rule reference
195
+ - The corpus: [docs/README.md](docs/README.md) — the spine, one chapter per kind, the shared references, the enforcement.
196
+ - API reference: [docs/reference/](docs/reference/) — compiled from source by `npm run docs`.
197
+ - Agent skill: [skills/jterrazz-test/](skills/jterrazz-test/) — the fork, the mental model, and a generated signature card per kind.