@jterrazz/test 16.1.1 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +1 -2980
- package/dist/browser/services.js +1 -0
- package/dist/browser/stubs.js +1 -0
- package/dist/browser-shared-index.browser.js +3233 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +3721 -2624
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +12 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1641 -3004
- package/dist/index.js +5095 -3835
- package/dist/mobile-session.global-setup.d.ts +15 -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 +3979 -5799
- package/dist/route-coverage.global-setup.d.ts +43 -0
- package/dist/route-coverage.global-setup.js +1 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +1 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +34 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +73 -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.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-listened-port.js +19 -0
- package/dist/shared-literate-plugin.js +184 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +22 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +328 -0
- package/dist/shared-process-alive.js +13 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-record-key.js +13 -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 +407 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +174 -0
- package/dist/shared-session-ledger.js +305 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +303 -0
- package/dist/shared-spec-grammar.js +66 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-specs-modules.js +1033 -0
- package/dist/shared-stub-reply.js +167 -0
- package/dist/shared-stub-server.js +142 -0
- package/dist/shared-temp-root.global-setup.js +73 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +306 -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 +1 -0
- package/dist/vitest.d.ts +93 -164
- package/dist/vitest.js +382 -550
- 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 +566 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +97 -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 +44 -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 +500 -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 +228 -0
- package/package.json +56 -36
- 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,221 @@
|
|
|
1
|
+
# The visitor
|
|
2
|
+
|
|
3
|
+
Three kinds of test drive something a person can see: a served page ([Website specs](../guides/website-specs.md)), a mounted component ([Component specs](../guides/component-specs.md)) and a screen on a simulator ([Mobile specs](../guides/mobile-specs.md)). Each hands its scenario a **visitor**, which acts on the surface the way a person would:
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
await visitor.fill(field('Email'), 'visitor@site.test');
|
|
7
|
+
await visitor.click(button('Subscribe'));
|
|
8
|
+
await visitor.see(content('Thanks for subscribing'));
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The visitor has one vocabulary on all three surfaces, and this page owns it: the **verbs** (`fill`, `click`, `see` ...), the **targets** they act on (`field()`, `button()`, `content()` ...), and the **matching** that turns a target into exactly one element. A target names what a person would name: a role and an accessible name. There is no CSS selector, no XPath and no raw predicate; the one escape hatch, `testId()`, must say in a comment what the element lacks (rule `test-id-states-what-is-missing`).
|
|
12
|
+
|
|
13
|
+
## Verbs
|
|
14
|
+
|
|
15
|
+
A scenario is the When: pure interaction, no `expect()` inside it (rule `no-expect-in-scenario`). Every verb auto-waits; there are no sleeps anywhere in the framework.
|
|
16
|
+
|
|
17
|
+
| Verb | Does |
|
|
18
|
+
| ------------------------ | ------------------------------------------------------------------ |
|
|
19
|
+
| `click(target)` | Click the element |
|
|
20
|
+
| `tap(target)` | Tap the element, the device's word for the same act |
|
|
21
|
+
| `fill(target, value)` | Fill a form field |
|
|
22
|
+
| `press(key)` | Press a key (e.g. `'Enter'`) |
|
|
23
|
+
| `select(target, option)` | Select an option in a select field |
|
|
24
|
+
| `check(target)` | Check a checkbox or radio |
|
|
25
|
+
| `hover(target)` | Hover the element |
|
|
26
|
+
| `goto(path)` | Navigate to another path of the site under test |
|
|
27
|
+
| `rerender(ui)` | What a PARENT does: new props, same mount |
|
|
28
|
+
| `unmount()` | What a parent does when it takes the thing off the screen |
|
|
29
|
+
| `see(target)` | **The synchronization primitive**: retries until visible |
|
|
30
|
+
| `gone(target)` | The absence primitive: retries until hidden or out of the document |
|
|
31
|
+
|
|
32
|
+
The website's and the component's visitors carry the same eight page verbs by construction (one declaration, `VisitorVerbs` in `src/visitor/verbs.ts`, each surface adding only its own words), and the device's visitor takes its `see` and `fill` from it. `goto()` does not exist on a component, because a component has no address. `rerender`/`unmount` exist nowhere else, because only a component has a parent inside the test. `gone()` is absent on mobile until the adapter can answer for absence honestly, which an XCUITest tree does not do yet.
|
|
33
|
+
|
|
34
|
+
### `see()` and `gone()` are the synchronization
|
|
35
|
+
|
|
36
|
+
`see()` is how a scenario waits. It resolves through render work (a deep link into a Metro cold bundle, a network round-trip, an animation, a suspended boundary), and it absorbs them all without a single sleep. `gone()` is the same primitive pointed the other way: it retries until the element is hidden or out of the document, which is how a dismissal is asserted without guessing at an animation's length.
|
|
37
|
+
|
|
38
|
+
Both take a modifier, so `see(focused(button('Open')))` waits for the keyboard to land and `gone(disabled(button('Save')))` waits for a control to become usable again.
|
|
39
|
+
|
|
40
|
+
`see()` ACTS ON NOTHING, and on the mobile specification that changes what `target-matches-one-element` asks of it: any visible match satisfies it, because an XCUITest tree legitimately exposes the same label twice (a container and its child both carry it), and a synchronization primitive refusing on that would punish honest screens. On the web and in a component, `see()` holds the same cardinality as every other verb.
|
|
41
|
+
|
|
42
|
+
### The per-specification matrix
|
|
43
|
+
|
|
44
|
+
Which surface declares which verb, target and modifier is generated from the code: [Test § The capability matrix](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-capability-matrix), its **Verb** and **Target** sections, an `n/a` for a word the specification does not declare. `within()` and `{ exact: false }` reach every surface. A blank cell is not a hole to be filled by a CSS selector; it is a boundary the adapter refuses at runtime, with a message naming it.
|
|
45
|
+
|
|
46
|
+
The ARIA landmarks are web-only: passing one to a mobile verb refuses at runtime with a message naming the boundary: an iOS screen has no ARIA regions, and inventing an equivalent would be a second vocabulary wearing the first one's words.
|
|
47
|
+
|
|
48
|
+
## Targets
|
|
49
|
+
|
|
50
|
+
The four every surface has:
|
|
51
|
+
|
|
52
|
+
| Target | Locates by |
|
|
53
|
+
| --------------- | ------------------------------------------------------------------------------------ |
|
|
54
|
+
| `button(name)` | accessible name, button role |
|
|
55
|
+
| `field(label)` | accessible name, input roles, or the label that names it |
|
|
56
|
+
| `content(text)` | any element whose text IS the text, whole (`{ exact: false }` to contain) |
|
|
57
|
+
| `testId(id)` | `data-testid` on the web, the accessibility identifier on a device: the escape hatch |
|
|
58
|
+
|
|
59
|
+
The two a browser adds, on the website and the component:
|
|
60
|
+
|
|
61
|
+
| Target | Locates by |
|
|
62
|
+
| --------------- | ----------------------------- |
|
|
63
|
+
| `link(name)` | accessible name, link role |
|
|
64
|
+
| `heading(name)` | accessible name, heading role |
|
|
65
|
+
|
|
66
|
+
Five more roles a visitor reads and acts on, each optionally named because a surface carries several of them and one of them usually carries no name at all:
|
|
67
|
+
|
|
68
|
+
| Target | Locates by |
|
|
69
|
+
| ----------------- | ------------------------------------------------------- |
|
|
70
|
+
| `dialog(name?)` | a `<dialog>` or `role="dialog"`. A closed one is absent |
|
|
71
|
+
| `status(name?)` | a live region announcing a result |
|
|
72
|
+
| `table(name?)` | a table, by its caption or accessible name |
|
|
73
|
+
| `row(name?)` | a row of a table or grid, by the text of its cells |
|
|
74
|
+
| `listitem(name?)` | an item of a list, the answer to an unnamed `<li>` |
|
|
75
|
+
|
|
76
|
+
And one role that is read where it SITS rather than where it shows:
|
|
77
|
+
|
|
78
|
+
| Target | Locates by |
|
|
79
|
+
| -------------- | ---------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `option(name)` | an option of a select or a listbox, by its label, named inside the field holding it, with `within()` |
|
|
81
|
+
|
|
82
|
+
`see(option('LinkedIn'))` asks whether the option is OFFERED, not whether it is on the screen: the options of a collapsed `<select>` are in the document and in the accessibility tree, and not one of them has a box until a visitor opens it. Which one the field is on is the `selected` modifier.
|
|
83
|
+
|
|
84
|
+
The **landmarks** are the ARIA landmark set and nothing more: a closed vocabulary keeps `within()` from becoming a second selector language. Each takes an optional accessible name, for surfaces carrying several of the same region, and `region` and `form` want one in practice, since an unnamed `<section>` or `<form>` is no landmark at all:
|
|
85
|
+
|
|
86
|
+
| Landmark | Matches |
|
|
87
|
+
| ---------------------- | ---------------------------------------- |
|
|
88
|
+
| `banner(name?)` | the page header |
|
|
89
|
+
| `navigation(name?)` | a `<nav>`; name it when several |
|
|
90
|
+
| `main(name?)` | the primary content |
|
|
91
|
+
| `complementary(name?)` | an `<aside>`, a sidebar |
|
|
92
|
+
| `contentinfo(name?)` | the page footer |
|
|
93
|
+
| `region(name?)` | a `<section>` with an accessible name |
|
|
94
|
+
| `form(name?)` | a named form landmark |
|
|
95
|
+
| `search(name?)` | a `<search>` element, or `role="search"` |
|
|
96
|
+
|
|
97
|
+
### Modifiers
|
|
98
|
+
|
|
99
|
+
Four, and none is a target of its own: each narrows a target by a STATE the vocabulary can already name the element of, and each is accepted by both `see` and `gone`.
|
|
100
|
+
|
|
101
|
+
| Modifier | Asks |
|
|
102
|
+
| -------------------------- | ---------------------------------------------------------------------------- |
|
|
103
|
+
| `focused(element)` | Where the keyboard is |
|
|
104
|
+
| `disabled(element)` | Whether the control refuses input; `enabled(element)` is the other direction |
|
|
105
|
+
| `selected(element)` | Which option the field is on |
|
|
106
|
+
| `valued(element, 'value')` | What the field holds: its live value, never its `value` attribute |
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
await visitor.press('Escape');
|
|
110
|
+
await visitor.gone(dialog('Settings'));
|
|
111
|
+
await visitor.see(focused(button('Open')));
|
|
112
|
+
await visitor.see(disabled(button('Publish')));
|
|
113
|
+
await visitor.see(selected(option('LinkedIn')));
|
|
114
|
+
await visitor.see(valued(field('Title'), 'Launch teaser'));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
FOCUS is a verb's business and never a golden's: the accessibility tree carries none, so a tree snapshot of a surface that handed the keyboard back and one that did not are byte-identical. ENABLEMENT the tree does carry (a disabled control reads `button "Publish" [disabled]`, a selected option `[selected]`), so a golden pins it for a whole outline and `see(disabled(...))` names the one control a test is about. The modifier also buys a direction a behavioural substitute cannot reach: clicking a disabled control is a timeout, not an answer.
|
|
118
|
+
|
|
119
|
+
Scope an option to its field when a surface carries several selects, and NAME the target when you do: three constructors inside a verb is one call too deep for `unicorn/max-nested-calls`, and the name reads better besides.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const channel = within(field('Channel'), selected(option('LinkedIn')));
|
|
123
|
+
await visitor.see(channel);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `within(scope, target)`
|
|
127
|
+
|
|
128
|
+
Search inside a scope, the way a person would say _"the Articles link **in the nav**"_:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
await visitor.click(within(navigation(), link('Articles')));
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Scopes compose outside-in, and any target works as one, including `testId()` when a container has no landmark role to stand on, which is every container on a device:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
await visitor.click(within(main(), within(region('Series'), link('Part 2'))));
|
|
138
|
+
await visitor.tap(within(testId('event-list'), button('Bookmark')));
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The scope is checked first, and every scope level must itself resolve to exactly one element: if the scope is the ambiguous level, the refusal names **it** rather than the target, so the fix lands on the right target.
|
|
142
|
+
|
|
143
|
+
### `testId()`: the one escape hatch
|
|
144
|
+
|
|
145
|
+
`testId(id)` reads `data-testid` on the web and the accessibility identifier on a device. It is the ONLY target that names something a user cannot perceive, and rule `test-id-states-what-is-missing` is an error on it: a `testId()` carries a `// testId: <what is missing>` comment on its own line or the one directly above.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// testId: the row has no accessible name; the cells carry the data
|
|
149
|
+
await visitor.click(within(testId('row-3'), button('Delete')));
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The comment is an INVARIANT, not a rationale: it states the property of the markup that makes the escape hatch necessary, so the day the element gains a name the comment is false and the target changes with it. A `testId` with no such line is a selector, and a selector is what this vocabulary exists to refuse.
|
|
153
|
+
|
|
154
|
+
## Matching
|
|
155
|
+
|
|
156
|
+
A verb acts on one element, and matching is how a target becomes it: the target is resolved on the surface, every scope level first, and the result must be exactly one element whose accessible name is the name the target wrote.
|
|
157
|
+
|
|
158
|
+
### Designating exactly one element
|
|
159
|
+
|
|
160
|
+
**A target must match exactly one element (rule `target-matches-one-element`).** When several match, the framework refuses the action instead of taking the first one:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
found: Ambiguous element: link("Articles") matched 2 elements on http://site.test/ambiguous:
|
|
164
|
+
1. <a href="/articles">Articles</a> in <nav>
|
|
165
|
+
2. <a href="/articles">Articles</a> in <footer>
|
|
166
|
+
why: A verb acts on exactly ONE element: a target that matches several is refused, never settled by the first.
|
|
167
|
+
fix: designate one of them:
|
|
168
|
+
• scope it within(navigation(), link("Articles")) [leaves 1 of 2] [also here: contentinfo()]
|
|
169
|
+
• other element a heading(), button() or field() may name one thing where this does not
|
|
170
|
+
read: docs/reference/linting.md#target-matches-one-element
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The refusal is a lesson in the four parts every message carries ([Linting](linting.md#what-a-finding-says)), one part per line.
|
|
174
|
+
|
|
175
|
+
Every suggestion carries what it LEAVES, so a rewrite that would keep the spec ambiguous never reads as a fix: a landmark holding every candidate is not offered at all. A candidate inside a named `<section>` is offered its region by name (`within(region("Orders"), ...)`, the name read from the section's `aria-label` or from what its `aria-labelledby` points at), since a page holds several regions; an unnamed `<section>` is no region, so it stays evidence (`in <section>`) and is never offered. The evidence also names the ACCESSIBLE name a candidate matched on when that is not its text: a role target matches the name, so `button('Delete post')` finds an `aria-label="Delete Post a"` too, and printing only the text would send you after something that never matched.
|
|
176
|
+
|
|
177
|
+
On a device the same refusal reads in the device's terms: there are no landmarks to offer, so the evidence offers the accessibility identifiers it found:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
found: Ambiguous element: button("Bookmark") matched 3 elements on the screen:
|
|
181
|
+
1. Button "Bookmark" [testId: event-1-bookmark]
|
|
182
|
+
2. Button "Bookmark" [testId: event-2-bookmark]
|
|
183
|
+
3. Button "Bookmark all"
|
|
184
|
+
why: A verb acts on exactly ONE element: a target that matches several is refused, never settled by the first.
|
|
185
|
+
fix: designate one of them:
|
|
186
|
+
• scope it within(testId('...'), button("Bookmark")): a screen has no landmarks, and any target works as the scope
|
|
187
|
+
• test id testId("event-1-bookmark") [also here: event-2-bookmark]
|
|
188
|
+
• other element a button() or field() may name one thing where this does not
|
|
189
|
+
read: docs/reference/linting.md#target-matches-one-element
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Taking "the first match" is the failure this rule exists to prevent: the spec stays green while the visitor acts on a different element, and nothing ever reports it. Ambiguity is an authoring mistake, not something DOM order should arbitrate.
|
|
193
|
+
|
|
194
|
+
### A name designates the accessible name WHOLE
|
|
195
|
+
|
|
196
|
+
`link('Articles')` does not reach "Read Articles". A substring default is the shape that lets a test pass for years against the element beside the one it named, and the refusal never comes, because there is only ever one match.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
await visitor.click(link('Articles')); // exactly "Articles"
|
|
200
|
+
await visitor.click(link('Articles', { exact: false })); // also "Read Articles"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Every named target accepts the option (`button`, `link`, `field`, `heading`, `content`, `option`, and the named landmarks) on every surface: the web compiles it to a whole-string match, the device to `==` and, opted out, to `CONTAINS`.
|
|
204
|
+
|
|
205
|
+
Reach for the opt-out only when the name genuinely carries a variable part (a count, a user's name); scope with `within()` when the problem is that the same name appears twice.
|
|
206
|
+
|
|
207
|
+
A name only part of an accessible name designates nothing, on every surface and at every level of a chain: `within(button('Proof of authorship'), content('Verified'))` against a button named "Proof of authorship Verified" fails on the SCOPE. The name to write is the ACCESSIBLE name, whole: what the ARIA tree (`result.tree`) prints for the element, never its text content: a button holding two inline children reads as `Experiments9` through its text and is named "Experiments 9", and a field's name is its label, which its text carries nothing of. On a device it is the element's label.
|
|
208
|
+
|
|
209
|
+
## Pitfalls
|
|
210
|
+
|
|
211
|
+
- **Asserting focus with a golden.** The tree carries no focus; see [§ Modifiers](#modifiers).
|
|
212
|
+
- **Expecting a name to match a prefix.** It does not: write the name whole, read off the ARIA tree, or scope it with `within(...)`.
|
|
213
|
+
- **Answering an ambiguity with `{ exact: false }`.** The option widens the match; ambiguity wants narrowing. Scope it.
|
|
214
|
+
- **Reaching for `testId()` first.** It is the last rung, and `test-id-states-what-is-missing` asks for the line saying why. A heading, a button or a field usually names the thing already.
|
|
215
|
+
- **Putting an `expect()` inside a scenario.** `no-expect-in-scenario` refuses it: a scenario is interaction, the Then is on the result.
|
|
216
|
+
- **Passing a landmark to a mobile verb.** It refuses at runtime, naming the boundary. Scope with `within(testId(...), ...)` instead.
|
|
217
|
+
- **Nesting three constructors in a verb call.** `unicorn/max-nested-calls` refuses it, and a named `const` reads better.
|
|
218
|
+
|
|
219
|
+
## Related
|
|
220
|
+
|
|
221
|
+
[Component specs](../guides/component-specs.md) · [Website specs](../guides/website-specs.md) · [Mobile specs](../guides/mobile-specs.md) · [Assertions](assertions.md) · [Tokens](tokens.md) · [Conventions](../concepts/conventions.md) · [Linting](linting.md)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
`@jterrazz/test` is a testing framework built on vitest. You describe what to test (an HTTP API, a CLI, a page, a module against a real database), and it starts what that needs, runs one action, and hands back a result you compare with `expect()`. This page covers the five ideas behind it, a first spec from install to green, and the question that picks the kind of test you write.
|
|
4
|
+
|
|
5
|
+
## Why it exists
|
|
6
|
+
|
|
7
|
+
Three things make a test suite expensive to keep, and the framework takes each one off your hands.
|
|
8
|
+
|
|
9
|
+
- **Infrastructure is code you maintain.** A suite that needs Postgres, a built binary or a browser usually grows its own `beforeAll` that starts them and an `afterAll` that sometimes forgets to stop them. Here you declare `postgres()`, `specification.cli(bin)` or a page visit, and the framework starts it, isolates it per worker and cleans it up.
|
|
10
|
+
- **Assertions drift from what the user sees.** A test that checks `status === 201` and one field passes when the body is wrong. Here a result is compared whole (the full response, the full table, the full output), written in the test or in a file next to it, with `{{tokens}}` where values move.
|
|
11
|
+
- **Every repository invents its own layout.** Here the conventions are a linter: where a test lives, what it may import and how it is named are checked by an oxlint plugin and a checker, and every finding says what it found, why the rule exists, how to fix this case, and where to read more.
|
|
12
|
+
|
|
13
|
+
## The mental model
|
|
14
|
+
|
|
15
|
+
Five ideas cover everything else in the manual. They match the four families of the docs map, plus the files a test stands on.
|
|
16
|
+
|
|
17
|
+
**1. A kind of test, and its runner.** What you test decides the kind: a module, a module against a real service (integration), a rendered component, a website, a mobile screen, an HTTP API, a job, a CLI, or the lint rules a project ships. Each kind except module and component has a constructor (`specification.api()`, `specification.cli()`, ...), called once per suite in a `*.specification.ts` file. It starts what the suite needs and returns a runner (`api`, `cli`, ...) and a `cleanup` you pass to `afterAll`. The pages call the code behind one kind a **specification**.
|
|
18
|
+
|
|
19
|
+
**2. A chain: setups, then one action.** Every test is one chain on that runner: zero or more setups (`.seed()`, `.intercept()`, `.clock()`, `.env()` ...), then exactly one action (`.get()`, `.exec()`, `.visit()`, `.trigger()`, `.call()` ...), which resolves to a result. The chain reads as the test's sentence: the setups are the Given, the action is the When.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
const result = await api.seed(file('./__seeds__/two-users.sql')).get('/users');
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**3. What surrounds the subject.** A subject rarely runs alone. Real services (`postgres()`, `redis()`, `sqlite()`, `process()`) are started for you; the network is declared with stubs (`defineStub`, `.intercept()`); time is pinned with `clock`; a port your code injects is doubled with `mockOf()`. These have an order of preference, the doubles ladder: use the real thing first, a declared service next, then a stub, and a typed double last.
|
|
26
|
+
|
|
27
|
+
**4. A visitor, for anything on a screen.** A page, a component or a native app is driven the way a person uses it: `visitor.click(button('Subscribe'))`, `visitor.see(content('Thanks'))`. Elements are found by role and accessible name, never by CSS selector.
|
|
28
|
+
|
|
29
|
+
**5. Expectations on the whole result.** A result exposes what the action produced (`result.response`, `result.stdout`, `result.table('users')`, `result.tree`), and one matcher, `toMatch`, compares it whole: against a value written in the test, or `file('./__expected__/...')` when several tests share it. Values that change between runs are written as tokens (`{{uuid}}`, `{{iso8601}}`), and `TEST_UPDATE=1` writes the expected values for you to review.
|
|
30
|
+
|
|
31
|
+
The whole-value comparison is the framework's `toMatch` on a framework result (`result.response`, `result.stdout`). On a plain string such as `result.stdout.text`, `toMatch` is vitest's own substring or RegExp check ([Assertions § Common mistakes](../reference/assertions.md#common-mistakes)).
|
|
32
|
+
|
|
33
|
+
Everything a test stands on sits in a folder next to it whose name starts with `_`: `__seeds__/` (SQL), `__requests__/` (what is sent), `__fixtures__/` (files the program reads), `__expected__/` (what is compared), `__setup__/` (helper code). The `_` marks a folder that is not a subject of the tests.
|
|
34
|
+
|
|
35
|
+
## A first spec
|
|
36
|
+
|
|
37
|
+
This walkthrough tests an HTTP API backed by SQLite, so it needs no Docker. It assumes a Hono app in `src/app.ts`, built by a `createApp({ databaseUrl })` function, and a schema in `schema.sql`.
|
|
38
|
+
|
|
39
|
+
### 1. Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -D @jterrazz/test @jterrazz/typescript vitest better-sqlite3
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`vitest` and `@jterrazz/typescript` are always required. Everything else is installed only when a test needs it: here, `better-sqlite3` for `sqlite()`. If one is missing, the error says which package and the install command. The full list is [Set up a project § What to install](set-up-a-project.md#what-to-install).
|
|
46
|
+
|
|
47
|
+
### 2. Configure vitest
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// jterrazz.config.ts
|
|
51
|
+
import { test } from '@jterrazz/test/config';
|
|
52
|
+
import { defineConfig } from '@jterrazz/typescript/config';
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
kind: 'api',
|
|
56
|
+
platform: 'node',
|
|
57
|
+
presets: [test()],
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`kind` and `platform` say what the project is (here, an HTTP service on Node), and `@jterrazz/typescript` derives the rest of the toolchain from them: the lint profile, the tsconfig preset, what the manifest must hold. An application kind (`api`, `website`, `mobile`) also asks for `"private": true` in `package.json`. The two lists, and which platforms each kind runs on, are the toolchain's [Getting started § Declare what the project is](https://github.com/jterrazz/package-typescript/blob/main/docs/start/getting-started.md#1-declare-what-the-project-is). `presets: [test()]` adds this package: the shared test defaults, the conventions, and the `test` section below.
|
|
62
|
+
|
|
63
|
+
Then `npx typescript fix` writes the files that declaration derives, `vitest.config.ts` among them, a line you never edit. The preset sets the shared defaults (timeouts, artefact paths, no retries), and the projects are read off the tree, each built by the helper of its specification: `unit` collects the `*.test.ts` files next to your modules (the project a module test runs in), and `specs/api/` makes the `api` project, which collects `specs/api/**/*.spec.ts`. [Set up a project](set-up-a-project.md#the-vitest-config) lists every helper and default.
|
|
64
|
+
|
|
65
|
+
This `api` project only names a vitest project. The `api` you send requests with is a different thing: the runner that step 3 builds.
|
|
66
|
+
|
|
67
|
+
### 3. Build the runner, once per suite
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// specs/api/api.specification.ts
|
|
71
|
+
import { specification } from '@jterrazz/test';
|
|
72
|
+
import { sqlite } from '@jterrazz/test/services';
|
|
73
|
+
import { afterAll } from 'vitest';
|
|
74
|
+
|
|
75
|
+
import { createApp } from '../../src/app.js';
|
|
76
|
+
|
|
77
|
+
export const { api, cleanup } = await specification.api({
|
|
78
|
+
services: { db: sqlite({ init: './schema.sql' }) },
|
|
79
|
+
server: ({ db }) => createApp({ databaseUrl: db.connectionString }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterAll(cleanup);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A spec imports from three entries: `@jterrazz/test` for what writes it (the constructors, `file()`, the targets, `match`), `@jterrazz/test/services` for what a specification starts (`sqlite()` here), and `@jterrazz/test/stubs` for what the network replies. A service or a stub imported from the root is a lint error, and `npx typescript fix` moves the import to its subpath (rule `service-or-stub-from-its-subpath`).
|
|
86
|
+
|
|
87
|
+
`server` returns your app, and the specification sends it each request through its `request(path, init)` method, which answers a Fetch `Response`; a Hono `app` is one as it stands. The app runs in the test process, wired to a real SQLite database built from `schema.sql`. Each worker gets its own copy, and every chain starts from empty tables.
|
|
88
|
+
|
|
89
|
+
An app on another framework needs two things. It needs a `request(path, init)` of its own: Express and Fastify have none, so `server` returns a wrapper you write that answers it with a `Response`. And it exposes no route table, so the run cannot measure route coverage until the project names its routes as an OpenAPI document, `test: { routes: './openapi.yaml' }` in `jterrazz.config.ts`; without one the api run fails ([API specs § An app that is not Hono](../guides/api-specs.md#an-app-that-is-not-hono)).
|
|
90
|
+
|
|
91
|
+
### 4. Write the test
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// specs/api/users/list/shape.spec.ts
|
|
95
|
+
import { file } from '@jterrazz/test';
|
|
96
|
+
import { describe, expect, test } from 'vitest';
|
|
97
|
+
|
|
98
|
+
import { api } from '../../api.specification.js';
|
|
99
|
+
|
|
100
|
+
describe('shape', () => {
|
|
101
|
+
test('lists the users the database holds', async () => {
|
|
102
|
+
// Given - two users, from the seed beside the test
|
|
103
|
+
const result = await api.seed(file('./__seeds__/two-users.sql')).get('/users');
|
|
104
|
+
|
|
105
|
+
// Then - the whole response
|
|
106
|
+
expect(result.response).toMatch(`
|
|
107
|
+
HTTP/1.1 200 OK
|
|
108
|
+
content-type: application/json
|
|
109
|
+
|
|
110
|
+
{"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
|
|
111
|
+
`);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```sql
|
|
117
|
+
-- specs/api/users/list/__seeds__/two-users.sql
|
|
118
|
+
INSERT INTO users (name) VALUES ('Alice'), ('Bob');
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The folder says what is tested (`users/list/` is `GET /users`), the file name says which question is asked of it (`shape`), and the one `describe` repeats it. Every test carries a `// Given -` and a `// Then -` line.
|
|
122
|
+
|
|
123
|
+
### 5. Run it
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx vitest --run --project api
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`--project api` runs one project; `npx vitest --run` runs every project the config declares, and is the line to put in `package.json` as `"test": "vitest --run"`.
|
|
130
|
+
|
|
131
|
+
The api project also checks route coverage: after the last test it names every route the app declares that no request reached, and a route left unreached fails the run, even on the first one. There is no switch to turn it off. To accept that debt for now, run once with `TEST_UPDATE=1`: it records the count in `.jterrazz/baseline.json`, which you commit, and the count can only go down from there ([API specs § Route coverage](../guides/api-specs.md#route-coverage)).
|
|
132
|
+
|
|
133
|
+
To write the expected value instead of typing it, leave it empty (`toMatch('')`) and run once with `TEST_UPDATE=1`. The framework writes what came back into the test, with tokens where values move; you read the diff before you commit it ([Tokens § Update mode](../reference/tokens.md#update-mode-tokens-are-preserved)).
|
|
134
|
+
|
|
135
|
+
### 6. Check the conventions
|
|
136
|
+
|
|
137
|
+
The conventions are already on: the `test()` step 2 lists carries them, and the `oxlint.config.ts` that `npx typescript fix` wrote reads it. One command enforces them, with the rest of the toolchain:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx typescript check
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
What it checks, and how to deviate from a rule, is [Set up a project § Turning the conventions on](set-up-a-project.md#turning-the-conventions-on).
|
|
144
|
+
|
|
145
|
+
### What you commit
|
|
146
|
+
|
|
147
|
+
| Path | Commit it? | Why |
|
|
148
|
+
| -------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| `jterrazz.config.ts` | yes | the one file you write: what the project is, its presets, its test projects |
|
|
150
|
+
| `vitest.config.ts`, `oxlint.config.ts`, `oxfmt.config.ts`, `tsconfig.json` | yes | derived by `typescript fix` and held by `typescript check`; an editor reads them by name |
|
|
151
|
+
| `.jterrazz/` | yes | the debt the ratchets hold, such as the `baseline.json` step 5 records; commit it with the change that recorded it, since the next run judges against it |
|
|
152
|
+
| `.artifacts/` | no | caches, reports and evidence every run rewrites: add `.artifacts/` to `.gitignore` |
|
|
153
|
+
|
|
154
|
+
## Which kind of test
|
|
155
|
+
|
|
156
|
+
Ask the questions in order: the first yes is the kind, and the kind decides where the file lives, which constructor it uses and which vitest project runs it.
|
|
157
|
+
|
|
158
|
+
<!-- GENERATED:fork. Do not edit by hand; run `npm run docs`. Source: lint/consumer/catalogue/fork.ts -->
|
|
159
|
+
|
|
160
|
+
| Question | Kind | Lives at | Constructor | Project | Read |
|
|
161
|
+
| ---------------------------------------------------------------------- | ---------------- | ------------------------------------------------ | ----------------------------- | ------------- | -------------------------------------------------------------------------------------------- |
|
|
162
|
+
| Does it render in a browser as a whole served page? | website | `specs/website/<domain>/<aspect>.spec.ts` | `specification.website()` | `website` | [Website specs](../guides/website-specs.md) |
|
|
163
|
+
| Does it render as a component (React, a DOM function, a hook)? | component | `__tests__/<file>.test.tsx` by `<file>.tsx` | none, the `component` chain | `component` | [Component specs](../guides/component-specs.md) |
|
|
164
|
+
| Does it render on a simulator? | mobile | `specs/mobile/<domain>/<aspect>.spec.ts` | `specification.mobile()` | `mobile` | [Mobile specs](../guides/mobile-specs.md) |
|
|
165
|
+
| Does it answer HTTP? | api | `specs/api/<resource>/<action>/<aspect>.spec.ts` | `specification.api()` | `api` | [API specs](../guides/api-specs.md) |
|
|
166
|
+
| Is it triggered by name, in-process? | job | `specs/job/<job>/<aspect>.spec.ts` | `specification.job()` | `job` | [Job specs](../guides/job-specs.md) |
|
|
167
|
+
| Is it a binary? | cli | `specs/cli/<command>/` (+ `<case>.spec.yaml`) | `specification.cli(bin)` | `cli` | [CLI specs](../guides/cli-specs.md), [Spec documents](../guides/spec-documents.md) |
|
|
168
|
+
| Is it a rule the project ships: a plugin, a checker, a preset? | lint | `specs/lint/<rule>/<aspect>.spec.ts` | `specification.lint()` | `lint` | [Lint specs](../guides/lint-specs.md) |
|
|
169
|
+
| Is it a module that needs a real service, or whose oracle is a golden? | integration | `specs/integration/<domain>/<aspect>.spec.ts` | `specification.integration()` | `integration` | [Integration specs](../guides/integration-specs.md) |
|
|
170
|
+
| Is it a module alone? | module | `__tests__/<file>.test.ts` by `<file>.ts` | none | `unit` | [Module tests](../guides/module-tests.md) |
|
|
171
|
+
| Is it the repository itself, a suite over several apps? | repository suite | `specs/<suite>/<domain>/<aspect>.test.ts` | none | any name | [Conventions § The third door](../concepts/conventions.md#the-third-door-a-repository-suite) |
|
|
172
|
+
|
|
173
|
+
<!-- /GENERATED:fork -->
|
|
174
|
+
|
|
175
|
+
The order matters: a page served over HTTP is a website, not an API, because the first question already caught it. And the table is total: a folder under `specs/` is a kind's name, a special folder of what the kinds share, or a repository suite; a probe of an adapter against a real database is an integration spec like any other.
|
|
176
|
+
|
|
177
|
+
The file suffix tells the kind apart: `.test.ts` or `.test.tsx` next to the code for a single unit, `.spec.ts` under `specs/<kind>/` for an assembled product, `<case>.spec.yaml` for a CLI session written as data, and `<kind>.specification.ts` for the file that builds a runner. The kind follows the subject, not the amount of machinery: a component needs a real browser and still sits next to its code, because it is one unit; a module that needs a real database sits under `specs/integration/`, because it is assembled with that database.
|
|
178
|
+
|
|
179
|
+
Before choosing a kind, ask whether the test should exist at all. A behaviour a user relies on is tested end to end, through the surface the user meets; a module test is kept only for a pure algorithm or a failure no end-to-end path can provoke ([Conventions § Which test earns its place](../concepts/conventions.md#which-test-earns-its-place)).
|
|
180
|
+
|
|
181
|
+
## Where to go next
|
|
182
|
+
|
|
183
|
+
- The chapter of your kind, from the table above. Each one has the same sections: what it specifies, the constructor, the chain, the result, what is unique to it, pitfalls.
|
|
184
|
+
- What surrounds a subject: [Stubs](../reference/stubs.md), [Services](../reference/services.md), [Time and doubles](../reference/time-and-doubles.md).
|
|
185
|
+
- Acting on a screen: [The visitor](../reference/visitor.md).
|
|
186
|
+
- Judging a result: [Assertions](../reference/assertions.md), [Tokens](../reference/tokens.md).
|
|
187
|
+
- Why the conventions are what they are: [Conventions](../concepts/conventions.md); every rule, generated from the code: [Linting](../reference/linting.md).
|