@jterrazz/test 17.0.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/browser/index.js +1 -2
  2. package/dist/browser/services.js +1 -2
  3. package/dist/browser/stubs.js +1 -2
  4. package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
  5. package/dist/checker.js +15 -824
  6. package/dist/in-place.global-setup.d.ts +1 -0
  7. package/dist/in-place.global-setup.js +1 -1
  8. package/dist/index.d.ts +2 -14
  9. package/dist/index.js +83 -204
  10. package/dist/mobile-session.global-setup.d.ts +1 -0
  11. package/dist/oxlint.cjs +2 -2
  12. package/dist/oxlint.js +67 -2321
  13. package/dist/route-coverage.global-setup.d.ts +1 -0
  14. package/dist/route-coverage.global-setup.js +1 -2
  15. package/dist/services.global-setup.d.ts +1 -1
  16. package/dist/services.global-setup.js +1 -2
  17. package/dist/services.js +1 -1
  18. package/dist/shared-appium.adapter.js +14 -14
  19. package/dist/shared-artifacts.js +7 -5
  20. package/dist/shared-derived-projects.js +23 -8
  21. package/dist/shared-in-place.js +1 -1
  22. package/dist/shared-ledger.js +1 -1
  23. package/dist/shared-listened-port.js +19 -0
  24. package/dist/shared-literate-plugin.js +2 -1
  25. package/dist/shared-pause.js +12 -1
  26. package/dist/shared-playwright.adapter.js +17 -19
  27. package/dist/shared-process-alive.js +13 -0
  28. package/dist/shared-record-key.js +13 -0
  29. package/dist/shared-serve.js +107 -65
  30. package/dist/shared-services.global-setup.js +4 -9
  31. package/dist/shared-session-ledger.js +10 -6
  32. package/dist/shared-spec-document.js +2 -64
  33. package/dist/shared-spec-grammar.js +66 -0
  34. package/dist/shared-specs-modules.js +1033 -0
  35. package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
  36. package/dist/shared-stub-server.js +30 -78
  37. package/dist/shared-temp-root.global-setup.js +3 -10
  38. package/dist/shared-testcontainers.adapter.js +111 -8
  39. package/dist/temp-root.global-setup.js +1 -2
  40. package/dist/vitest.d.ts +17 -22
  41. package/dist/vitest.js +37 -53
  42. package/docs/concepts/architecture.md +20 -20
  43. package/docs/concepts/conventions.md +2 -2
  44. package/docs/guides/cli-specs.md +2 -0
  45. package/docs/guides/develop.md +20 -17
  46. package/docs/guides/mobile-specs.md +3 -3
  47. package/docs/guides/release.md +4 -3
  48. package/docs/guides/website-specs.md +1 -1
  49. package/docs/reference/linting.md +2 -2
  50. package/docs/reference/services.md +9 -9
  51. package/docs/reference/stubs.md +4 -2
  52. package/docs/start/set-up-a-project.md +31 -17
  53. package/package.json +10 -10
@@ -49,15 +49,15 @@ A site's dev server, an API the site calls, the bundler a simulator loads from:
49
49
 
50
50
  The options type is `ProcessOptions`, one shape for the one external process every specification starts.
51
51
 
