@jterrazz/test 16.1.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +2 -2867
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4551 -2612
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3002
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6073 -5616
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6065 -5609
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +374 -507
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -349
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -244
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -242
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -305
- package/docs/19-linting.md +0 -510
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Services: databases, caches, and their init scripts
|
|
2
|
+
|
|
3
|
+
Infrastructure is declared as a **named record** of service factories on the runner. The record key is the ONLY name a service has: it is your test vocabulary in `.seed()` / `.table()` targeting, it types the `server`/`jobs` factory parameters, it is the name the startup report prints, and, kebab-cased, it is the folder the service reads its init script from. A handle carries no second name of its own.
|
|
4
|
+
|
|
5
|
+
## Service factories
|
|
6
|
+
|
|
7
|
+
All four import from the services subpath, as do the types of a service and its handle (rule `service-or-stub-from-its-subpath`):
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { postgres, process, redis, sqlite } from '@jterrazz/test/services';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Factory | Backing | Options | Connection string shape |
|
|
14
|
+
| ------------ | ------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------- |
|
|
15
|
+
| `postgres()` | Docker container | `image`, `env` | `postgresql://user:pass@host:port/db` |
|
|
16
|
+
| `redis()` | Docker container | `image` | `redis://host:port` |
|
|
17
|
+
| `sqlite()` | **No Docker**: a template file copied per worker | `init` (SQL file) or `prismaSchema` (runs `prisma db push`), paths from the cwd | `file:/.../<name>.sqlite` |
|
|
18
|
+
| `process()` | **No Docker**: a child process the framework owns | `command`, `ready`, `port`, `cwd`, `env`, `before`, `timeout` | `http://127.0.0.1:port` |
|
|
19
|
+
|
|
20
|
+
**Each factory names its own optional peer**, and names it at startup rather than at install time: `postgres()` and `redis()` need `testcontainers` (and `pg` / `redis` for the driver a table read uses), `sqlite()` needs `better-sqlite3`, and `process()` needs nothing but node. A project that declares none of them installs none of them: the drivers are optional peers since 16.0, each loaded lazily by the one adapter under `services/` that owns it ([Set up a project](../start/set-up-a-project.md#what-to-install)). The refusal, when one is missing, names the service that asked, the peer, the install command of the package manager the project's own lockfile names, and (under pnpm) the `onlyBuiltDependencies` line a native binding also needs before it is built at all.
|
|
21
|
+
|
|
22
|
+
`image` overrides the container image and `env` (postgres only) the environment variables; each factory carries a default for both, so a record that states neither still starts a real service.
|
|
23
|
+
|
|
24
|
+
The default image is the slim one, pinned by digest: `postgres:17-alpine` and `redis:7-alpine`, each at the digest of its multi-platform index, held in one constant (`src/services/containers/service-images.ts`). Every machine and every run starts the same bytes; a bare tag would move under the suite, and bumping the pin is a deliberate edit of that constant.
|
|
25
|
+
|
|
26
|
+
A postgres container is disposable, and it is started as one: its data directory is a memory disk (`tmpfs` on `/var/lib/postgresql/data`), and the server runs with `fsync`, `synchronous_commit` and `full_page_writes` off: every write that exists only to survive a crash is skipped. A handle states this as its `containerOptions` (a `command` and `tmpfs` mounts), which the container is started with; a handle of your own may state them too.
|
|
27
|
+
|
|
28
|
+
After the runner starts, each service handle exposes `.connectionString`, which is what you pass to your app:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
export const { api, cleanup } = await specification.api({
|
|
32
|
+
services: {
|
|
33
|
+
db: postgres(), // → reported as "db", init from docker/db/
|
|
34
|
+
analyticsDb: postgres(), // → "analytics-db", init from docker/analytics-db/
|
|
35
|
+
cache: redis(), // → "cache"
|
|
36
|
+
},
|
|
37
|
+
server: ({ db, analyticsDb, cache }) =>
|
|
38
|
+
createApp({
|
|
39
|
+
databaseUrl: db.connectionString,
|
|
40
|
+
analyticsDatabaseUrl: analyticsDb.connectionString,
|
|
41
|
+
redisUrl: cache.connectionString,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `process()`: the one shape an external process takes
|
|
47
|
+
|
|
48
|
+
A site's dev server, an API the site calls, the bundler a simulator loads from: each of them is a command, a way of knowing it is ready, and a lifetime. `process()` is the one shape for all three, and the framework owns the lifetime. Without it each would be declared somewhere else, and the last one would be a `beforeAll` spawning a child and an `afterAll` that sometimes forgets to kill it.
|
|
49
|
+
|
|
50
|
+
The options type is `ProcessOptions`, one shape for the one external process every specification starts.
|
|
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) |
|
|
61
|
+
|
|
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
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { specification } from '@jterrazz/test';
|
|
66
|
+
import { postgres, process } from '@jterrazz/test/services';
|
|
67
|
+
|
|
68
|
+
const api = process({
|
|
69
|
+
before: 'make build',
|
|
70
|
+
command: 'bin/server web --port $PORT',
|
|
71
|
+
env: ({ db }) => ({ DATABASE_URL: db.connectionString }),
|
|
72
|
+
ready: '/health',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const { cleanup, website } = await specification.website({
|
|
76
|
+
server: (services) =>
|
|
77
|
+
process({
|
|
78
|
+
command: 'next dev',
|
|
79
|
+
env: { NEXT_PUBLIC_API_URL: services.api.connectionString },
|
|
80
|
+
}),
|
|
81
|
+
services: { api, db: postgres() },
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Its URL is known once it is ready.** `url` read at declaration time is refused: a process started beside others reads theirs from the started services record: `env: (services) => ...`, `server: (services) => ...`.
|
|
86
|
+
|
|
87
|
+
**The framework ends what it started.** A process is spawned detached (its own process group on POSIX), and on teardown the whole group is sent `SIGTERM`, escalating to `SIGKILL` after a 2 s grace, so what the command forked (a watcher, a background server) dies with it instead of outliving the run. On POSIX, a run that dies before its teardown (killed, `SIGKILL` included) ends the group all the same: a watchdog outside the group holds a pipe from the run, and when the pipe closes it sends the group the same `SIGTERM`, then `SIGKILL`. Every served process is ended the same way, by the same code: a website's server, a mobile run's Appium server, a cli document's services. A mobile run's Appium server owns more than its group: the WebDriverAgent it starts for the simulator runs in a group of its own, and the teardown and the watchdog end it as well ([Mobile specs](../guides/mobile-specs.md#the-simulator-and-what-the-run-leaves-on-it)).
|
|
88
|
+
|
|
89
|
+
`website()` and `mobile()` take a `services` record for exactly this: a site started beside the API it calls, a simulator started beside its Metro bundler (`process({ command: 'expo start', ready: /Metro waiting on .*:(\d+)/ })`). What used to be a bundler bootstrap the repository maintained is a declaration.
|
|
90
|
+
|
|
91
|
+
**The run's id is minted, never sampled.** Every process of one specification is handed `TEST_RUN_ID` in its environment, one value per RUN, where a run is one evaluation of the specification module, so two test files importing the same specification get a child each with an id of its own. A test that needs a label unique to its run reads that variable; it never builds one from `Date.now()` or `Math.random()`, which rule `no-sampled-value-in-expectation` refuses under an oracle and which makes two runs of the same suite disagree.
|
|
92
|
+
|
|
93
|
+
## The services record: three jobs for one key
|
|
94
|
+
|
|
95
|
+
1. **Vocabulary.** The key is how tests target the service: `.seed(file('./__seeds__/x.sql'), { database: 'analyticsDb' })`, `result.table('events', { database: 'analyticsDb' })`. Typed: a typo is a compile error.
|
|
96
|
+
2. **Typing.** The `server` / `jobs` factory receives the exact same record, fully typed, once everything is started (rule `server-sees-declared-services`).
|
|
97
|
+
3. **Identity.** The key is the name the service is known by: the startup report prints it, and its **kebab-case** conversion is the folder the init script is read from. `analyticsDb` reports as `analytics-db` and initialises from `docker/analytics-db/init.sql`, with no option to state anywhere:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
services: {
|
|
101
|
+
db: postgres(), // → "db"
|
|
102
|
+
analyticsDb: postgres(), // → "analytics-db"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In a cli spec, the record also drives env injection into the child process: `<KEY>_URL` per service (the key uppercased to **CONSTANT_CASE** at camelCase boundaries: `analyticsDb` gives `ANALYTICS_DB_URL`), plus `DATABASE_URL` / `REDIS_URL` when unambiguous (rule `service-urls-injected`; see [CLI specs](../guides/cli-specs.md#auto-injected-connection-urls)).
|
|
107
|
+
|
|
108
|
+
### The `database:` rule
|
|
109
|
+
|
|
110
|
+
With **2 or more databases** in the record, `database:` is mandatory on every `.seed()` and `.table()`. With exactly **one**, it is forbidden (redundant). There is no in-between: the option is either always present or never present within a project. A `.seed()` or a `.table()` in a project that declares no database is refused, and so is a `database:` naming no key of the record.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// one database → no database: anywhere
|
|
114
|
+
await migrateCli.seed(file('./__seeds__/legacy-schema.sql')).exec('up');
|
|
115
|
+
await expect(result.table('schema_migrations')).toMatch([...]);
|
|
116
|
+
|
|
117
|
+
// two databases → database: everywhere
|
|
118
|
+
await api.seed(file('./__seeds__/catalog.sql'), { database: 'db' }).request(file('./__requests__/new-order.http'));
|
|
119
|
+
await expect(result.table('events', { database: 'analyticsDb' })).toMatch([...]);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## `docker/<service>/init.sql`
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
docker/
|
|
126
|
+
├── db/
|
|
127
|
+
│ └── init.sql # schema for the "db" service; builds its template database
|
|
128
|
+
└── analytics-db/
|
|
129
|
+
└── init.sql # schema for the "analytics-db" service
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This convention is `postgres()`'s alone: `redis()` takes no init script, and `sqlite()` names its schema with its own `init` or `prismaSchema` option ([below](#sqlite-without-docker)).
|
|
133
|
+
|
|
134
|
+
- `docker/<service>/init.sql` builds the service's **template database**, matched by the **kebab-case of the record key**, so the analytics schema above belongs to `analytics-db/` (the folder `analyticsDb` names).
|
|
135
|
+
- The template is built ONCE per container: its name is the script's digest, one worker builds it under an advisory lock while the others wait and then find it, and each worker then takes `CREATE DATABASE ... TEMPLATE`: a copy of the finished schema, whatever the script did to build it. A script that fails leaves no template behind, and the error names the script.
|
|
136
|
+
- A project with one database needs no folder of its own: a postgres handle falls back to `docker/postgres/init.sql` when nothing sits under its own name.
|
|
137
|
+
- Testcontainers starts each declared service from the handle's image and environment. There is no second definition of the stack to keep in step.
|
|
138
|
+
|
|
139
|
+
## One container per run
|
|
140
|
+
|
|
141
|
+
A container-backed service is started once per RUN, not once per file or per project. The `api()`, `job()` and `integration()` projects each carry a global setup, and every one of them joins the same run-level server: it serves the run's containers on one loopback port and provides that address to every project's workers. The first file that declares a service starts its container; every later file of the run, in any project and any worker, is handed the same one; the last project to tear down stops them. So a run of `api()` and `integration()` that both declare `postgres()` starts one postgres container, and each project's files still take databases of their own. Two declarations alike (same image, port, environment and container options) are one container, so `db: postgres()` and `analyticsDb: postgres()` share one server, and each takes a database of its own ([below](#per-worker-isolation)). Every `redis()` is the same request too, so `cache: redis()` and `queue: redis()` share one container, started with `--databases 256`, and each takes an index of its own: one's reset flushes that index alone and never reaches the other's keys.
|
|
142
|
+
|
|
143
|
+
A project written by hand, or a specification whose helper carries no such setup (`cli()`, `website()`, `mobile()`), is handed no run container. There, every service a specification declares starts a container of its own and stops it with the file: `cache: redis()` and `queue: redis()` are two containers, each still on an index of its own, and two alike `postgres()` are two servers. The sharing above is the services setup's. It is shared within one version of the package: in a workspace whose projects pin different versions of `@jterrazz/test`, each version serves its own run containers, since their protocols may differ.
|
|
144
|
+
|
|
145
|
+
The setup reaches Docker in the background and pulls each pinned default image the daemon lacks, so the first file waits on a pull at most once. When no Docker daemon answers, every file that declares a service fails with one sentence that names Docker, and a project whose specs declare no service never needs Docker at all.
|
|
146
|
+
|
|
147
|
+
### Reused across runs, locally
|
|
148
|
+
|
|
149
|
+
`TESTCONTAINERS_REUSE_ENABLE=true` keeps the containers alive between runs: the next run finds them by the hash of their definition instead of starting them, and a postgres finds its templates already built. It is off unless asked for, and off on CI whatever the variable says: a CI run starts from nothing and leaves nothing. A reused container is one run's at a time: two runs at once on one machine share it, and each would drop the other's worker databases.
|
|
150
|
+
|
|
151
|
+
Every container the package starts carries the label `com.jterrazz.test=true`, and a reused one `com.jterrazz.test.reuse=true` besides, since it outlives the run by design and nothing reaps it.
|
|
152
|
+
|
|
153
|
+
The containers of a run that is not reused are removed at teardown. A run that dies leaves them to testcontainers' reaper (Ryuk), and every run on a machine shares one reaper session while any of them holds it: the session's containers are removed only once its LAST client is gone. A run killed mid-way is therefore reaped only when every run sharing the reaper has ended, and with runs in parallel, that can be hours.
|
|
154
|
+
|
|
155
|
+
The owner sweep covers the time in between. Every container the package starts that is not reused carries `com.jterrazz.test.owner=<host>:<pid>`, the process that started it; every services setup sweeps first, beside the image pulls, and removes a container whose owner names this host and a pid no process answers, running or not, whatever reaper still holds its session.
|
|
156
|
+
|
|
157
|
+
The sweep runs only when a services setup starts, so it bounds a killed run's leftovers without removing them at once. They stay until the next services setup on the machine starts or the reaper's last client is gone, whichever comes first. Nothing sweeps when the setup tears down.
|
|
158
|
+
|
|
159
|
+
The host in the label is `os.hostname()`, which on macOS follows the network the machine is on. A container labelled under an earlier network's name names no host the sweep recognises, so it is never swept and is left to the reaper, or to `docker rm` by the label below. The sweep fails safe: it removes only what it can prove is dead.
|
|
160
|
+
|
|
161
|
+
The same sweep covers a reaper that is gone. It runs with auto-remove, so a runtime that stops every container, or a `docker stop` of them all, takes it away and leaves the session's containers stopped behind it. A container that carries a testcontainers session, has ended, and whose session no running reaper holds is removed too, whichever package or version started it.
|
|
162
|
+
|
|
163
|
+
### Cleaning up leftover containers
|
|
164
|
+
|
|
165
|
+
The label lists every container the package started. The reused ones carry no session and no sweep touches them, so they are removed by hand:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
docker ps -a --filter label=com.jterrazz.test=true
|
|
169
|
+
docker rm -f $(docker ps -aq --filter label=com.jterrazz.test.reuse=true)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Per-worker isolation
|
|
173
|
+
|
|
174
|
+
Vitest runs test files in parallel workers; the framework isolates them automatically: no configuration, no sharding. Vitest draws every worker of a run from one pool, so two workers running at once never share a pool id, whichever project each serves: the bands below hold across the projects that share [one container](#one-container-per-run).
|
|
175
|
+
|
|
176
|
+
| Service | Isolation strategy |
|
|
177
|
+
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
178
|
+
| `postgres()` | A database per worker, cloned from the template its init script built; the connection string's path |
|
|
179
|
+
| `redis()` | A database index per (worker, service) pair (pool id `w`'s services take `4w − 3` to `4w`), acquired empty, the connection string's path |
|
|
180
|
+
| `sqlite()` | Template file copied per worker |
|
|
181
|
+
|
|
182
|
+
The handle's `connectionString` is its own isolation's: a postgres worker's database, a redis (worker, service) pair's index: the app built from it writes where the reset flushes, and a client opened on the container's bare address reaches the container's own database, which no worker uses and no reset flushes.
|
|
183
|
+
|
|
184
|
+
A redis container's 256 indexes are its own `0` and four per worker, so it serves pool ids 1 to 63 holding four redis services each; a process outside a vitest pool is worker `0` and shares pool id 1's band, since it never runs beside a pool against the run's container. A fifth service in one worker, or a pool id past 63, is refused before it connects, with the way out in the sentence. Two handles of one (worker, service) pair share its index, and the index stays theirs until the last of them releases it.
|
|
185
|
+
|
|
186
|
+
On top of worker isolation, **every chain resets the databases** (rule `sequence-expressed-by-seeding`): a spec starts from seeds, never from a previous spec's leftovers. The reset truncates every table and restarts every generated id, and a postgres worker's database is a whole clone, sequences included: the first row a chain inserts is id `1` in every worker, so an id is a value a test may state.
|
|
187
|
+
|
|
188
|
+
## Root auto-discovery
|
|
189
|
+
|
|
190
|
+
The framework locates the project root by walking **up from the specification file** to the **nearest** directory carrying `package.json`, so in a workspace the member being tested wins over the repository root above it. Inside a git work tree it reads the tree git does not ignore (a `.gitignore`d `package.json` marks no root, a new one nobody added does), which is the tree `no-redundant-root` reads too, so the lint and the run derive one root. The `root` option is an override for the cases where the convention cannot work (e.g. fixtures deliberately kept outside the package); pointing `root` at the directory the walk would find anyway is redundant, and `no-redundant-root` refuses it.
|
|
191
|
+
|
|
192
|
+
## SQLite without Docker
|
|
193
|
+
|
|
194
|
+
For CLIs (or apps) on SQLite, tests run with zero Docker:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
export const { cli, cleanup } = await specification.cli('shoply', {
|
|
198
|
+
services: {
|
|
199
|
+
db: sqlite({ init: './schema.sql' }), // schema from a SQL file
|
|
200
|
+
// or: db: sqlite({ prismaSchema: './prisma/schema.prisma' })
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`sqlite()` builds a template database once (from `init` SQL or by running `prisma db push` on `prismaSchema`), then hands each worker its own copy. `.seed()` and `result.table()` work identically to Postgres.
|
|
206
|
+
|
|
207
|
+
`init` and `prismaSchema` are both resolved against the **current working directory**, the folder vitest runs from (the project root when you run it as [Getting started](../start/getting-started.md#5-run-it) does), never against the specification file. They name a file of the project, the schema the app itself ships, not ground a test stands on, so they take a bare path rather than `file()`, which resolves against the file that calls it.
|
|
208
|
+
|
|
209
|
+
`prismaSchema` is passed to the CLI as `npx prisma db push --force-reset --schema <absolute path>`: the schema is named explicitly, so it does not have to be the one Prisma would discover from a `prisma.config.ts` at the cwd. Declare no `prismaSchema` and the bare invocation runs, leaving discovery to Prisma.
|
|
210
|
+
|
|
211
|
+
### Where the template lives
|
|
212
|
+
|
|
213
|
+
The template is a **project** artefact: `<root>/.artifacts/vitest/sqlite/template-<sha8>.sqlite`, under the root `no-redundant-root` discovered ([above](#root-auto-discovery)) and covered by the same `.gitignore` line as everything else in `.artifacts/` ([Set up a project](../start/set-up-a-project.md#where-a-run-writes)).
|
|
214
|
+
|
|
215
|
+
It used to live in the machine-global OS tmpdir, where two checkouts of one repository shared a single file: whichever ran first built it, the other silently inherited that schema, and a branch that changed the schema poisoned the branch beside it. A path under the project root cannot be reached from another checkout at all. Deleting `.artifacts/` is how you force a cold rebuild.
|
|
216
|
+
|
|
217
|
+
The file name stays **keyed on the schema**: the digest taken over the kind of schema (`init` / `prismaSchema`), its resolved path and its content. Within one project that is what makes reuse mean "the same schema": edit a schema and the next run builds a new template instead of reading the old one, whose header is perfectly valid and whose tables are wrong.
|
|
218
|
+
|
|
219
|
+
### One worker builds, the others wait
|
|
220
|
+
|
|
221
|
+
Workers race for a cold cache, so the build is serialised by a lock beside the template, and a worker that loses the race **waits** for the winner rather than building too. It is taken with an exclusive create, so exactly one worker can win it; the check-then-write it replaced let several believe they had won, and the concurrent `prisma db push` calls that followed died on `database is locked`. A suite kept sequential (`fileParallelism: false`) only for that reason can go parallel again.
|
|
222
|
+
|
|
223
|
+
Two more properties fall out of it: the winner builds on a private path and **renames** the finished file into place, so a reader never sees a half-built template (the SQLite header is written long before the tables are); and a lock nobody has touched for two minutes is treated as a crashed holder's and broken, so a killed worker cannot wedge the suite.
|
|
224
|
+
|
|
225
|
+
## Pitfalls
|
|
226
|
+
|
|
227
|
+
- **Importing `process` and reading `process.env` in one file.** The factory shadows Node's global `process`, so `process.env.CI` in that file reads a property of the factory and fails. Alias the factory (`import { process as service } from '@jterrazz/test'`) or read the environment through `import { env } from 'node:process'`.
|
|
228
|
+
- **Naming the `init.sql` folder after the raw record key.** The folder is the key's **kebab-case** form: key `analyticsDb` reads `docker/analytics-db/init.sql`, not `docker/analyticsDb/`.
|
|
229
|
+
- **Sharing state across specs "because the container is shared".** The container is shared; the data is not: chains reset databases (rule `sequence-expressed-by-seeding`), and workers are isolated (rule `state-isolated-per-worker`).
|
|
230
|
+
- **Passing `root` that auto-discovery would have found.** Redundant override (rule `no-redundant-root`).
|
|
231
|
+
- **Reading a green local run as proof that `prismaSchema` is wired.** The template is cached across runs, so a checkout that built it once never re-runs `db push`, so a schema path that would fail on a fresh clone (or in CI) stays invisible. Delete `.artifacts/vitest/sqlite/` to test the cold path.
|
|
232
|
+
- **Counting `sqlite()` + `redis()` as "2 databases" for rule `database-named-only-when-several`.** The rule counts **databases**; with one SQL database and one redis, `database:` stays forbidden on `.seed()`/`.table()` and `DATABASE_URL`/`REDIS_URL` are both unambiguous for CLI injection (rule `service-urls-injected`).
|
|
233
|
+
|
|
234
|
+
## Related
|
|
235
|
+
|
|
236
|
+
[API specs](../guides/api-specs.md) · [Job specs](../guides/job-specs.md) · [CLI specs](../guides/cli-specs.md) · [Conventions](../concepts/conventions.md)
|