@jterrazz/test 16.1.1 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +2 -2980
- 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 +4534 -2628
- 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 -3004
- 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 +6069 -5634
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6127 -5693
- 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 +373 -525
- 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 -438
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -282
- 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 -248
- 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 -332
- package/docs/19-linting.md +0 -512
- 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,333 @@
|
|
|
1
|
+
# Conventions
|
|
2
|
+
|
|
3
|
+
The conventions say where a test lives, what it compares, what it may stand on and how its files are named. Most of them are rules a machine checks, and each of those has its normative sentence in the code, generated into the catalogue of [Linting](../reference/linting.md). This page holds the rest: the principles those rules come from, and the judgements no rule can make, which are review's.
|
|
4
|
+
|
|
5
|
+
They bind every test file of a repository that lists the `test()` preset, a plain unit test that imports nothing of this package included.
|
|
6
|
+
|
|
7
|
+
## Quick rules
|
|
8
|
+
|
|
9
|
+
The rest of the chapter explains each of these. When a rule is broken, the finding names it.
|
|
10
|
+
|
|
11
|
+
- **Where a test goes.** A module's test sits in the `__tests__/` of its module's folder: `dir/__tests__/<file>.test.ts` for `dir/<file>.ts`, or `.test.tsx` for a rendered component. A spec of the assembled product goes under `specs/<kind>/<subject>/<aspect>.spec.ts`. Its runner, `<kind>.specification.ts`, sits at the root of `specs/<kind>/`.
|
|
12
|
+
- **One file, one subject, one `describe`.** The folder names the subject (`users/list/` is `GET /users`). The file name names the question asked of it (`pagination`), and the file's one top-level `describe` repeats that name. A module test has no aspect: its `describe` names the module's export it is about: `describe('rank', ...)` in `__tests__/ranking.test.ts` for `export function rank` ([§ The aspect](#the-aspect)).
|
|
13
|
+
- **Given, then Then.** Every test carries a `// Given -` line and a `// Then -` line. A `// When -` line is optional, in any kind of test; written, it sits between the two (`when-between-given-and-then`), and `given-and-then-markers-present` neither asks for it nor counts it. Its chain has exactly one action.
|
|
14
|
+
- **What a test stands on sits in a special folder, `__<word>__/`.** `__seeds__/` holds SQL, `__requests__/` what is sent, `__fixtures__/` files the program reads, `__expected__/` what is compared, and `__setup__/` helper code. At the root of `specs/` they hold what several kinds share. No other `__<word>__/` is allowed under `specs/`, and in a code root only `__tests__/`.
|
|
15
|
+
- **Write it in the test, or in a file.** A value one test compares, or a request one test sends, is written in the test, up to 30 lines for a request or body and 150 for an expectation. Past those caps, or when several tests read it, it goes in a file. A seed is always a `.sql` file.
|
|
16
|
+
- **Shared things sit at the lowest common folder.** A seed or a helper read by `events/list/` and `events/get/` goes in `events/__seeds__/` or `events/__setup__/`. Only what several kinds share goes in the special folders at the root, `specs/__seeds__/`, `specs/__setup__/`.
|
|
17
|
+
- **Paths are written with `file()` or `directory()`, in the test.** They resolve against the spec file that calls them.
|
|
18
|
+
- **A repository suite is the one exception.** A test of the repository itself, such as what its tarball holds, has no module to sit beside and no runner to reach. It goes in a first-level folder of `specs/` that is not a kind's name ([§ A repository suite](#the-third-door-a-repository-suite)).
|
|
19
|
+
|
|
20
|
+
## Which test earns its place
|
|
21
|
+
|
|
22
|
+
A test earns its place by proving what a user relies on, in real conditions. A test that restates the implementation, repeats a spec, or stays green when the behaviour breaks is deleted, not kept for the count, and a suite's value is measured by the mutants it kills, not by its line coverage. [The fork](../start/getting-started.md#which-kind-of-test) says which kind a test is; this says whether it should exist at all.
|
|
23
|
+
|
|
24
|
+
**End to end is the default.** A behaviour a user relies on is specified through the surface the user meets, on a specification under `specs/`, with the real engines: the app in-process, a declared service ([Services](../reference/services.md)), a real Chromium, the built binary. A module test is the exception, and it is kept only when one of two criteria holds:
|
|
25
|
+
|
|
26
|
+
| Keep the module test when | Because |
|
|
27
|
+
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| the module is a pure algorithm (a parser, a matcher, a ranking) whose edge cases are cheaper at its own level | each edge through the surface costs a whole chain, and at the module it costs one line of a `test.each` |
|
|
29
|
+
| the failure it proves is one no end-to-end path can provoke honestly: a broken invariant, a race, a refused engine | reaching it from the surface would mean faking the engine, and the spec would then prove the fake |
|
|
30
|
+
|
|
31
|
+
**Every test file is given one verdict, and only one.**
|
|
32
|
+
|
|
33
|
+
| Verdict | When | What is done |
|
|
34
|
+
| ----------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
35
|
+
| keep-module | one of the two criteria above holds | it stays in its module's `__tests__/` |
|
|
36
|
+
| promote | it proves something a user relies on, at the wrong level | the spec is written through the surface, then the module test is deleted |
|
|
37
|
+
| delete | it restates the implementation, repeats a spec, tests a private helper, or stays green when the behaviour breaks | it is deleted, not kept for the count |
|
|
38
|
+
| strengthen | it earns its place, and its assertions probe a part of an outcome that can be compared whole | its oracle becomes the whole outcome |
|
|
39
|
+
|
|
40
|
+
**A published package is specified from its artefact.** What a package publishes (every subpath of its `exports` map under each condition, the declarations a consumer compiles against, every bin) is proven against the tarball `npm pack` writes, installed in a fixture's `node_modules/` as a consumer installs it (the package beside what its `dependencies` and required peers reach, and each package the consumer's own install would hold linked by name, never the checkout's whole install), and never against the checkout's `src/` or its own `dist/`: a file the `files` field leaves out, or an import the manifest does not declare, passes every spec that reads the checkout and fails every consumer. This package does it with its own `.link('@jterrazz/test', packed())`, on `specs/cli/packed-cli.specification.ts`: `specs/cli/node/` loads every published subpath and resolves every package the browser build imports under the browser condition, `specs/cli/tsc/` compiles a consumer module against the installed `.d.ts` ([Test](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-meta-test-channel)). The browser build is checked from the install for resolution only: it runs in the component project, which aliases `@jterrazz/test` to the checkout's `dist/browser/index.js`.
|
|
41
|
+
|
|
42
|
+
**An assertion compares an outcome someone would care about**, whole when it can. That half is mechanized, in the `expectations` family of the catalogue ([Linting](../reference/linting.md)): `result-compared-whole`, `no-status-only-test`, `no-existence-only-assertion`, `no-probe-cluster`, `no-double-only-assertion`, `no-sampled-value-in-expectation`.
|
|
43
|
+
|
|
44
|
+
**Mutants measure a suite, not lines.** Full line coverage with surviving mutants proves only that the lines ran; a mutant that survives names the assertion that is missing or too weak. A tool such as [Stryker](https://stryker-mutator.io) finds them. This package runs a zero-survivor mutation verdict inside the gate, incrementally: the module tests over the sources they test, and the whole suite, specs included, over what the module tests let live and the lines a change touches that they leave ([Test § The mutants no test kills](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-mutants-no-test-kills)).
|
|
45
|
+
|
|
46
|
+
**Why no rule decides the verdict.** Whether a module is a pure algorithm, whether a failure is provokable from the surface, and whether a test repeats a spec are readings of the product, not shapes of a file: a rule would have to know every path the specs drive. What a machine can take is taken: the assertion rules above, and the surviving mutant, which is the evidence a verdict is argued from ([What stays human](#what-stays-human-and-why)).
|
|
47
|
+
|
|
48
|
+
## One way to write a test
|
|
49
|
+
|
|
50
|
+
The framework's constructors serve a product reached through an entry: an API, a job, a binary, a page, a screen. The conventions reach further: they bind every test file of the repository, a plain unit test of a pure function included, whether or not it imports `@jterrazz/test`. A repository has one way to write a test, so a reader moving from a module test to an API spec reads the same shape, and a rule that judged only the framework's tests would leave most test files unguarded.
|
|
51
|
+
|
|
52
|
+
So every test sits where its kind says ([the fork](../start/getting-started.md#which-kind-of-test)), narrates a Given then a Then, keeps its doubles out of `src/`, and never sleeps. The hygiene floor (no committed `.only` or `.skip`, an assertion in every test, no two titles alike in a file, a lowercase title) is oxlint's own `vitest` plugin, which `@jterrazz/typescript` turns on in every profile; this package adds only what that plugin has no rule for ([Linting § What oxlint's own `vitest` plugin owns](../reference/linting.md#what-oxlints-own-vitest-plugin-owns)).
|
|
53
|
+
|
|
54
|
+
## One file, one subject
|
|
55
|
+
|
|
56
|
+
A spec file answers one question about one subject, and both are written in its path: the folder is the SUBJECT, the file name the ASPECT asked of it: `<specs>/api/events/list/pagination.spec.ts` is the pagination of `GET /events`. A reader who knows the route knows where its specs are, and a change to that route opens one folder.
|
|
57
|
+
|
|
58
|
+
### The subject, and its standard name
|
|
59
|
+
|
|
60
|
+
The subject is what the file's chains reach, read statically: an api chain's method and route, a job chain's job, a cli chain's command, a website chain's page, a mobile chain's journey. A file reaches ONE (`one-subject-per-spec`); a file reaching several is several files, and `typescript fix` splits it, each test going with the imports and file-level constants it reads, whole or not at all: where it would have to guess (a file to name, two subjects on one folder, a test in a loop) or would write fewer tests than it read, it leaves the file and the finding says why ([the row](../reference/linting.md#one-subject-per-spec)). What two of the new files still read is written once: the split moves it, exported, into `<lowest common folder>/__setup__/<aspect>.ts` (the aspect of the file it splits), and each file imports it, never a copy per file; the folder is the one the placement law of [the naming recap](#the-naming-recap) names. A declaration that builds a `file()` or a `directory()` stays in the files that read it, since a module no test is may not build a path (`paths-named-in-the-test`).
|
|
61
|
+
|
|
62
|
+
The folder is the subject's STANDARD name, derived and never listed (`spec-in-subject-folder`), so two repositories never argue it:
|
|
63
|
+
|
|
64
|
+
| Subject | Folder |
|
|
65
|
+
| ------------------------------- | ----------------------------- |
|
|
66
|
+
| `GET /events` | `events/list/` |
|
|
67
|
+
| `GET /events/:id` | `events/get/` |
|
|
68
|
+
| `POST /events` | `events/create/` |
|
|
69
|
+
| `PUT` or `PATCH /events/:id` | `events/update/` |
|
|
70
|
+
| `DELETE /events/:id` | `events/delete/` |
|
|
71
|
+
| `GET /events/:id/timeline` | `events/timeline/` |
|
|
72
|
+
| `GET /health`, a singleton | `health/get/` |
|
|
73
|
+
| `GET /market/:symbol/chart` | `market/chart/` |
|
|
74
|
+
| a method-less route, `ALL /mcp` | `mcp/`, whatever the verb |
|
|
75
|
+
| a job | `specs/job/<job>/` |
|
|
76
|
+
| a cli command, `spawn shop` | `specs/cli/<word>/`, `spawn/` |
|
|
77
|
+
|
|
78
|
+
How the folder is derived:
|
|
79
|
+
|
|
80
|
+
- **Collections.** A GET on one segment lists a plural noun and gets anything else.
|
|
81
|
+
- **Parameters.** A segment is a parameter when the chain writes the route's template (`.get('/market/:symbol/chart', { params: { symbol: 'btc' } })`), or when it is anything but lowercase words (`42`, `AAPL`, a template hole).
|
|
82
|
+
- **An ambiguous word.** A filled path whose word sits right after a collection with more path after it (`btc` in `/indicators/btc/chart`) reads two ways, a value or a segment of the route. `spec-in-subject-folder` files no folder from it and asks for the template, or for `{ params: {} }` when the word is the route's own.
|
|
83
|
+
- **Collections English does not mark.** The plural test cannot know them, so a repository names them in `jterrazz.config.ts`: `test: { collections: ['data', 'people'] }`, and `GET /data` is filed under `data/list/`.
|
|
84
|
+
- **Routes taken on any method.** A chain cannot tell a route that takes any method (`app.all('/mcp', ...)`) from one that takes the verb it sends, so a repository names those there too: `test: { methodless: { '/mcp': 'the MCP transport answers GET and POST' } }`. A POST and a PUT to `/mcp` are then one subject, `ALL /mcp`, filed under `mcp/` and never split by verb. The api run holds the list to the app's own route table, when the app exposes one ([API specs § A route taken on any method](../guides/api-specs.md#a-route-taken-on-any-method)).
|
|
85
|
+
- **Commands.** A command is its first word, so a positional argument is never read as a subcommand (`exec('spawn shop')` is `spawn/`). A runner whose commands take several words declares them, `specification.cli(bin, { commands: ['db', 'migrate'] })`, and a command is then every leading word the list holds: `exec('db migrate --dry-run')` is `db/migrate/`.
|
|
86
|
+
- **Pages and journeys** have no standard name: the file reaches one, and its folder stays the author's.
|
|
87
|
+
- **A test reaching two subjects** cannot be cut. It states what it stands on with a seed rather than a second action.
|
|
88
|
+
|
|
89
|
+
### The aspect
|
|
90
|
+
|
|
91
|
+
The file name is the aspect, and the file's one top-level `describe` is titled with it (`one-describe-per-file`), so a failure's first word names its file. An aspect is kebab-case and says which question the file asks, never a name for what fit nowhere else (`misc`, `other`, `basic`, `general`, `more`, `edge-cases`, `tests`, `main`; `spec-named-for-its-aspect`). For an api, the vocabulary to reach for first, advised and not enforced:
|
|
92
|
+
|
|
93
|
+
| Aspect | Asks |
|
|
94
|
+
| ------------ | ----------------------------------------------- |
|
|
95
|
+
| `pagination` | the pages, their size, their cursor |
|
|
96
|
+
| `filters` | what a query parameter narrows |
|
|
97
|
+
| `sorting` | the order a list comes back in |
|
|
98
|
+
| `language` | what the `Accept-Language` of a request changes |
|
|
99
|
+
| `validation` | what a malformed request is refused with |
|
|
100
|
+
| `not-found` | what an unknown id answers |
|
|
101
|
+
| `auth` | who may, and what the others get |
|
|
102
|
+
| `shape` | the whole response of the ordinary case |
|
|
103
|
+
|
|
104
|
+
A module test is held to the same one `describe`, titled with its SUBJECT rather than an aspect: the symbol of the module it names that the test is about (the one in the folder above its `__tests__/`), read from that module's exports: `__tests__/locale-note.test.tsx` for the `LocaleNote` it renders opens `describe('LocaleNote', ...)`. Which export answers, in order, is [`one-describe-per-file`'s row](../reference/linting.md#one-describe-per-file); a test naming no module is titled with its base name.
|
|
105
|
+
|
|
106
|
+
A file holds at most 400 lines (`spec-file-within-line-limit`); past it, the tests usually group by a prefix of their titles, and that is where the file is cut.
|
|
107
|
+
|
|
108
|
+
A test's body spans at most 60 lines of gestures (`test-body-within-line-limit`), in a module test as in a spec; the value a `toMatch`, a `toStrictEqual` or a `toEqual` compares is the claim, not a gesture, and `ground-shared-or-inline` caps a `toMatch` one instead. The toolchain's function cap leaves a test file alone, because the file's one `describe` is a function as long as the file; the cap it would have put on each test is this one.
|
|
109
|
+
|
|
110
|
+
### A specimen's spec
|
|
111
|
+
|
|
112
|
+
This section matters only to a repository that tests a testing tool. A framework specifies itself against small sample apps it keeps as ground, under `specs/__fixtures__/`. These are SPECIMENS, not the project's product. A spec that drives a specimen specifies the FRAMEWORK, and the route, the job or the command the specimen answers on is no subject of the project: `one-subject-per-spec` and `spec-in-subject-folder` leave it alone.
|
|
113
|
+
|
|
114
|
+
A spec drives a specimen when either holds:
|
|
115
|
+
|
|
116
|
+
- **its runner names the shared fixtures**: the specification module, the one that module builds on, or the spec itself when it builds its runner inline. It names them by:
|
|
117
|
+
- a file inside a specimen's folder: the app module, the binary, the script a command runs;
|
|
118
|
+
- or the folder itself, as the root a `server: { command }` of the runner it feeds runs from, written in the call or in a `const` the call names. A root alone does not count: it is where the services read their ground, and no app.
|
|
119
|
+
- **it is an integration spec importing the module of the shared fixtures its chains call.**
|
|
120
|
+
|
|
121
|
+
Where such a spec is filed is the framework's own to say; this package's answer is [Test § A specimen's capability folder](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#a-specimens-capability-folder). An application's specs keep the route's folder; its routes are its product.
|
|
122
|
+
|
|
123
|
+
### What a file shares
|
|
124
|
+
|
|
125
|
+
A file shares only what its tests share. A SETTING every chain of a file repeats (the same `.clock(...)`, the same `.headers(...)`, the same `.env(...)`) is stated once, on a view of the specification: `const events = api.with({ clock: '2026-03-04T09:30:00Z' })`, every chain of `events` starting with it and its own setups merging over it. A seed and a stub are the Given of one test and stay on its chain; `no-repeated-chain-setting` refuses the setting every chain repeats, and `typescript fix` declares the view. A file-level expectation serves two tests or more, and one a single test reads is written in that test (`expected-constant-shared`). An expectation is never imported from another module of the specs tree (`no-imported-expectation`): a helper the test calls, a stub, the runner are imports; the value a matcher compares is the test's own. The product's own data is the subject's, not an expectation: a spec may compare against `package.json`'s version or a constant the product exports.
|
|
126
|
+
|
|
127
|
+
## The naming recap
|
|
128
|
+
|
|
129
|
+
**One folder per verb that reads the disk.** Each verb that takes what a spec stands on has its folder, named for what it holds: `seed()` reads `__seeds__/`, `fixture()` reads `__fixtures__/`, `intercept()` takes what `__setup__/stubs/` declares, `request()` reads `__requests__/`, and `toMatch()` reads `__expected__/`. The inputs come in several kinds because several mechanisms set them up: a database, a working directory, the network, the request itself. The expectation is one kind, because one verb compares every subject.
|
|
130
|
+
|
|
131
|
+
One rule decides every folder of a spec tree: **a folder whose name starts with `_` is never a subject, and a subject never starts with `_`.** A subject's folder is derived (from a route, a command, a job), so it may take any word (`GET /requests` files under `api/requests/list/`), and only the marks keep the folders that are no subject from colliding with one: a special folder is spelt `__<word>__/`, the spelling the ecosystem already reads as no route (Jest's `__tests__/`, `__mocks__/`). The special folders are a closed list, one in the kit that every tool's globs read (`special-folder-in-closed-list`, `reserved-folder-in-closed-list`):
|
|
132
|
+
|
|
133
|
+
- `__setup__/`: the TypeScript that prepares specs: a test adapter, a shared value, a helper, a stub facade and its kind folders (`__setup__/stubs/`). Never a spec, never data. It is code: linted, formatted and typechecked like any module, and every module of a specs tree that is no spec, test or runner lives in one (`spec-code-in-setup-folder`).
|
|
134
|
+
- `__fixtures__/`, `__expected__/`, `__requests__/`, `__seeds__/`: the GROUND, material a spec names by its path, spelled in full on the line that reads it (`file('./__seeds__/users.sql')`, `directory('../../__fixtures__/shop/')`). The ground names are reserved wherever a test stands on a file, in a module's `__tests__/` under `src/` as much as under `specs/`: the toolchain and the vitest preset ignore all four at any depth, and production code may import nothing from them (`no-test-code-in-production`).
|
|
135
|
+
|
|
136
|
+
At the specs root, the same five hold what the specifications share; there is no folder of shared things: the placement law below puts a thing at the root exactly when its readers span specifications. Any other `_` name is refused, its fix naming the list: an unknown `__<word>__/` (`__mocks__/`, `__snapshots__/`, `__helpers__/`), a subject that starts with `_`, and an earlier spelling. `_setup/`, `_fixtures/`, `_expected/`, `_requests/` and `_seeds/` name their special folder, and what `_shared/` held moves to the special folders at the specs root.
|
|
137
|
+
|
|
138
|
+
A folder with a special name but no marks (`setup/`, `seeds/`, `requests/`, `expected/`, `fixtures/`, and `doubles/` for test adapters) is refused where it holds no spec, the fix naming its special folder; a `shared/` holding no spec is refused too, its fix the special folders of the folder that encloses it. Inside a `__setup__/` the names are the code's own, and `__setup__/` holds code alone: a ground folder or a file that is no module inside it is refused, the fix naming the ground folder of its role beside it. `docker/` sits at the project root, outside any row of specs, and is untouched by the rule.
|
|
139
|
+
|
|
140
|
+
**The specs root holds its specifications and the special folders of what they share, and nothing else** (`specs-root-holds-specifications-and-shared`):
|
|
141
|
+
|
|
142
|
+
- A repository suite is a first-level folder too ([the third door](#the-third-door-a-repository-suite)).
|
|
143
|
+
- A specification folder holds its runners at its ROOT (`<name>.specification.ts`, the one place a runner lives, in every `depth` mode: `specs-at-declared-depth`), then subject folders, specs and special folders. Any other folder holds subject folders, specs and special folders.
|
|
144
|
+
- What the specifications share sits in the special folders of its role at the root: `<specs>/__setup__/`, `<specs>/__fixtures__/`, `<specs>/__seeds__/`, `<specs>/__requests__/`, `<specs>/__expected__/`.
|
|
145
|
+
- A seed the specifications share is `<specs>/__seeds__/catalog.sql`, reached as `../../__seeds__/catalog.sql`. Parked at `<specs>/__fixtures__/__seeds__/`, it is file state by its first ground folder, and `verb-reads-its-ground-folder` refuses it, naming `<specs>/__seeds__/`.
|
|
146
|
+
|
|
147
|
+
**A test adapter is named for the port it replaces**: `<port>.adapter.ts` in a `__setup__/`, whatever its class is called: `class FixedTickerSource implements TickerSourcePort` is `ticker-source.adapter.ts` (`adapter-named-for-its-port`, which reads a class implementing a type imported from outside the specs tree; a double built another way is review's to name). "Fixture" means input data: a file in `__fixtures__/`, or a module test's `__tests__/<file>.fixtures.ts` under `src/`; under `specs/` a `*.fixtures.ts` is refused, and the module is named for what it holds.
|
|
148
|
+
|
|
149
|
+
Three further rules decide WHETHER and WHERE shared things live, and they are one question asked from three sides. **The placement law is the same for code and for data: a shared thing sits in the reserved folder of the LOWEST folder every one of its readers shares**: beside them when one leaf reads it, in `events/__seeds__/` or `events/__setup__/` when `events/list/` and `events/get/` do, and in the special folders at the specs root only when its readers span specifications. `ground-shared-or-inline` holds data to it, `setup-module-at-lowest-common-folder` holds a `__setup__/` module to it, read on the imports: a module one spec alone imports belongs in that spec, as a file one test alone reads does.
|
|
150
|
+
|
|
151
|
+
**A file exists only if it is shared** (`ground-shared-or-inline`, both halves autofixed by `typescript fix`):
|
|
152
|
+
|
|
153
|
+
- **One reader: write it in place.** A golden or a request ONE test reads, and no module test names, belongs in that test: the value `toMatch` compares, the request's text or its body.
|
|
154
|
+
- **The caps.** The exceptions are a directory tree, a binary file, a text no literal survives in place (one opening on a line break whose lines share an indentation, which the comparison's dedent strips from either form), or a value longer than its role's cap: 30 lines for a request or a body, 150 for an expectation. The caps hold the other way too: a request written in the test past 30 lines, or an expectation past 150, belongs in a file beside the test.
|
|
155
|
+
- **Always files.** A `__fixtures__/` entry is file state and stays a file, and so does a seed: SQL is never written in a test, so a seed one test reads lives beside it in `./__seeds__/`.
|
|
156
|
+
- **Several readers: the lowest folder they share.** An entry kept above it (the shared ground's included) belongs in that folder's ground of the same role: `<specs>/__seeds__/x.sql` read by one leaf becomes `<leaf>/__seeds__/x.sql`. Every reader counts where it stands: a test at its folder, a runner at its specification root, a `__setup__/` module at the folder that owns that `__setup__/`.
|
|
157
|
+
- **Readers outside the specs tree.** Only a reader outside the specs tree (a module test, the member's Makefile or its vitest config) keeps an entry where it is whatever the specs say, by a relative literal, the literal pieces `named-path-exists` reads, or a path spelled from the member root (`specs/<path>`).
|
|
158
|
+
|
|
159
|
+
**What several leaves share lives there once**: an entry two leaves hold byte for byte, under the same role, is a copy whatever each is called, and belongs once in the ground of the lowest folder they share (`no-duplicate-ground`; two leaves that mean to diverge tomorrow diverge the day they do, and a copy held until then is debt the ratchet records; when the copies share a name, `typescript fix` moves one up, deletes the others and rewrites every relative literal that named one; copies of several names wait for the author to pick the name).
|
|
160
|
+
|
|
161
|
+
**A path leaves its leaf only by climbing**: a `file()` or `directory()` whose target is neither under the referring spec's own directory, nor under the ground of a folder that encloses it, nor under one of the four at the specs root, is an error (`no-reach-into-sibling-ground`); a leaf reaching into a sibling's ground is one such path. Climbing is how sharing reads: `events/list/` and `events/get/` reach `../__seeds__/` of `events/`, and a route's leaf reaches the ground of the route above it (`map/pois/detail/` reads `../__seeds__/base.sql` of `map/pois/`).
|
|
162
|
+
|
|
163
|
+
So write ground and helpers local first: the day a second leaf copies an entry, `no-duplicate-ground` moves it up to the folder they share, the day only one leaf is left, `ground-shared-or-inline` moves it back down, and the day only one test is left, `ground-shared-or-inline` writes it into that test. Where a shared thing sits says who depends on it: a reader trusts the folder to tell them what a change reaches, and it does only while the thing sits exactly as high as its readers.
|
|
164
|
+
|
|
165
|
+
Two more hold what each path says. **The verb and the folder agree**: `seed()` reads `__seeds__/`, `request()` and a request body read `__requests__/`, `fixture()` reads `__fixtures__/`, a golden reads `__expected__/` (`verb-reads-its-ground-folder`). **What a path names exists**, of the kind its wrapper says, and so does a path a test resolves from its own directory with literal pieces alone (`resolve(import.meta.dirname, '..', 'fixtures', 'gone')`, one piece only when spelled `./` or `../`, since a lone bare piece is as often a folder the test makes), a module test's as a spec's (`named-path-exists`, the checker, reported where the path is written; the runtime refuses the same with the path it resolved to). A fixer that moves ground rewrites every whole relative literal that named it, a module test's included; a path built from pieces, or spelled into a command a process runs, is the author's to rewrite, and `named-path-exists` names it once the ground has moved. And a path is **built in the test that stands on it**: it resolves against the spec that starts the chain, so a `file()` built in a shared helper would name a different file for every spec that reaches it; a helper takes the `file()` as a parameter (`paths-named-in-the-test`).
|
|
166
|
+
|
|
167
|
+
| Thing | Rule |
|
|
168
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| Specs root | `specs/` (`api/`, `job/`, `cli/`, `integration/`, `website/`, `mobile/`, `lint/`): one folder per constructor, beside the special folders of what the specifications share: `__setup__/`, `__fixtures__/`, `__seeds__/`, `__requests__/`, `__expected__/`, and any repository suite ([the third door](#the-third-door-a-repository-suite)) |
|
|
170
|
+
| Specification | `specs/<specification>/<name>.specification.ts` (at the specification root) |
|
|
171
|
+
| Instances | `api`, `job`, `cli`, `integration`, `website`, `mobile`, `lint`, enforced by the destructuring (`no-runner-alias`) |
|
|
172
|
+
| Spec file | `specs/<specification>/<subject>/<aspect>.spec.ts`, the assembled product's word (`file-suffix-matches-place`); the subject's folder derived where the specification has one ([One file, one subject](#one-file-one-subject)) |
|
|
173
|
+
| Spec document | `<case>.spec.yaml`, beside the spec it belongs to, never under `__expected__/` ([Spec documents](../guides/spec-documents.md)) |
|
|
174
|
+
| Module test | `__tests__/<file>.test.ts`, in the folder of `<file>.ts` (under `src/`) |
|
|
175
|
+
| Component test | `__tests__/<file>.test.tsx`, in the folder of `<file>.tsx`, or of the `<file>.ts` of a hook or a DOM function; beside it under `specs/` ([Component specs](../guides/component-specs.md)) |
|
|
176
|
+
| Module fixtures | `__tests__/<file>.fixtures.ts`, beside the `.test.ts` under `src/` (typed exports): outside `specs/`, the one name the tests of a module share code or data through, test material only a test imports (`fixtures-imported-by-tests-only`). What the module tests of several folders share sits in the `__tests__/` of the lowest folder they share (`test-in-its-tests-folder`); never under `specs/`, where a fixture is data in `__fixtures__/` |
|
|
177
|
+
| Setup | `__setup__/<meaning>.ts`, in the lowest folder its readers share; a test adapter `__setup__/<port>.adapter.ts`, named for the port it replaces |
|
|
178
|
+
| Stubs | `__setup__/stubs/<name>.stubs.ts` (facade) · `__setup__/stubs/<kind>/<name>.ts` (unit, kind ∈ http\|openai\|anthropic, the stub builders the package exports) |
|
|
179
|
+
| Stub data | `__fixtures__/<name>.json` (served, imported by its unit) · `__setup__/stubs/<kind>/<name>.request.ts` (matched) |
|
|
180
|
+
| Requests | what a spec sends: `__requests__/<name>.http`, read by `.request(file('./__requests__/<name>.http'))`, and a body `__requests__/<name>.<ext>`, sent by `.post(path, file('./__requests__/<name>.json'))` |
|
|
181
|
+
| Seeds | database state: `__seeds__/<name>.sql`, read by `.seed(file('./__seeds__/<name>.sql'))`, beside its test, or at the level of the tests that share it; SQL is never written in a test |
|
|
182
|
+
| Fixtures | `__fixtures__/<name>` (file state), in the lowest folder its readers share; across specifications the pool `specs/__fixtures__/`, reached by the path that climbs to it, as `<specs>/__seeds__/` is for seeds |
|
|
183
|
+
| Goldens | `__expected__/<name>.<ext>` (everything asserted, response `.http` included), read by `.toMatch(file('./__expected__/<name>.<ext>'))`; a tree by `.toMatch(directory('./__expected__/<name>/'))` |
|
|
184
|
+
| Service keys | name the service: the report prints the key, and its kebab-case form is the folder its init script sits in |
|
|
185
|
+
| Framework env | `TEST_UPDATE` |
|
|
186
|
+
|
|
187
|
+
## The third door: a repository suite
|
|
188
|
+
|
|
189
|
+
Two doors carry every test the fork names: a UNIT sits in the `__tests__/` of the folder of the module it covers (`<file>.test.ts`, `test-in-its-tests-folder`), and the ASSEMBLED product sits under `specs/<specification>/` and reaches the runner that folder promises (`<aspect>.spec.ts`, `file-suffix-matches-place` and `specification-test-uses-its-runner`). A few specifications have neither address, and they are not mistakes.
|
|
190
|
+
|
|
191
|
+
### What has no address of its own
|
|
192
|
+
|
|
193
|
+
What they specify is a REPOSITORY's own surface read as files: what its packed tarball carries, the links its shipped pages hold, the files a build is expected to write. There is no one module to sit beside (they resolve every module there is) and no runner to reach, since nothing is started. Put under a specification folder, `specification-test-uses-its-runner` refuses them ("reaches no runner"); put beside a module under `src/`, `test-in-its-tests-folder` refuses them ("no neighbour `exports.ts` found"). Both refusals are right, and a legitimate file shape with no green position is a hole in the conventions, not a defect in the file.
|
|
194
|
+
|
|
195
|
+
### The door, and the depth it is judged by
|
|
196
|
+
|
|
197
|
+
**The third door is a FIRST-LEVEL folder under `specs/` that is not a specification name.** It is a repository suite: it covers a TREE rather than a product served through an entry. `file-suffix-matches-place`'s rename clause does not reach it, `specification-test-uses-its-runner` and `specification-folder-has-its-runner` do not reach it, and `specs-at-declared-depth`'s declared depth is what judges its shape.
|
|
198
|
+
|
|
199
|
+
**`specs-at-declared-depth` judges a suite exactly as it judges a specification.** The suite folder is the first level and the tests sit one level down, in a folder named for what they cover: at the default depth, `specs/<suite>/<domain>/<aspect>.test.ts`. A test written straight into the suite folder is refused for the same reason one at a specification root is: the level that says what a file covers would be missing.
|
|
200
|
+
|
|
201
|
+
**A suite is a door because it holds tests.** One test is enough: a first-level folder holding a single `.test.ts` is a suite, and legal. A first-level folder that holds none (a `support/` of helpers and mocks) is no suite, no specification and no special folder, and `specs-at-declared-depth` refuses each module in it, a runner included, in every mode: a runner lives at the root of the specification it builds, a test helper in the `__setup__/` of the lowest folder its tests share, a fixture under `specs/__fixtures__/`.
|
|
202
|
+
|
|
203
|
+
### The one shared suite name
|
|
204
|
+
|
|
205
|
+
One suite carries a name every repository shares, because two repositories inventing two names for one idea is the drift a vocabulary exists to stop: a package's own published SURFACE is `surface/`. Every other suite is named for what it covers. The rules a repository ships are no suite: they are the lint specification's subject, and `specs/lint/` is that specification's folder ([Lint specs](../guides/lint-specs.md)).
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
specs/
|
|
209
|
+
└── surface/ # what the package publishes
|
|
210
|
+
└── package/
|
|
211
|
+
├── tarball.test.ts # the files `npm pack` writes, and none it should not
|
|
212
|
+
└── shipped-links.test.ts # every link a shipped page holds resolves
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
What a consumer DOES with the package (loading a subpath, compiling against its declarations, running its bin) is no suite's: it is a cli spec on the tarball installed as a consumer installs it ([Which test earns its place](#which-test-earns-its-place)).
|
|
216
|
+
|
|
217
|
+
### The suffix, and the one thing the door does not license
|
|
218
|
+
|
|
219
|
+
The files there carry `.test.ts` (the UNIT's word) because what they prove is read without assembling anything, and `unit()` is what collects them (`roots`, or `include` outright). A specification helper would put them in a project whose budget and services they never use.
|
|
220
|
+
|
|
221
|
+
One thing the door does not license: a module test parked under `specs/` to escape `test-in-its-tests-folder`. The question to ask of a file before it goes through is whether any single module could hold its test: if one could, that module is its address.
|
|
222
|
+
|
|
223
|
+
## Process rules (review-borne)
|
|
224
|
+
|
|
225
|
+
Three rules cannot be mechanized: they turn on judgement no single channel can settle. They are listed in the catalogue for completeness, but their full rationale lives here.
|
|
226
|
+
|
|
227
|
+
### The folder follows the assets
|
|
228
|
+
|
|
229
|
+
The grouping criterion: a test that owns **its own** asset directories (`__fixtures__/`, `__expected__/`, `__seeds__/`, ...) gets **its own** domain folder; specs **without local assets** (or sharing the `specs/__fixtures__/` pool) group as sibling `<aspect>.spec.ts` files inside a named **group** folder. Both shapes are legal: the assets decide, and a nascent single-test domain is legitimate. The static rule `specs-at-declared-depth` checks only placement; which of the two shapes is right is the review call.
|
|
230
|
+
|
|
231
|
+
Placement itself is **declared**, because a spec tree may legitimately have a shape this package cannot know:
|
|
232
|
+
|
|
233
|
+
```jsonc
|
|
234
|
+
"jterrazz/specs-at-declared-depth": ["error", { "depth": "specification-domain" }] // the default
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| `depth` | The tree |
|
|
238
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
239
|
+
| `'specification-domain'` | the default: `specs/<specification>/<domain>/<aspect>.spec.ts`, `*.specification.ts` at the specification root; an api or cli spec's domain is its subject's standard name, one folder or several (`events/list/`) |
|
|
240
|
+
| `'specification'` | the assets decide the folder: a spec at the specification root (`specs/<specification>/<aspect>.spec.ts`) OR one domain down, never deeper; `*.specification.ts` at the specification root |
|
|
241
|
+
| `'mirror'` | the tree mirrors a structure outside itself (a command tree, a source tree): a spec at any depth ≥ 1, named `<dir>/<dir>.spec.ts` |
|
|
242
|
+
| `'off'` | no placement check, for a tree whose shape is guarded by something stronger and project-specific |
|
|
243
|
+
|
|
244
|
+
A project states the shape it has (`specification` when asset-less tests sit beside their siblings at the specification root, `mirror` when the tree mirrors something outside itself) and keeps a checked shape, instead of switching the rule off and keeping none.
|
|
245
|
+
|
|
246
|
+
One clause of the rule is not the project's to declare, and holds in every mode, `off` included: **a folder named `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` is ground, never a specification, domain or suite folder**, so no spec lives inside one, and a domain named with one of the four words is refused. Depth is a shape a tree may choose; the ground/member split is the naming law recapped in [The naming recap](#the-naming-recap).
|
|
247
|
+
|
|
248
|
+
Ground is not always inert. It may be **code** the specs stand on (the build of the subject under test, a harness the runner spawns), and code carries its unit test beside it, since a specs tree holds no `__tests__/`. So the clause lets exactly one pairing through: `<module>.test.ts` NEXT to the `<module>.ts` it is named after, inside the ground it belongs to. A test with no module beside it, or a `*.specification.ts`, is a spec that wandered in and is still reported, which is why the clause needs no `off`, and why no project has to switch off a rule it cannot switch off.
|
|
249
|
+
|
|
250
|
+
### `output-asserted-as-a-golden`: the whole result, not a cluster of greps
|
|
251
|
+
|
|
252
|
+
**Every test compares its whole result.** A response, a json, a table, a tool's output (a linter, a compiler, a product CLI) is asserted as a **full snapshot per scoped use case** (written where the test reads it, ``expect(result.response).toMatch(`...`)``, or in a file when several tests share it), and a precise assertion on one property is allowed only beside it, stating what the case is about (`no-vitest-snapshot` mechanizes it for a response, a json and a table). Each case gets **its own fixture project** (its small valid/invalid files): the fixture IS the Given, no shared `beforeAll` state, and the assertion is the whole snapshot (``expect(result.stdout).toMatch(`...`)`` + `exitCode`), volatile parts covered by tokens, written by `TEST_UPDATE=1` from an empty `toMatch('')`.
|
|
253
|
+
|
|
254
|
+
`.grep()` / `toContain` remain the **scalpel**: targeted probes, never the default mode. They are legitimate only for:
|
|
255
|
+
|
|
256
|
+
- **(a)** assertions of **absence**;
|
|
257
|
+
- **(b)** output **cut at an arbitrary instant** (`waitFor`, a long-running process);
|
|
258
|
+
- **(c)** **container-log probes**;
|
|
259
|
+
- **(d)** asserting **rule ids** in the E2E lint specs (avoiding coupling to a third-party binary's exact format);
|
|
260
|
+
- **(e)** probes into **third-party-formatted** output.
|
|
261
|
+
|
|
262
|
+
Every other use is converted to a full snapshot. A single "kitchen-sink" project + full snapshot serves as the whole-surface regression net (it churns; that is its job). The static channel cannot tell a legitimate grep from a lazy one, hence the process channel. The mechanized boundary for a result (a response, a json or a table read and never compared whole) is `no-vitest-snapshot`, and a lone HTTP-status probe `no-status-only-test`; the negative-fixture guard is `negative-golden-frozen` (wrapped forms) plus a process rule for helper-routed residue.
|
|
263
|
+
|
|
264
|
+
## Source-code architecture
|
|
265
|
+
|
|
266
|
+
The `architecture` family governs the source tree rather than the spec tree, and it splits in two.
|
|
267
|
+
|
|
268
|
+
**The layer map is the project's to declare.** This package cannot know another repository's architecture, so it ships no layer rule: a project states its layers in `jterrazz.config.ts`, under `typescript.architecture`, and `@jterrazz/typescript`'s Architecture pass holds its imports to them. What THIS repository declares, and why each sanctioned edge exists, is [Architecture](https://github.com/jterrazz/package-test/blob/main/docs/concepts/architecture.md); a consumer's own map is its own chapter's, written the same way.
|
|
269
|
+
|
|
270
|
+
**The test-file rules need no declaration.** `test-in-its-tests-folder` (a module's test sits in the `__tests__/` of its folder) and `no-module-mock` (no `vi.mock`, `__mocks__/` or data-asset imports under `src/`) hold in any repository that adopts this preset: they are part of the floor described above, not of the layer map. A module's typed fixtures are a `__tests__/<file>.fixtures.ts` beside its test, as the naming recap says.
|
|
271
|
+
|
|
272
|
+
## A message teaches
|
|
273
|
+
|
|
274
|
+
A finding is the only page of the catalogue most readers ever see, so it carries what the reader needs to act without opening another: **what was found** and where, **why** the convention holds, **the fix for this case** (its paths and names, and `typescript fix` when a fixer does it), and **where to read** the rule whole. A message that names the fault and stops sends its reader to the chapter for what the line could have said; one that states the rule and not the case sends them back to the code to find out what it saw.
|
|
275
|
+
|
|
276
|
+
The four parts are the same everywhere a rule speaks (a static rule, a checker pass, a refusal the runtime throws), and none is optional. How each channel prints them is [Linting](../reference/linting.md#what-a-finding-says)'s. The runtime throws nothing else: an error only the framework can cause (a broken invariant of its own) says so, and that the bug is not the reader's to fix.
|
|
277
|
+
|
|
278
|
+
## What stays human, and why
|
|
279
|
+
|
|
280
|
+
Every convention a machine can settle is a row with a channel. These are the
|
|
281
|
+
ones no channel can decide, listed with the reason, and with what the machine
|
|
282
|
+
takes of them anyway, so neither side is silently dropped.
|
|
283
|
+
|
|
284
|
+
| Convention | Why no channel decides it | What the machine takes |
|
|
285
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `folder-follows-assets` grouping: own domain vs sibling aspects | what the assets will become is a reading of the domain | `ground-beside-its-only-reader` flags the one-owner half |
|
|
287
|
+
| `output-asserted-as-a-golden` the legitimacy of a probe | absence, cut output and a third-party format are meanings, not shapes | `result-compared-whole` / `no-status-only-test` / `no-probe-cluster` draw the cluster boundary |
|
|
288
|
+
| `negative-golden-frozen` residue routed through a helper | inter-procedural analysis is out of an oxlint JS plugin's reach | `negative-golden-frozen`'s bounded heuristic |
|
|
289
|
+
| the truth of a Given/Then sentence, or of a title | a sentence can be well-formed and false | `given-and-then-markers-present`, `when-between-given-and-then`, `marker-on-one-line` and `vitest/valid-title` hold the shape |
|
|
290
|
+
| whether an updated golden is RIGHT | correctness is the diff, read at review | `no-volatile-literal` holds the volatile literals, in a document and under `__expected__/` alike; the pinned-value half of ground waits for `no-unseeded-value-in-golden` |
|
|
291
|
+
| whether an allow-listed `vi.mock` has no seam | the claim is disputable, and recorded in the entry's `reason` | `no-module-mock` holds the list, and an entry with no reason clears nothing |
|
|
292
|
+
| `exact` versus `within` when both disambiguate | taste between two user-facing forms | `test-id-states-what-is-missing` forces a reason on the escape hatch; `target-matches-one-element` offers both |
|
|
293
|
+
| `expected-named-for-its-content`: an expected file is named for what it HOLDS, not for the parameters that produced it: `limit-one.http` becomes `single-quiz-page.http` | what a response holds is a reading of its body; a name built from the request is still a well-formed name | none |
|
|
294
|
+
| one fixture per answer of the system | a reading of what the chain proves | `document-description-unique` holds unique descriptions |
|
|
295
|
+
| stub realism | only a recorded exchange could judge it | none |
|
|
296
|
+
| whether a module test earns its place: a pure algorithm, or a failure no end-to-end path provokes | it is a reading of the product and of every path its specs drive | the `expectations` rules hold the oracle; a surviving mutant names the weak test |
|
|
297
|
+
| the depth a repository declares | reviewed once, when the tree is born | `specs-at-declared-depth` thereafter |
|
|
298
|
+
|
|
299
|
+
## The backlog: the criterion written, coded on its first occurrence
|
|
300
|
+
|
|
301
|
+
A rule is written when its defect class has been SEEN once and its criterion is decidable. A class nobody has seen yet is not a rule: it is a line here, with the criterion it would be written against, so the day it appears the guard is a translation rather than a design. A dry run of the whole catalogue over every repository its author maintains is what moves a line either way.
|
|
302
|
+
|
|
303
|
+
| Line | The criterion, ready to code |
|
|
304
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
305
|
+
| `no-intercept-in-a-loop` | an `.intercept(` call inside a `for`/`while` body, or a `.map(`/`forEach(` callback, in a test file |
|
|
306
|
+
| `no-marker-inside-a-declaration` | a Given/When/Then marker whose offset falls inside a `VariableDeclaration` with ≥ 2 declarators |
|
|
307
|
+
| `no-domain-repeated-in-golden-path` | a `toMatch(file(` path whose directory part repeats the leaf's own domain folder |
|
|
308
|
+
| `no-ground-under-a-screen-specification` | a `__seeds__/` or `__requests__/` under a specification whose runner constructs `website()` or `mobile()`; a screen owns no database |
|
|
309
|
+
| `no-unseeded-value-in-golden` | a uuid, or an iso8601 whose time is NOT midnight UTC, in a file under `__expected__/` that appears in no `__seeds__/`, `__requests__/`, `__fixtures__/`, `stubs/` or referring test of the same leaf, nor in the `specs/__fixtures__/` pool. The hour is part of the criterion: a clock read during a run is never exactly midnight, a publication date always is ([ADR-006](https://github.com/jterrazz/package-test/blob/main/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md) records what the dry run found) |
|
|
310
|
+
| `no-golden-in-module-test` | a `toMatch(file(...))` in a `module`-role file OUTSIDE every `specs/` tree: a unit test standing on a golden file is an integration spec, and `specs/integration/` is where it belongs (`golden-path-has-extension` holds the same call inside a specs tree) |
|
|
311
|
+
| `no-unreachable-only-negative` | a test whose stub list is `http.unreachable()` on every feed and whose assertions name `result.error` alone: the case proves the call FAILED, not that it never happened. The fix it teaches is [`http.never(request)`](../reference/stubs.md#never-a-call-that-must-not-happen) |
|
|
312
|
+
| `no-empty-golden` | a file under `__expected__/` whose trimmed content is exactly `{{any}}` |
|
|
313
|
+
| `no-duplicate-stub-payload` | two response payloads with the same content hash under one `stubs/` tree, outside the shared pool |
|
|
314
|
+
| `no-inline-body-beside-requests` | a request body written inline in a test that also carries a `__requests__/` directory |
|
|
315
|
+
| `no-retry` | a `retry:` option on `test(`/`describe(`, or `test.retry(`, or `retry` in a vitest config (react-query's `retry: false` is not one) |
|
|
316
|
+
| `no-namespace-spy` | a `vi.spyOn(<namespace import>, ...)`: a module's own export spied through its namespace object |
|
|
317
|
+
|
|
318
|
+
**Parked**, for a criterion that is not settled: **`no-negated-golden`** (15 hits across those repositories, every one a `toMatch(/re/)` the criterion must exclude) and **`no-unreachable-stub`** (the criterion has to compare filter arguments, not just methods and urls).
|
|
319
|
+
|
|
320
|
+
## Changing the conventions
|
|
321
|
+
|
|
322
|
+
- A rule a machine can check is added to the code (its row in `lint/consumer/catalogue/manifest.ts` beside its implementation), and `npm run docs` regenerates the catalogue. It is never written here: the code is its one source.
|
|
323
|
+
- A principle, or a criterion no machine can settle, is added here, as a process rule or in [What stays human](#what-stays-human-and-why).
|
|
324
|
+
- A defect class seen once and decidable becomes a rule; one nobody has seen yet is a line of [the backlog](#the-backlog-the-criterion-written-coded-on-its-first-occurrence).
|
|
325
|
+
|
|
326
|
+
## Pitfalls
|
|
327
|
+
|
|
328
|
+
- **Treating these docs as the spec.** They explain and illustrate; this constitution and the generated catalogue ([Linting](../reference/linting.md)) decide. When the docs and the catalogue disagree, the docs are fixed in the same change.
|
|
329
|
+
- **Editing the generated catalogue by hand.** The [Linting](../reference/linting.md) catalogue is GENERATED from `lint/consumer/catalogue/manifest.ts`; a hand edit is overwritten by the next `npm run docs` and fails the freshness meta-test. Change the rule's text in the code; regenerate.
|
|
330
|
+
|
|
331
|
+
## Related
|
|
332
|
+
|
|
333
|
+
[Getting started](../start/getting-started.md) · [Time and doubles](../reference/time-and-doubles.md) · [Assertions](../reference/assertions.md) · [Linting](../reference/linting.md)
|