52
- | Option | Means |
53
- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
- | `command` | the shell command that starts it, receiving the chosen port as `PORT` |
55
- | `ready` | a **path** polled until it answers (default `/`), or a **RegExp** over the child's output whose one capture group is the port |
56
- | `port` | a fixed port instead of a free one the OS assigns; a server that cannot bind it (`EADDRINUSE`) is refused naming this option, whatever it printed |
57
- | `cwd` | a working directory relative to the project root, or a function returning one, called when the process starts (once per file, for a spec that lays a fresh project for each) |
58
- | `env` | the child's environment: an object, or a function of the services started beside it |
59
- | `before` | a one-shot command run ONCE per evaluation of the specification module (under vitest's default isolation, once per test file), which must exit 0: a build, a migration, a fixture load; it runs with the same `env` as the command, so a build that reads a variable reads it here too |
60
- | `timeout` | the readiness budget, in milliseconds (default 30 000) |
52
+ | Option | Means |
53
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `command` | the shell command that starts it, receiving the chosen port as `PORT` |
55
+ | `ready` | a **path** polled until it answers (default `/`), or a **RegExp** over the child's output whose one capture group is the port |
56
+ | `port` | a fixed port instead of a free one the OS assigns; a port another process already answers on (`127.0.0.1` or `::1`, the hosts readiness asks) is refused before the command runs, since it would answer in the server's place, and a server that cannot bind it (`EADDRINUSE`) is refused naming this option, whatever it printed |
57
+ | `cwd` | a working directory relative to the project root, or a function returning one, called when the process starts (once per file, for a spec that lays a fresh project for each) |
58
+ | `env` | the child's environment: an object, or a function of the services started beside it |
59
+ | `before` | a one-shot command run ONCE per evaluation of the specification module (under vitest's default isolation, once per test file), which must exit 0: a build, a migration, a fixture load; it runs with the same `env` as the command, so a build that reads a variable reads it here too |
60
+ | `timeout` | the readiness budget, in milliseconds (default 30 000) |
61
61
 
62
62
  It sits in a `services` record like any other service, so it starts after the databases and can be handed their connection strings, and it is stopped with the specification:
63
63
 
@@ -290,9 +290,11 @@ await using _ = await intercept(posts, { origin: 'http://console.test' });
290
290
 
291
291
  **An empty list is refused.** `intercept(defineStubs())` guards nothing, and a subject with no network needs no intercept at all, and the message says so. "This subject makes no network call" is said the other way round: declare each call it could make with [`http.never()`](#never-a-call-that-must-not-happen), and the spec fails if it makes one.
292
292
 
293
- **A request fetch refuses is refused.** Under node, a `fetch` whose RequestInit fetch itself would refuse (an empty method, a forbidden one such as `TRACE`, a `GET` or `HEAD` with a body, an invalid header) rejects with fetch's own `TypeError` before any stub sees it. msw alone would build its request with `init.method || 'GET'` and serve the first three, so a subject's literal method mutated to `''` would be answered by a `GET` stub and its mutant survive. `specs/integration/intercept/refused-init.spec.ts` is the proof. In a page the browser's fetch refuses them before the worker.
293
+ **A request fetch refuses is refused.** Under node, a `fetch` whose RequestInit fetch itself would refuse (an empty method, a forbidden one such as `TRACE`, a `GET` or `HEAD` with a body, an invalid header) rejects with fetch's own `TypeError` before any stub sees it: msw builds the `Request` from the RequestInit, as fetch does, and a subject's literal method mutated to `''` is never answered by a `GET` stub. `specs/integration/intercept/refused-init.spec.ts` is the proof. In a page the browser's fetch refuses them before the worker.
294
294
 
295
- **A body the subject cancels settles.** Under node, every reply to `fetch` reaches the subject with its body as a stream this package owns, so `response.body.cancel()` (what an SDK client does with a body it will not read, before a retry) resolves at once. The status, the headers and the address the reply came from are read as they were served. msw keeps a branch of every body for itself and never cancels it; the owned stream is what stands between that branch and the subject's cancel. A reply no stub answered is owned too, since msw branches the body of a request it passes through to the network as well. `specs/api/intercept/body-cancel.spec.ts` is the proof for a stub's reply: an app that cancels the body of `http.error(503, { ... })` answers its request. `specs/integration/intercept/passthrough-cancel.spec.ts` is the proof for a reply passed through to a real server. In a page nothing is wrapped: the worker's reply reaches the subject through the browser's own network stack, and its cancel settles as served; `specs/component/intercept/quote-status.test.tsx` is the proof.
295
+ **A method off the HTTP parser is refused.** Under node, msw reads a request off the socket with an HTTP parser, which takes only the capitalised methods of node's `http.METHODS`; fetch upper-cases DELETE, GET, HEAD, OPTIONS, POST and PUT and sends any other method as written, so a subject's `patch` (or `Patch`, or `FOO`) would pass through msw unread and reach the real network. While stubs are armed, such a `fetch` never leaves the process: the subject receives a `501` naming it, and the chain fails on rule `network-strict-after-first-stub`, though a `PATCH` stub declares the route. `specs/integration/intercept/refused-init.spec.ts` is the proof. In a page no parser stands between the subject and the handlers: the worker hands msw the request itself, whose method lookup ignores case, so a `PATCH` stub answers a `patch` (the lookup's proof is `src/stubs/delivery/__tests__/msw-handlers.test.ts`).
296
+
297
+ **A body the subject cancels settles.** Under node, `response.body.cancel()` on a reply to `fetch` (what an SDK client does with a body it will not read, before a retry) resolves at once, whether a stub answered or the request passed through to the network: msw hands the reply to undici over a socket, so the body the subject holds is undici's own. `specs/api/intercept/body-cancel.spec.ts` is the proof for a stub's reply: an app that cancels the body of `http.error(503, { ... })` answers its request. `specs/integration/intercept/passthrough-cancel.spec.ts` is the proof for a reply passed through to a real server. In a page the worker's reply reaches the subject through the browser's own network stack, and its cancel settles as served; `specs/component/intercept/quote-status.test.tsx` is the proof.
296
298
 
297
299
  **Around a `.render()` the chain's registration wins.** A rendered unit's network is declared on the chain (`component.intercept(c).render(...)`) because the render registers its stubs last and its strictness is total ([Component specs](../guides/component-specs.md)). A module-scope `intercept()` opened around a `.render()` is shadowed by it and proves nothing. In a `.test.tsx`, `intercept()` is for code the TEST calls itself, never for what the render fetches.
298
300
 
@@ -8,7 +8,9 @@ What a project installs, how its `vitest.config.ts` is written, how it turns the
8
8
  npm install -D @jterrazz/test @jterrazz/typescript vitest
9
9
  ```
10
10
 
11
- **`vitest ^5` and `@jterrazz/typescript ^11` are the only required peers.** The framework registers its matchers into vitest and has no runner of its own. The toolchain, [`@jterrazz/typescript`](https://github.com/jterrazz/package-typescript), is required because the framework prints its refusals through the toolchain's rules kit and its published types extend the toolchain's `JterrazzConfig` (`@jterrazz/typescript/config`). Its lint conventions are a separate, opt-in step ([below](#turning-the-conventions-on)).
11
+ **`vitest ^5.0.4` and `@jterrazz/typescript ^11` are the only required peers.** The framework registers its matchers into vitest and has no runner of its own. The toolchain, [`@jterrazz/typescript`](https://github.com/jterrazz/package-typescript), is required because the framework prints its refusals through the toolchain's rules kit and its published types extend the toolchain's `JterrazzConfig` (`@jterrazz/typescript/config`). Its lint conventions are a separate, opt-in step ([below](#turning-the-conventions-on)).
12
+
13
+ **The vitest floor is msw's.** The framework carries msw 3, and 5.0.4 is the first vitest whose `@vitest/mocker` accepts it. Under an older one the mocker holds msw 2 at the top of `node_modules` and msw 3 lands nested under this package, where Vite never resolves `msw/browser` for a component test; the peer refuses that tree at install instead.
12
14
 
13
15
  ### Project requirements
14
16
 
@@ -51,7 +53,7 @@ read: docs/start/set-up-a-project.md#what-to-install
51
53
  Three more constraints come from the peers themselves:
52
54
 
53
55
  - **One Playwright for the whole family.** This package's `playwright` peer is the exact version it is gated on, and a project declares that version: two revisions on one machine each need their own Chromium build, and `typescript doctor` names the build a project's Playwright wants and the machine lacks. `npx playwright install chromium`, run in the project, provisions it.
54
- - **The browser provider moves with vitest.** `@vitest/browser-playwright@X` requires `vitest` at exactly `X`, so the two are upgraded together: `npm install -D vitest@5.0.1 @vitest/browser-playwright@5.0.1`. `component()` checks the two versions match and names both when they do not.
56
+ - **The browser provider moves with vitest.** `@vitest/browser-playwright@X` requires `vitest` at exactly `X`, so the two are upgraded together: `npm install -D vitest@5.0.5 @vitest/browser-playwright@5.0.5`. `component()` checks the two versions match and names both when they do not.
55
57
  - **Vite `^6.4 || ^7 || ^8`** for a component. `component()` reads the installed major and states the JSX transform on the key that version reads.
56
58
  - **The ARIA dialect of a `.aria.yaml` golden is Playwright's.** A Playwright major can reword a role line, and every committed tree golden then moves with it in one `TEST_UPDATE=1` run.
57
59
 
@@ -98,7 +100,7 @@ It reads the `jterrazz.config.ts` beside the module whose `import.meta.url` it i
98
100
 
99
101
  ### The project helpers
100
102
 
101
- There is one helper per kind (`unit`, `integration`, `component`, `website`, `mobile`, `api`, `job`, `cli` and `lint`), named by the key of a project, and exported from `@jterrazz/test/vitest` beside `vitestConfig`, for a config written by a tool of your own. Each collects its folder (`specs/<kind>/**/*.spec.ts`, plus the `<case>.spec.yaml` documents for `cli()`) and carries what its kind needs: a browser for `component()`, a simulator session for `mobile()`, and for `api()`, `job()` and `integration()` a global setup that starts each service container once and shares it across every file and project of the run ([Services § One container per run](../reference/services.md#one-container-per-run)). Docker is needed only by a spec that declares `postgres()` or `redis()`: with no Docker running, each such file fails with one sentence naming Docker, and every other file runs. Every helper accepts:
103
+ There is one helper per kind (`unit`, `integration`, `component`, `website`, `mobile`, `api`, `job`, `cli` and `lint`), named by the key of a project, and exported from `@jterrazz/test/vitest` beside `vitestConfig`, for a config written by a tool of your own; called directly, `component()` takes its `root` as an absolute path, since only `vitestConfig` knows where the project sits. Each collects its folder (`specs/<kind>/**/*.spec.ts`, plus the `<case>.spec.yaml` documents for `cli()`) and carries what its kind needs: a browser for `component()`, a simulator session for `mobile()`, and for `api()`, `job()` and `integration()` a global setup that starts each service container once and shares it across every file and project of the run ([Services § One container per run](../reference/services.md#one-container-per-run)). Docker is needed only by a spec that declares `postgres()` or `redis()`: with no Docker running, each such file fails with one sentence naming Docker, and every other file runs. Every helper accepts:
102
104
 
103
105
  | Option | Does |
104
106
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -108,9 +110,9 @@ There is one helper per kind (`unit`, `integration`, `component`, `website`, `mo
108
110
 
109
111
  `unit()` and `component()` are a pair: `unit()` collects `**/*.test.ts` outside `specs/` and leaves out `**/*.test.tsx`, which `component()` collects. So the suffix beside a file decides which project runs it. `unit()` also takes `roots` when the modules are not at the repository root, and `isolate: false` to run a worker's files in one module graph, only for a project whose tests leave no state behind in a module, and the default `vitestConfig` hands it. `component()` takes the options of the app it renders (`vite`, `wrap`, `viewport`, `clock`, `timezone`, `locale`, `root`), described in [Component specs](../guides/component-specs.md#the-constructor-there-is-none); `mobile()` takes the simulator the run shares ([Mobile specs](../guides/mobile-specs.md#one-session-per-run)).
110
112
 
111
- Projects run in groups (the node kinds first, then `website`, then `component`, then `mobile`), so two browser projects never open two Chromiums at once and a simulator runs alone.
113
+ Every project runs in one group, on the workers the preset sizes ([below](#what-the-preset-sets)): a `website` file takes a worker as a node file does, and the `component` pages run beside them in their one Chromium, as many at once as the run has workers. A project whose files run one at a time (`serial`, and `mobile()` unless it states `serial: false`) runs after that group, in a group of its own, so a simulator never shares its machine.
112
114
 
113
- `api()`, `job()` and `integration()` also take `runner`, the module their spec files import their runner from, such as `'./specs/api/api.specification.ts'`. The tree derives it where the folder holds one specification module, named for it (`specs/api/api.specification.ts` alone in `specs/api/`), so a project states it only for a module named otherwise. With it, the files of a worker share one module graph (`isolate: false`) and the specification runs again before each file (`setupFiles`): the app's modules load once per worker instead of once per file, and every file still boots its own app on its own services. A spec file imports its runner from the specification itself, since a module of the shared graph that imports it keeps the first file's runner.
115
+ `api()`, `job()`, `integration()`, `cli()` and `website()` also take `runner`, the module (or the list of modules) their spec files import their runner from, such as `'./specs/api/api.specification.ts'`. The tree derives it from the folder: every specification module directly in it (`specs/website/*.specification.ts`), each after the sibling modules it imports, so a module reading another's address reads this file's. With it, the files of a worker share one module graph (`isolate: false`) and every one of those modules runs again before each file (`setupFiles`), booting its subject whichever module the file imports: the worker, the framework and the modules the specs import load once per worker instead of once per file, and every file still boots its own subject (an app on its own services, a binary, a served site) and cleans it up. A spec file imports its runner from the specification itself, since a module of the shared graph that imports it keeps the first file's runner. A `cli()` project shares its worker only when its documents bind one of its runners: a document imports the module its `literate` door names, which the shared graph would not run again.
114
116
 
115
117
  An env-gated sub-suite is an `exclude`, not a second project:
116
118
 
@@ -123,19 +125,20 @@ const integration = {
123
125
 
124
126
  ### What the preset sets
125
127
 
126
- | Setting | Value | Why |
127
- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
128
- | `test.testTimeout`, `hookTimeout` | `30_000` | vitest's 5 s never survived a container boot or a build |
129
- | `test.retry` | `0` | A flaky test is fixed or deleted; a retry hides it |
130
- | `test.restoreMocks`, `unstubGlobals`, `unstubEnvs` | `true` | A spy, a stubbed global or a stubbed env a test forgets to restore would fail a later file that did nothing wrong |
131
- | `test.exclude` | vitest's defaults + `.artifacts/**` + `**/{__expected__,__fixtures__,__requests__,__seeds__}/**` | What a test stands on is input, never a suite to run; `.artifacts/` holds copies of the project, a mutation sandbox among them |
132
- | `cacheDir`, `test.coverage.reportsDirectory`, `test.attachmentsDir`, `test.outputFile` | under `.artifacts/vitest/` | Everything a tool writes lives at `.artifacts/<tool>/` ([below](#where-a-run-writes)) |
133
- | `test.globalSetup` | update mode, the run's temp root | Update mode writes after the run is reported; the temp root is [below](#where-a-run-writes) |
134
- | `plugins` | `literate()`, when `literate:` is given | Turns each `<case>.spec.yaml` into a test file ([Spec documents](../guides/spec-documents.md)) |
128
+ | Setting | Value | Why |
129
+ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
+ | `test.testTimeout`, `hookTimeout` | `30_000` | vitest's 5 s never survived a container boot or a build |
131
+ | `test.retry` | `0` | A flaky test is fixed or deleted; a retry hides it |
132
+ | `test.restoreMocks`, `unstubGlobals`, `unstubEnvs` | `true` | A spy, a stubbed global or a stubbed env a test forgets to restore would fail a later file that did nothing wrong |
133
+ | `test.exclude` | vitest's defaults + `.artifacts/**` + `**/{__expected__,__fixtures__,__requests__,__seeds__}/**` | What a test stands on is input, never a suite to run; `.artifacts/` holds copies of the project, a mutation sandbox among them |
134
+ | `cacheDir`, `test.coverage.reportsDirectory`, `test.attachmentsDir`, `test.outputFile` | under `.artifacts/vitest/` | Everything a tool writes lives at `.artifacts/<tool>/` ([below](#where-a-run-writes)) |
135
+ | `test.globalSetup` | update mode, the run's temp root | Update mode writes after the run is reported; the temp root is [below](#where-a-run-writes) |
136
+ | `test.maxWorkers` | the Vitest workers of the toolchain's capacity model | A run takes its share of the machine beside the gates running on it, and a run inside a gate's slice (`TYPESCRIPT_CAPACITY`) stays in it ([the toolchain's Quality checks § How many test runners](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/quality-checks.md#how-many-test-runners)) |
137
+ | `test.env.NODE_COMPILE_CACHE` | `.artifacts/node-compile-cache/`, as an absolute path | Every Node process a run starts reuses the code an earlier one compiled ([below](#where-a-run-writes)) |
135
138
 
136
- Coverage attaches to every process a run spawns (`autoAttachSubprocess`), and reads `dist/` only to map each bundle back to the source it was built from (`excludeAfterRemap`): a spec that runs the built command counts toward the sources it exercised.
139
+ Coverage attaches to every process a run spawns (`autoAttachSubprocess`), and reads `dist/` only to map each bundle back to the source it was built from (`excludeAfterRemap`): a spec that runs the built command counts toward the sources it exercised. A coverage run compiles without the cache: vitest turns it off, since V8 stores a cached script without the source positions precise coverage reads.
137
140
 
138
- It sets nothing else on purpose. `fileParallelism`, `reporters`, `environment`, `env` and every `include` are true of one project and not of another, so a project states them; a helper does it for you. The coverage provider is yours to install.
141
+ It sets nothing else on purpose. `fileParallelism`, `reporters`, `environment`, every other `env` variable and every `include` are true of one project and not of another, so a project states them; a helper does it for you. The coverage provider is yours to install.
139
142
 
140
143
  The defaults reach every inline project too, since vitest resolves each project as its own config. A project given as a glob string, a promise or a function is handed back untouched.
141
144
 
@@ -175,10 +178,11 @@ export default await oxlintConfig(import.meta.url);
175
178
  | `.artifacts/vitest/coverage/` | the coverage provider | Rewritten per coverage run |
176
179
  | `.artifacts/vitest/sqlite/template-<key>.sqlite` | `sqlite()`'s schema template | Reused until the schema changes |
177
180
  | `.artifacts/vitest/evidence/<pid>-.../` | the screenshot of a website or mobile scenario that failed | Kept until the next run starts |
181
+ | `.artifacts/node-compile-cache/` | Node, for the workers and every process they spawn | Reused across runs |
178
182
 
179
183
  **Everything else is scratch, in one temp folder per run.** Before the first worker starts, the preset creates `jterrazz-test-runs-<uid>/<pid>-...` in the OS temp directory and points `TMPDIR`, `TMP` and `TEMP` at it, so every temp folder a worker, a spawned child or a tool it runs makes lands inside. That holds each CLI spec's working directory, the per-worker database copies and a browser's profile. The run removes the folder when it ends, and the next run removes the folder of any run that died first. A CLI spec's working directory goes sooner, once its test has finished, except under `test.concurrent`, where it waits for the run.
180
184
 
181
- **Two environment variables are yours to set**: `TEST_UPDATE=1`, which turns on update mode ([Tokens § Update mode](../reference/tokens.md#update-mode-tokens-are-preserved)), and `TEST_MOBILE_HEADED=1`, which shows a mobile run's simulator in a window instead of running it headless ([Mobile specs](../guides/mobile-specs.md#the-simulator-and-what-the-run-leaves-on-it)). The framework also reads `VITEST_POOL_ID`, which vitest sets, to isolate each worker's databases.
185
+ **Three environment variables are yours to set**: `TEST_UPDATE=1`, which turns on update mode ([Tokens § Update mode](../reference/tokens.md#update-mode-tokens-are-preserved)), `TESTCONTAINERS_REUSE_ENABLE=true`, which keeps the service containers alive from one local run to the next ([Services § Reused across runs, locally](../reference/services.md#reused-across-runs-locally)), and `TEST_MOBILE_HEADED=1`, which shows a mobile run's simulator in a window instead of running it headless ([Mobile specs](../guides/mobile-specs.md#the-simulator-and-what-the-run-leaves-on-it)). The framework also reads `VITEST_POOL_ID`, which vitest sets, to isolate each worker's databases.
182
186
 
183
187
  ## Two rough edges of the toolchain's flow
184
188
 
@@ -196,6 +200,16 @@ export default await oxlintConfig(import.meta.url);
196
200
 
197
201
  Each release's notes are its [GitHub release](https://github.com/jterrazz/package-test/releases); the package keeps no changelog. A major carries no bridge from the one before (no codemod, no rename map, no rule that recognises the old spellings; see [ADR-014](https://github.com/jterrazz/package-test/blob/main/docs/decisions/014-a-major-carries-no-bridge-from-the-one-before.md)), so a project moves by hand, and the rules of the new major name each leftover in their own terms.
198
202
 
203
+ ### From 17 to 18
204
+
205
+ | What a 17 project wrote | What 18 asks for |
206
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
207
+ | `defineSpecConfig()` and its `SpecConfig` type, in a hand-written config | the derived one-liner, `await vitestConfig(import.meta.url)`, over `test.projects` ([§ The vitest config](#the-vitest-config)) |
208
+ | the `SpecificationConfig` type | nothing in its place: it was the intersection of five chain configurations, and no constructor takes it |
209
+ | `component()` called directly, reading its root off the caller's file | `component({ root })`, an absolute path; a project `vitestConfig` builds is handed its root as before |
210
+ | a strict-stub test matching `declared backend: no stub matches` | `strict stubs: unmatched request`, the one wording every delivery answers an unmatched request with |
211
+ | `vitest` and `@vitest/browser-playwright` below 5.0.4 | both at 5.0.4 or later, in one install ([§ What to install](#what-to-install)) |
212
+
199
213
  ### From 16 to 17
200
214
 
201
215
  | What a 16 project wrote | What 17 asks for |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "17.0.0",
3
+ "version": "18.0.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, background jobs, websites, mobile apps, rendered components and the rules a project ships: one entry point, real infrastructure and a real browser, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",
@@ -84,26 +84,26 @@
84
84
  "test:repeat": "vitest --run --repeats 10"
85
85
  },
86
86
  "dependencies": {
87
- "@oxlint/plugins": "^1.85.0",
88
- "msw": "^2.15.0",
87
+ "@oxlint/plugins": "^1.89.0",
88
+ "msw": "^3.0.3",
89
89
  "oxc-parser": "^0.150.0",
90
90
  "vitest-mock-extended": "^5.0.0",
91
91
  "yaml": "^2.9.0"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@hono/node-server": "^2.1.1",
95
- "@jterrazz/typescript": "^11.0.0",
95
+ "@jterrazz/typescript": "^11.1.0",
96
96
  "@types/better-sqlite3": "^9.6.0",
97
97
  "@types/node": "^26.6.2",
98
98
  "@types/pg": "^8.23.1",
99
99
  "@types/react": "^19.3.0",
100
100
  "@types/react-dom": "^19.3.0",
101
- "@vitest/browser-playwright": "5.0.1",
102
- "@vitest/coverage-v8": "^5.0.1",
101
+ "@vitest/browser-playwright": "5.0.5",
102
+ "@vitest/coverage-v8": "^5.0.5",
103
103
  "ajv": "^8.20.0",
104
104
  "better-sqlite3": "^13.0.3",
105
105
  "hono": "^4.13.9",
106
- "oxlint": "1.85.0",
106
+ "oxlint": "1.89.0",
107
107
  "pg": "^8.23.0",
108
108
  "playwright": "1.63.0",
109
109
  "react": "^19.3.0",
@@ -112,7 +112,7 @@
112
112
  "redis": "^6.2.1",
113
113
  "testcontainers": "^12.1.0",
114
114
  "vite": "^8.3.0",
115
- "vitest": "5.0.1",
115
+ "vitest": "5.0.5",
116
116
  "vitest-browser-react": "^2.3.0",
117
117
  "webdriverio": "^9.32.0"
118
118
  },
@@ -129,9 +129,9 @@
129
129
  "redis": "^6.1.0",
130
130
  "testcontainers": "^12.0.4",
131
131
  "vite": "^6.4.0 || ^7 || ^8",
132
- "vitest": "^5",
132
+ "vitest": "^5.0.4",
133
133
  "vitest-browser-react": "^2.3.0",
134
- "webdriverio": "^9.30.0"
134
+ "webdriverio": "^9.30.0 || ^10.0.0"
135
135
  },
136
136
  "peerDependenciesMeta": {
137
137
  "@vitest/browser-playwright": {