@jterrazz/test 16.1.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +2 -2867
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4551 -2612
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3002
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6073 -5616
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6065 -5609
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +374 -507
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -349
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -244
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -242
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -305
- package/docs/19-linting.md +0 -510
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
package/dist/stream.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
import { t as CaptureScope } from "./match.js";
|
|
2
|
-
import { A as placeholderPatternSource, D as hasPlaceholders, P as textEquals } from "./literate.js";
|
|
3
|
-
//#region src/model/contracts/contract.ts
|
|
4
|
-
/**
|
|
5
|
-
* Declare one contract. Identity function — its value is the enforced shape
|
|
6
|
-
* and the naming convention:
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* // specs/api/reports/contracts/openai/classify-article.ts
|
|
10
|
-
* import { defineContract, openai } from '@jterrazz/test';
|
|
11
|
-
*
|
|
12
|
-
* export default defineContract({
|
|
13
|
-
* request: openai.responses({ user: PROMPT, tools: ['classify'] }),
|
|
14
|
-
* response: openai.reply({ categories: ['TECH'] }),
|
|
15
|
-
* });
|
|
16
|
-
*
|
|
17
|
-
* // Dynamic — the response is computed from the observed request:
|
|
18
|
-
* export default defineContract({
|
|
19
|
-
* request: http.post('https://api.example.com/echo'),
|
|
20
|
-
* response: (request) => http.json({ received: request.body }),
|
|
21
|
-
* });
|
|
22
|
-
*/
|
|
23
|
-
function defineContract(contract) {
|
|
24
|
-
return contract;
|
|
25
|
-
}
|
|
26
|
-
/** Is this a {@link Contracts} composite (rather than a contract or a list)? */
|
|
27
|
-
function isContracts(value) {
|
|
28
|
-
return typeof value === "object" && value !== null && Array.isArray(value.contracts) && typeof value.with === "function";
|
|
29
|
-
}
|
|
30
|
-
/** Is this a single {@link Contract} (rather than a bare request half)? */
|
|
31
|
-
function isContract(value) {
|
|
32
|
-
return typeof value === "object" && value !== null && "request" in value && "response" in value;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The route a contract claims — method + the DECLARED url source. Two
|
|
36
|
-
* contracts share a route when they would be written on the same line of a
|
|
37
|
-
* declaration: same method, same url pattern (`.source` for a RegExp).
|
|
38
|
-
*/
|
|
39
|
-
function routeKeyOf(contract) {
|
|
40
|
-
const { method, url } = contract.request;
|
|
41
|
-
return url instanceof RegExp ? `${method} re:${url.source}` : `${method} str:${url}`;
|
|
42
|
-
}
|
|
43
|
-
/** Human-readable route of a contract — used in every failure message. */
|
|
44
|
-
function describeRoute(contract) {
|
|
45
|
-
const { method, url } = contract.request;
|
|
46
|
-
return `${method === "*" ? "ANY" : method} ${url instanceof RegExp ? String(url) : url}`;
|
|
47
|
-
}
|
|
48
|
-
/** Flatten contracts, lists, and composites into one ordered list. */
|
|
49
|
-
function flatten(items) {
|
|
50
|
-
const flat = [];
|
|
51
|
-
for (const item of items) if (Array.isArray(item)) flat.push(...flatten(item));
|
|
52
|
-
else if (isContracts(item)) flat.push(...item.contracts);
|
|
53
|
-
else flat.push(item);
|
|
54
|
-
return flat;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Compose contracts into the artifact a test imports — it's contracts all the
|
|
58
|
-
* way down: a composite may extend contracts, lists, and other composites,
|
|
59
|
-
* recursively, order preserved.
|
|
60
|
-
*
|
|
61
|
-
* `.with(...)` derives a scenario: contracts whose route the overrides claim
|
|
62
|
-
* are removed from the base, and the overrides are prepended. Under
|
|
63
|
-
* first-match selection a more specific override (`/articles/gone-1`) also
|
|
64
|
-
* wins over a generic base route (`/articles/{{uuid}}`) it does not replace.
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* // contracts/newsroom.contracts.ts
|
|
68
|
-
* export default defineContracts(events, articleById);
|
|
69
|
-
* export const withArticleGone = (id: string) =>
|
|
70
|
-
* newsroom.with(articleGone(id));
|
|
71
|
-
*/
|
|
72
|
-
function defineContracts(...items) {
|
|
73
|
-
const contracts = Object.freeze(flatten(items));
|
|
74
|
-
return {
|
|
75
|
-
contracts,
|
|
76
|
-
with(...overrides) {
|
|
77
|
-
const added = flatten(overrides);
|
|
78
|
-
const claimed = new Set(added.map(routeKeyOf));
|
|
79
|
-
const kept = contracts.filter((contract) => !claimed.has(routeKeyOf(contract)));
|
|
80
|
-
return defineContracts([...added, ...kept]);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/** Normalize any accepted input into a flat contract list. */
|
|
85
|
-
function contractsOf(input) {
|
|
86
|
-
return flatten([input]);
|
|
87
|
-
}
|
|
88
|
-
//#endregion
|
|
89
|
-
//#region src/model/contracts/types.ts
|
|
90
|
-
/**
|
|
91
|
-
* The marker that tells a STREAMED body from a value that happens to be an
|
|
92
|
-
* object. A body reaches three different engines (msw's server, msw's worker,
|
|
93
|
-
* the `node:http` stub) and each of them serialises "an object" as JSON, so a
|
|
94
|
-
* stream declared as a plain value arrives as `{}`. The tag is what each
|
|
95
|
-
* engine recognises before it reaches for `JSON.stringify`.
|
|
96
|
-
*/
|
|
97
|
-
const STREAM_BODY = "@jterrazz/test:stream";
|
|
98
|
-
/** Is this body a declared stream rather than a value to serialise? */
|
|
99
|
-
function isStreamBody(body) {
|
|
100
|
-
return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
|
|
101
|
-
}
|
|
102
|
-
//#endregion
|
|
103
|
-
//#region src/model/timing/pause.ts
|
|
104
|
-
/**
|
|
105
|
-
* One answer to "wait" — the only `setTimeout` the model owns.
|
|
106
|
-
*
|
|
107
|
-
* Two things in the framework genuinely have to wait: the spacing between the
|
|
108
|
-
* pieces of a declared stream, and the poll behind `waitUntil`. Everything
|
|
109
|
-
* else that waits is a spec doing it by hand, which is what J2 refuses. Stated
|
|
110
|
-
* once so there is one place to look when time stops behaving.
|
|
111
|
-
*/
|
|
112
|
-
async function pause(ms) {
|
|
113
|
-
await new Promise((resolve) => {
|
|
114
|
-
setTimeout(resolve, ms);
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/model/contracts/queue.ts
|
|
119
|
-
/**
|
|
120
|
-
* The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
|
|
121
|
-
* Both engines consume it: the MSW integration on api/jobs chains, and the
|
|
122
|
-
* declared stub backend on website/mobile chains.
|
|
123
|
-
*
|
|
124
|
-
* Selection: the FIRST contract in declaration order that matches the observed
|
|
125
|
-
* request and is not exhausted. A contract with no `times` is unlimited (a
|
|
126
|
-
* re-render or a retry replays it); `times: n` is spent after n serves, so an
|
|
127
|
-
* ordered sequence is written as finite contracts before an unlimited tail:
|
|
128
|
-
* `[error500 ×3, ok]`.
|
|
129
|
-
*
|
|
130
|
-
* Strictness (CONVENTIONS D7): when nothing matches — including "everything
|
|
131
|
-
* that matches is exhausted" — the engine answers 501, records the request,
|
|
132
|
-
* and the chain fails with {@link ContractQueue.unmatchedError}. A chain that
|
|
133
|
-
* declared zero contracts is not guarded at all (unchanged boundary).
|
|
134
|
-
*/
|
|
135
|
-
/** Base used to parse origin-relative observed URLs (`/events?x=1`). */
|
|
136
|
-
const RELATIVE_BASE = "http://contract.invalid";
|
|
137
|
-
/** Split a declared url into its base (origin+path, or path) and query halves.
|
|
138
|
-
* Never through `new URL()`, which would percent-encode `{{token}}` braces. */
|
|
139
|
-
function splitDeclared(url) {
|
|
140
|
-
const separator = url.indexOf("?");
|
|
141
|
-
if (separator === -1) return {
|
|
142
|
-
base: url,
|
|
143
|
-
query: new URLSearchParams()
|
|
144
|
-
};
|
|
145
|
-
return {
|
|
146
|
-
base: url.slice(0, separator),
|
|
147
|
-
query: new URLSearchParams(url.slice(separator + 1))
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
/** Decode percent-escapes, falling back to the raw text on malformed input. */
|
|
151
|
-
function safeDecode(text) {
|
|
152
|
-
try {
|
|
153
|
-
return decodeURIComponent(text);
|
|
154
|
-
} catch {
|
|
155
|
-
return text;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/** Token-aware comparison of one declared value against an observed one. */
|
|
159
|
-
function valueMatches(declared, observed) {
|
|
160
|
-
return textEquals(declared, observed, new CaptureScope());
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Does an observed URL satisfy a declared one? A RegExp tests the full URL; a
|
|
164
|
-
* PATH FORM (`/articles/{{uuid}}`) ignores the origin; an absolute string
|
|
165
|
-
* compares origin+pathname. In both string forms `{{token}}` segments compare
|
|
166
|
-
* structurally and declared query params are a SUBSET of the observed ones.
|
|
167
|
-
*/
|
|
168
|
-
function urlMatches(declared, observed) {
|
|
169
|
-
if (declared instanceof RegExp) return declared.test(observed);
|
|
170
|
-
let url;
|
|
171
|
-
try {
|
|
172
|
-
url = new URL(observed, RELATIVE_BASE);
|
|
173
|
-
} catch {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
const { base, query } = splitDeclared(declared);
|
|
177
|
-
const observedBase = base.startsWith("/") ? url.pathname : `${url.origin}${url.pathname}`;
|
|
178
|
-
const decodedBase = base.startsWith("/") ? safeDecode(url.pathname) : `${url.origin}${safeDecode(url.pathname)}`;
|
|
179
|
-
if (!valueMatches(base, observedBase) && !valueMatches(base, decodedBase)) return false;
|
|
180
|
-
for (const key of new Set(query.keys())) {
|
|
181
|
-
const observedValues = url.searchParams.getAll(key);
|
|
182
|
-
if (!query.getAll(key).every((value) => observedValues.some((actual) => valueMatches(value, actual)))) return false;
|
|
183
|
-
}
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Does an observed request satisfy a contract? Method (`*` matches any), URL,
|
|
188
|
-
* then the contract's own `match` predicate (headers/query/body filters).
|
|
189
|
-
*/
|
|
190
|
-
function contractMatches(contract, request) {
|
|
191
|
-
const { match, method, url } = contract.request;
|
|
192
|
-
if (method !== "*" && method.toUpperCase() !== request.method.toUpperCase()) return false;
|
|
193
|
-
if (!urlMatches(url, request.url)) return false;
|
|
194
|
-
return match ? match(request) : true;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* The URL pattern an out-of-process router (MSW) must register so the request
|
|
198
|
-
* REACHES the queue. Deliberately no narrower than {@link contractMatches} —
|
|
199
|
-
* the predicate above stays the authority on what actually matches.
|
|
200
|
-
*/
|
|
201
|
-
function routePatternOf(declared) {
|
|
202
|
-
if (declared instanceof RegExp) return declared;
|
|
203
|
-
const { base } = splitDeclared(declared);
|
|
204
|
-
if (base.startsWith("/")) return new RegExp(String.raw`^https?:\/\/[^/?#]+${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
|
|
205
|
-
if (hasPlaceholders(base)) return new RegExp(String.raw`^${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
|
|
206
|
-
return base;
|
|
207
|
-
}
|
|
208
|
-
/** How a declared contract reads in a failure message: spent, served, or silent. */
|
|
209
|
-
function stateOf({ contract, served }) {
|
|
210
|
-
const { times } = contract;
|
|
211
|
-
if (times !== void 0 && served >= times) return ` (exhausted after ${times})`;
|
|
212
|
-
return served === 0 ? "" : ` (served ${served} time(s))`;
|
|
213
|
-
}
|
|
214
|
-
var ContractQueue = class {
|
|
215
|
-
entries;
|
|
216
|
-
constructor(contracts) {
|
|
217
|
-
this.entries = contracts.map((contract) => ({
|
|
218
|
-
contract,
|
|
219
|
-
served: 0
|
|
220
|
-
}));
|
|
221
|
-
}
|
|
222
|
-
/** How many contracts the chain declared. */
|
|
223
|
-
get size() {
|
|
224
|
-
return this.entries.length;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Routes to register with an out-of-process router, deduped by pattern —
|
|
228
|
-
* two contracts on the same URL share one handler and one queue.
|
|
229
|
-
*/
|
|
230
|
-
get routes() {
|
|
231
|
-
const routes = /* @__PURE__ */ new Map();
|
|
232
|
-
for (const { contract } of this.entries) {
|
|
233
|
-
const url = routePatternOf(contract.request.url);
|
|
234
|
-
const key = String(url);
|
|
235
|
-
const route = routes.get(key) ?? {
|
|
236
|
-
methods: [],
|
|
237
|
-
url
|
|
238
|
-
};
|
|
239
|
-
if (!route.methods.includes(contract.request.method)) route.methods.push(contract.request.method);
|
|
240
|
-
routes.set(key, route);
|
|
241
|
-
}
|
|
242
|
-
return [...routes.values()];
|
|
243
|
-
}
|
|
244
|
-
/** The declared routes, in order — the enumeration a 501 body carries. */
|
|
245
|
-
declaredRoutes() {
|
|
246
|
-
return this.entries.map(({ contract }) => describeRoute(contract));
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Serve the first contract in declaration order that matches and is not
|
|
250
|
-
* exhausted, or null when nothing matches (including "everything that
|
|
251
|
-
* matches is spent") — the strict-violation signal.
|
|
252
|
-
*/
|
|
253
|
-
take(request) {
|
|
254
|
-
for (const entry of this.entries) {
|
|
255
|
-
const { times } = entry.contract;
|
|
256
|
-
if (times !== void 0 && entry.served >= times) continue;
|
|
257
|
-
if (!contractMatches(entry.contract, request)) continue;
|
|
258
|
-
entry.served += 1;
|
|
259
|
-
return entry.contract;
|
|
260
|
-
}
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* The chain-end failure for contracts declared `required: true` that were
|
|
265
|
-
* never requested (or not exactly `times` times), or null when every
|
|
266
|
-
* requirement held.
|
|
267
|
-
*/
|
|
268
|
-
requiredError() {
|
|
269
|
-
const unmet = [];
|
|
270
|
-
for (const { contract, served } of this.entries) {
|
|
271
|
-
if (!contract.required) continue;
|
|
272
|
-
const { times } = contract;
|
|
273
|
-
if (times === void 0) {
|
|
274
|
-
if (served === 0) unmet.push(` - ${describeRoute(contract)} — declared and required but never requested`);
|
|
275
|
-
} else if (served !== times) unmet.push(` - ${describeRoute(contract)} — declared required with times: ${times} but was requested ${served} time(s)`);
|
|
276
|
-
}
|
|
277
|
-
if (unmet.length === 0) return null;
|
|
278
|
-
return /* @__PURE__ */ new Error(`Required contract(s) never satisfied during the chain:\n${unmet.join("\n")}\nA required contract states the call MUST happen — either the code no longer makes it, or the contract no longer describes it.`);
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* The strict failure for a request that matched no contract (CONVENTIONS
|
|
282
|
-
* D7): method + URL of the offending request, plus every declared contract
|
|
283
|
-
* and how often it was served.
|
|
284
|
-
*/
|
|
285
|
-
unmatchedError(method, url) {
|
|
286
|
-
const declared = this.entries.length === 0 ? " (no contracts declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.contract)}${stateOf(entry)}`).join("\n");
|
|
287
|
-
return /* @__PURE__ */ new Error(`Unmatched outgoing HTTP request during spec: ${method} ${url}\nDeclared contracts:\n${declared}\nEvery outgoing request of a chain that declares contracts must match one — add a contract for it (or raise its \`times\` when it is exhausted).`);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
//#endregion
|
|
291
|
-
//#region src/model/contracts/stream.ts
|
|
292
|
-
/**
|
|
293
|
-
* A declared stream, made real — ONE serialisation for all three engines.
|
|
294
|
-
*
|
|
295
|
-
* msw's server and msw's worker take a `ReadableStream` body verbatim
|
|
296
|
-
* (https://mswjs.io/docs/http/mocking-responses/streaming) and the `node:http`
|
|
297
|
-
* stub pipes one into its response, so what "arriving in pieces" MEANS —
|
|
298
|
-
* the order, the spacing, the close after the last chunk — is stated here and
|
|
299
|
-
* nowhere else. A second copy of it would be a second answer to "what does the
|
|
300
|
-
* subject read", and the facets would drift.
|
|
301
|
-
*/
|
|
302
|
-
/**
|
|
303
|
-
* The pieces of `body`, in order, `delayBetweenChunks` milliseconds apart,
|
|
304
|
-
* closing after the last one.
|
|
305
|
-
*/
|
|
306
|
-
function toReadableStream(body) {
|
|
307
|
-
const encoder = new TextEncoder();
|
|
308
|
-
let index = 0;
|
|
309
|
-
return new ReadableStream({ async pull(controller) {
|
|
310
|
-
const chunk = body.chunks[index];
|
|
311
|
-
if (chunk === void 0) {
|
|
312
|
-
controller.close();
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
|
|
316
|
-
index += 1;
|
|
317
|
-
controller.enqueue(encoder.encode(chunk));
|
|
318
|
-
} });
|
|
319
|
-
}
|
|
320
|
-
//#endregion
|
|
321
|
-
export { isStreamBody as a, defineContracts as c, STREAM_BODY as i, isContract as l, ContractQueue as n, contractsOf as o, pause as r, defineContract as s, toReadableStream as t, isContracts as u };
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
//#region src/model/elements/ambiguity.ts
|
|
2
|
-
/**
|
|
3
|
-
* The ambiguity refusal — CONVENTIONS W3.
|
|
4
|
-
*
|
|
5
|
-
* A visit descriptor must designate exactly one element. Acting on "the
|
|
6
|
-
* first match" is the failure mode this module exists to prevent: the spec
|
|
7
|
-
* keeps passing while the visitor clicks something else, and nothing ever
|
|
8
|
-
* reports it. So when a descriptor matches several elements the framework
|
|
9
|
-
* refuses, and the refusal has to carry everything needed to fix it without
|
|
10
|
-
* opening a browser — the descriptor in the caller's own vocabulary, every
|
|
11
|
-
* candidate, and the concrete rewrites that would resolve it.
|
|
12
|
-
*
|
|
13
|
-
* Pure string building: it takes captured data and returns a message, so the
|
|
14
|
-
* wording is unit-testable and the browser integration stays a thin adapter.
|
|
15
|
-
*/
|
|
16
|
-
/** `kind` → the constructor that builds it, so errors echo the caller's source. */
|
|
17
|
-
const CONSTRUCTORS = {
|
|
18
|
-
banner: "banner",
|
|
19
|
-
button: "button",
|
|
20
|
-
complementary: "complementary",
|
|
21
|
-
contentinfo: "contentinfo",
|
|
22
|
-
dialog: "dialog",
|
|
23
|
-
field: "field",
|
|
24
|
-
form: "form",
|
|
25
|
-
heading: "heading",
|
|
26
|
-
link: "link",
|
|
27
|
-
listitem: "listitem",
|
|
28
|
-
main: "main",
|
|
29
|
-
navigation: "navigation",
|
|
30
|
-
option: "option",
|
|
31
|
-
region: "region",
|
|
32
|
-
row: "row",
|
|
33
|
-
search: "search",
|
|
34
|
-
status: "status",
|
|
35
|
-
table: "table",
|
|
36
|
-
testId: "testId",
|
|
37
|
-
text: "content"
|
|
38
|
-
};
|
|
39
|
-
/** The landmark a context string maps back to, for a copy-pasteable suggestion. */
|
|
40
|
-
const CONTEXT_LANDMARKS = {
|
|
41
|
-
aside: "complementary()",
|
|
42
|
-
banner: "banner()",
|
|
43
|
-
complementary: "complementary()",
|
|
44
|
-
contentinfo: "contentinfo()",
|
|
45
|
-
footer: "contentinfo()",
|
|
46
|
-
form: "form()",
|
|
47
|
-
header: "banner()",
|
|
48
|
-
main: "main()",
|
|
49
|
-
nav: "navigation()",
|
|
50
|
-
navigation: "navigation()",
|
|
51
|
-
search: "search()"
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Render a descriptor as the source that would build it — `link('Articles')`,
|
|
55
|
-
* `within(navigation(), link('Articles', { exact: false }))`. The error speaks
|
|
56
|
-
* the vocabulary the author wrote, never playwright's — so the option appears
|
|
57
|
-
* only when the AUTHOR stated one, whichever way.
|
|
58
|
-
*/
|
|
59
|
-
function formatElement(element) {
|
|
60
|
-
const bare = formatBare(element);
|
|
61
|
-
return element.scope ? `within(${formatElement(element.scope)}, ${bare})` : bare;
|
|
62
|
-
}
|
|
63
|
-
function formatBare(element) {
|
|
64
|
-
const args = [];
|
|
65
|
-
if (element.name !== void 0) args.push(JSON.stringify(element.name));
|
|
66
|
-
if (element.exact !== void 0) args.push(`{ exact: ${String(element.exact)} }`);
|
|
67
|
-
return `${CONSTRUCTORS[element.kind]}(${args.join(", ")})`;
|
|
68
|
-
}
|
|
69
|
-
/** `1. <a href="/articles">Articles</a> in <nav>` — one evidence line per candidate. */
|
|
70
|
-
function formatMatch(match, index) {
|
|
71
|
-
const attribute = match.detail ? ` ${quoteDetail(match)}` : "";
|
|
72
|
-
const named = match.accessibleName ? ` named ${JSON.stringify(match.accessibleName)}` : "";
|
|
73
|
-
const context = match.context ? ` in <${match.context}>` : "";
|
|
74
|
-
return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${named}${context}`;
|
|
75
|
-
}
|
|
76
|
-
function quoteDetail(match) {
|
|
77
|
-
return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
|
|
78
|
-
}
|
|
79
|
-
/** Each landmark the candidates sit in, and how many of them it holds. */
|
|
80
|
-
function landmarkCounts(matches) {
|
|
81
|
-
const counts = /* @__PURE__ */ new Map();
|
|
82
|
-
for (const match of matches) {
|
|
83
|
-
const landmark = match.context === void 0 ? void 0 : CONTEXT_LANDMARKS[match.context];
|
|
84
|
-
if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
|
|
85
|
-
}
|
|
86
|
-
return [...counts].toSorted(([, a], [, b]) => a - b);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* The rewrites worth offering, in the order they should be tried. Scoping is
|
|
90
|
-
* always available; `exact` only when it would actually narrow the set, and
|
|
91
|
-
* the count it would leave is stated so a still-ambiguous suggestion never
|
|
92
|
-
* reads as a fix.
|
|
93
|
-
*/
|
|
94
|
-
function formatFixes(element, matches) {
|
|
95
|
-
const fixes = [];
|
|
96
|
-
const narrowing = landmarkCounts(matches).filter(([, count]) => count < matches.length);
|
|
97
|
-
if (!element.scope) {
|
|
98
|
-
const [first, ...rest] = narrowing;
|
|
99
|
-
const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
|
|
100
|
-
fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${formatBare(element)})` : `scope it within(${first[0]}, ${formatBare(element)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
|
|
101
|
-
}
|
|
102
|
-
if (element.exact === false && element.name !== void 0) {
|
|
103
|
-
const remaining = matches.filter((match) => match.text === element.name).length;
|
|
104
|
-
if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatBare({
|
|
105
|
-
...element,
|
|
106
|
-
exact: true
|
|
107
|
-
})} [leaves ${remaining} of ${matches.length}]`);
|
|
108
|
-
}
|
|
109
|
-
fixes.push("other element a heading(), button() or field() may name one thing where this does not");
|
|
110
|
-
return fixes;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* The W3 refusal. A distinct class so the visit wrapper can recognize an
|
|
114
|
-
* already-complete message and let it through instead of nesting it inside
|
|
115
|
-
* `visit scenario failed: …`, which would print the whole thing twice.
|
|
116
|
-
*/
|
|
117
|
-
var AmbiguousElementError = class extends Error {
|
|
118
|
-
name = "AmbiguousElementError";
|
|
119
|
-
};
|
|
120
|
-
/**
|
|
121
|
-
* Build the refusal thrown when a descriptor matches more than one element.
|
|
122
|
-
*
|
|
123
|
-
* @param options.element The descriptor as the caller wrote it.
|
|
124
|
-
* @param options.matches Every candidate, in DOM order (already truncated).
|
|
125
|
-
* @param options.url The page the visitor was on when it happened.
|
|
126
|
-
*/
|
|
127
|
-
function describeAmbiguity(options) {
|
|
128
|
-
const { element, matches, url } = options;
|
|
129
|
-
const fixes = formatFixes(element, matches).map((fix) => ` • ${fix}`);
|
|
130
|
-
return [
|
|
131
|
-
`Ambiguous element: ${formatElement(element)} matched ${matches.length} elements on ${url}.`,
|
|
132
|
-
"",
|
|
133
|
-
"A spec must designate exactly one element. Acting on the first match would let",
|
|
134
|
-
"this test keep passing while the visitor interacts with something else.",
|
|
135
|
-
"",
|
|
136
|
-
"Matched:",
|
|
137
|
-
...matches.map((match, index) => formatMatch(match, index)),
|
|
138
|
-
"",
|
|
139
|
-
"Disambiguate with one of:",
|
|
140
|
-
...fixes,
|
|
141
|
-
"",
|
|
142
|
-
"Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
|
|
143
|
-
].join("\n");
|
|
144
|
-
}
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region src/model/elements/substring-warning.ts
|
|
147
|
-
/**
|
|
148
|
-
* The transitional warning for a name that matches only as a SUBSTRING.
|
|
149
|
-
*
|
|
150
|
-
* Until 16.0 a descriptor's name matched any element whose accessible name
|
|
151
|
-
* CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
|
|
152
|
-
* test could pass for years against the wrong element without anyone noticing,
|
|
153
|
-
* because the wrong element was usually next to the right one. Exact is the
|
|
154
|
-
* default now.
|
|
155
|
-
*
|
|
156
|
-
* A lint rule cannot carry this: whether a name matched whole or in part is a
|
|
157
|
-
* fact about a RUN, and a verdict fed by a previous run's artefact is not
|
|
158
|
-
* deterministic. So it is a runtime warning, printed by the adapter that did
|
|
159
|
-
* the looking, at the moment it has both answers in hand — nothing found under
|
|
160
|
-
* exact, something found under substring.
|
|
161
|
-
*
|
|
162
|
-
* It is printed ONCE per descriptor per process: a scenario that clicks the
|
|
163
|
-
* same link in ten tests has one thing to fix, not ten lines to read.
|
|
164
|
-
*/
|
|
165
|
-
/** How long the window lasts, stated in the message itself. */
|
|
166
|
-
const WINDOW = "through 16.x";
|
|
167
|
-
/** Descriptors already reported in this process. */
|
|
168
|
-
const reported = /* @__PURE__ */ new Set();
|
|
169
|
-
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
170
|
-
function identityOf(element) {
|
|
171
|
-
const own = `${element.kind}:${element.name ?? ""}`;
|
|
172
|
-
return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Report that `element` designated nothing as a whole name, but would have
|
|
176
|
-
* designated something as a substring. Returns whether a line was printed,
|
|
177
|
-
* which is what the adapters' own tests assert on.
|
|
178
|
-
*/
|
|
179
|
-
function warnSubstringOnly(element, where, found) {
|
|
180
|
-
const identity = identityOf(element);
|
|
181
|
-
if (reported.has(identity)) return false;
|
|
182
|
-
reported.add(identity);
|
|
183
|
-
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
184
|
-
console.warn(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
189
|
-
* would change.
|
|
190
|
-
*
|
|
191
|
-
* Every level of the chain is asked the same question the target is asked —
|
|
192
|
-
* `within(button('Proof of authorship'), content('Verified'))` against a
|
|
193
|
-
* button named "Proof of authorship Verified" failed on the SCOPE, and a
|
|
194
|
-
* warning that only ever widened the target printed nothing at all. The chain
|
|
195
|
-
* is walked outside-in and rebuilt as it goes, since an inner level's answer
|
|
196
|
-
* is meaningless under a scope that designates nothing.
|
|
197
|
-
*
|
|
198
|
-
* A level that states an `exact` of its own is left alone: an author who wrote
|
|
199
|
-
* `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
|
|
200
|
-
* was already exact.
|
|
201
|
-
*
|
|
202
|
-
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
203
|
-
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
204
|
-
* never runs — the test is already over. The probes are counts, which wait for
|
|
205
|
-
* nothing, and the common case (the name matches whole) costs one per level.
|
|
206
|
-
*/
|
|
207
|
-
async function widenedForWindow(element, where, probe) {
|
|
208
|
-
const chain = [];
|
|
209
|
-
for (let level = element; level; level = level.scope) chain.unshift(level);
|
|
210
|
-
let rebuilt;
|
|
211
|
-
let widened = false;
|
|
212
|
-
for (const level of chain) {
|
|
213
|
-
const candidate = {
|
|
214
|
-
...level,
|
|
215
|
-
...rebuilt === void 0 ? {} : { scope: rebuilt }
|
|
216
|
-
};
|
|
217
|
-
const loose = await widenedLevel(candidate, where, probe);
|
|
218
|
-
widened ||= loose !== void 0;
|
|
219
|
-
rebuilt = loose ?? candidate;
|
|
220
|
-
}
|
|
221
|
-
return widened ? rebuilt : void 0;
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* The one level, widened and warned about, or `undefined` when it designates
|
|
225
|
-
* something already, states its own `exact`, carries no name, or would
|
|
226
|
-
* designate nothing either way.
|
|
227
|
-
*/
|
|
228
|
-
async function widenedLevel(candidate, where, probe) {
|
|
229
|
-
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
230
|
-
try {
|
|
231
|
-
if (await probe.count(candidate) > 0) return;
|
|
232
|
-
const loose = {
|
|
233
|
-
...candidate,
|
|
234
|
-
exact: false
|
|
235
|
-
};
|
|
236
|
-
if (await probe.count(loose) !== 1) return;
|
|
237
|
-
warnSubstringOnly(candidate, where, await probe.nameOf(loose));
|
|
238
|
-
return loose;
|
|
239
|
-
} catch {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
//#endregion
|
|
244
|
-
export { formatElement as a, describeAmbiguity as i, widenedForWindow as n, AmbiguousElementError as r, warnSubstringOnly as t };
|
package/docs/01-architecture.md
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# 01 — Architecture
|
|
2
|
-
|
|
3
|
-
What this package IS: one runner model behind six constructors, one chain that needs none, five source trees with declared edges, and seven channels through which its conventions are enforced — each row of the catalogue naming exactly one. The chapters that follow explain how to USE each facet; this one draws the lines they sit inside.
|
|
4
|
-
|
|
5
|
-
| The shape | Held below |
|
|
6
|
-
| -------------------------- | -------------------------------------------------------------------------------- |
|
|
7
|
-
| The runner model | [The runner model](#the-runner-model) — constructor, handle, chain, result |
|
|
8
|
-
| The source trees | [The five trees](#the-five-trees) — `model`, `facets`, `seams`, `runner`, `lint` |
|
|
9
|
-
| How a convention is held | [The seven enforcement channels](#the-seven-enforcement-channels) |
|
|
10
|
-
| What ships out of the tree | [What the tree publishes](#what-the-tree-publishes) |
|
|
11
|
-
|
|
12
|
-
The rules themselves — what each family says, what a reviewer must judge — are the constitution, [18 — Conventions](18-conventions.md); their normative sentences are the generated catalogue, [19 — Linting](19-linting.md).
|
|
13
|
-
|
|
14
|
-
## The runner model
|
|
15
|
-
|
|
16
|
-
Every facet is the same four steps. A **constructor** takes options and returns a **handle**; the handle opens a **chain** of zero or more setups closed by exactly one terminal action; the action executes and resolves to a **typed result**; the result is asserted through vitest's `expect()`. Nothing else is public — there is no imperative escape hatch, because a spec that can do anything proves nothing in particular.
|
|
17
|
-
|
|
18
|
-
Six constructors exist and the list is closed (`src/facets/specification.ts`):
|
|
19
|
-
|
|
20
|
-
| Constructor | Handle destructures to | Subject under test |
|
|
21
|
-
| ----------------------------- | ---------------------------------- | ------------------------------------------------ |
|
|
22
|
-
| `specification.api()` | `{ api, cleanup, docker }` | An HTTP API, built and served in this process |
|
|
23
|
-
| `specification.jobs()` | `{ jobs, cleanup }` | A background pipeline, triggered by name |
|
|
24
|
-
| `specification.cli()` | `{ cli, cleanup, docker }` | A command binary in a fresh temp directory |
|
|
25
|
-
| `specification.integration()` | `{ integration, cleanup, docker }` | A module against real services, or a golden |
|
|
26
|
-
| `specification.website()` | `{ website, cleanup, url }` | A rendered page — fetched, or driven in chromium |
|
|
27
|
-
| `specification.mobile()` | `{ mobile, cleanup, udid }` | A native screen on an iOS simulator |
|
|
28
|
-
|
|
29
|
-
The asymmetry in that column is the model, not an oversight: `jobs` never spawns a container, so it is handed no `docker`; `website` and `mobile` drive a browser and a simulator rather than an orchestrated stack, so they carry neither.
|
|
30
|
-
|
|
31
|
-
**One facet has no constructor**, and the same reason explains it: a rendered component starts nothing. `component` ([07 — Component specs](07-component.md)) is a chain the package exports directly — `component.intercept(…).render(<X />)` — so there is no handle to destructure and no `cleanup` to pass to `afterAll`. What every render of a project shares (the providers, the Vite pipeline, the viewport) is the PROJECT's, stated once by `component()` in `vitest.config.ts`.
|
|
32
|
-
|
|
33
|
-
A runner is created **once per suite**, in a `*.specification.ts` file, and imported by the test files beside it. That split is what makes the container lifecycle affordable — one Postgres per suite, not one per test — and it is why `afterAll(cleanup)` belongs in the specification file and nowhere else.
|
|
34
|
-
|
|
35
|
-
### The seam under the chain
|
|
36
|
-
|
|
37
|
-
`SpecificationBuilder` (`src/model/chain/builder.ts`) holds the chain for every facet; each facet contributes its own setups and its own terminal actions on top. The infrastructure a chain needs is reached through **ports** — `src/model/ports/` declares eight of them (`browser`, `cli`, `container`, `database`, `device`, `isolation`, `server`, `service`) — and an integration implements one. So the chain knows "a database exists"; it never knows Postgres.
|
|
38
|
-
|
|
39
|
-
Every seam is opened lazily rather than imported, and each one is an optional peer loaded by the one module that owns it: `playwright` for `.visit()`, `appium`/`webdriverio` for `.open()`, `@vitest/browser-playwright` + `vitest-browser-react` + `react` for `component.render()`, and — since 16.0 — `better-sqlite3`, `pg`, `redis` and `testcontainers` for the services a record declares. A project that tests no page, no component and no database installs none of them; `src/seams/peer.ts` owns the one message a missing peer produces, which names the facet that asked, the command, and under pnpm the `onlyBuiltDependencies` line a native binding needs.
|
|
40
|
-
|
|
41
|
-
## The five trees
|
|
42
|
-
|
|
43
|
-
The source tree is five trees with declared, one-directional edges. The map is not the linter's to assume — `i1-layer-boundaries` ships inert — so this package declares its own as `FRAMEWORK_LAYERS` in `oxlint.config.ts`, and that declaration is the enforced statement of what follows.
|
|
44
|
-
|
|
45
|
-
| Tree | May import | Holds |
|
|
46
|
-
| --------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
-
| `model/` | itself, the docker/process/yaml seams, the msw seam through two named lazy imports, `runner/update`, and each facet's `*.result` module | What every facet is made of: the chain and the orchestrator, the results and their accessors, the element vocabulary, the contracts and `intercept()`, the `{{token}}` engine, the goldens, the clock, the doubles, the ports |
|
|
48
|
-
| `facets/` | `model/`, itself, the runner's project shape, and the one seam each constructor opens | Seven folders, the SAME four files in each: `<facet>.specification.ts` (the constructor), `<facet>.chain.ts`, `<facet>.result.ts`, `<facet>.project.ts` (the vitest helper), plus what is unique to the facet |
|
|
49
|
-
| `seams/` | its OWN external dependency, plus `model/` and the facet vocabulary an adapter projects into | One folder per dependency: `postgres`, `redis`, `sqlite`, `testcontainers`, `docker`, `hono`, `playwright`, `vitest-browser`, `appium`, `msw`, `openai`, `anthropic`, `yaml`, `process` |
|
|
50
|
-
| `runner/` | `vitest`, `vite`, `@vitest/browser-playwright`, `model/`, `facets/` | The CONFIG side of the runner coupling: the preset, update mode, the `literate()` plugin, the shape a node facet's project takes, and the project index |
|
|
51
|
-
| `lint/` | itself, plus a short list of PURE `model/` modules | The tool-facing channel: the oxlint plugin, the conventions checker, the catalogue manifest and generator |
|
|
52
|
-
|
|
53
|
-
**Every facet folder carries the same four files, whether or not it owns all four.** The setups and terminal actions of the six node facets are ONE builder, because a second copy of "declare a contract, seed a table, run the thing" is how two facets drift — so `api.chain.ts` and its five siblings are a named re-export of that builder rather than an implementation, and `jobs.result.ts` re-exports the shape the builder states. The shape is uniform so that a reader who has opened one facet has opened all seven; the re-export says, in the folder itself, which of the four a facet does not own.
|
|
54
|
-
|
|
55
|
-
Three of those edges carry their reason in the declaration itself. `model/` reaches `seams/docker`, `seams/process` and the yaml parser because none of the three has a dependency of its own to leak, and it reaches each facet's `*.result` module because the ONE builder constructs all seven results — a value edge, stated rather than hidden. `lint/` reaches exactly the pure modules the runner also uses — the token list, the ground names, the root walk, the `<case>.spec.yaml` parser — so that the file the lint accepts is the file the runner runs, from ONE parser. And the runner is named in exactly TWO places outside a test, one per side of it: `runner/` for the config side — the preset, the project shape, the plugin — and `seams/vitest-browser/` for the page side, where the locators, the visitor, the golden commands and the page's own `vitest` primitives live. Swapping the runner would be a rewrite of those two folders and of nothing else; `model/` states what a render IS and never which runner performs it, which is why the layer map exempts that seam from the prod-import ban (F2) by name.
|
|
56
|
-
|
|
57
|
-
The table's May-import column is a reading of `FRAMEWORK_LAYERS`, never a second statement of it: the declaration in `oxlint.config.ts` is what the linter enforces and what a change edits.
|
|
58
|
-
|
|
59
|
-
There are TWO composition roots and neither names a layer. `src/index.ts` is node's: it wires the container integrations into the registry seam and re-exports the public surface. `src/browser/index.ts` is the page's, and `src/surface.ts` is what they agree on — see [What the tree publishes](#what-the-tree-publishes).
|
|
60
|
-
|
|
61
|
-
## The seven enforcement channels
|
|
62
|
-
|
|
63
|
-
A convention this package states is held by a **channel**, and every row of the catalogue names exactly one of seven. The guiding aim is that most enforcement is programmatic, not review-borne.
|
|
64
|
-
|
|
65
|
-
- **statique** — the `jterrazz/*` oxlint plugin: one file per rule under `src/lint/rules/model/` or `src/lint/rules/<facet>/` — the tree the rule reaches, which is also the `facet` field of its manifest row — AST analysis, one diagnostic per file.
|
|
66
|
-
- **upstream** — a convention an oxlint vitest-plugin rule already holds, set as an OPTION from the `testing` fragment rather than duplicated as a rule of ours ([ADR-005](decisions/005-the-doubles-ladder-is-closed.md)).
|
|
67
|
-
- **checker** — passes of the `jterrazz-test-check` binary reading what an AST cannot: the `{{token}}` grammar of `_requests/` and `_expected/` fixtures, the `<case>.spec.yaml` document family, cross-file analyses that cross a `*.specification.ts` with its tests, and the member pass that judges a package rather than a tree.
|
|
68
|
-
- **runtime** — the framework refuses incorrect usage as it executes, where static analysis abstains (a non-literal argument) or cannot reach (the network, a container lifecycle).
|
|
69
|
-
- **type** — what the compiler refuses with no rule at all, proven by `src/type-channel.test-d.ts`.
|
|
70
|
-
- **meta** — what the framework's own suite holds about itself: the catalogue's freshness, its completeness, the reach of its rules — [03 — Testing](03-testing.md) owns the channel.
|
|
71
|
-
- **process** — the judgement no single channel settles: asset-driven grouping, golden-file discipline, retro-propagation.
|
|
72
|
-
|
|
73
|
-
The channel a rule sits on is not prose here: it is a field of `src/lint/manifest.ts`, the single source of truth from which the catalogue in [19 — Linting](19-linting.md) is generated. A rule and its normative sentence live together in the code, so the two cannot drift.
|
|
74
|
-
|
|
75
|
-
## What the tree publishes
|
|
76
|
-
|
|
77
|
-
The bundle is built by `tsdown` into `dist/` and its shape follows how each entry is consumed. `index` and `vitest`, plus the `checker`/`catalog` CLIs, are ESM-only — vitest is ESM-only and the CLIs are invoked as `node dist/*.js`. The `oxlint` plugin ships dual, because oxlint loads it from a consumer project that may itself be CommonJS.
|
|
78
|
-
|
|
79
|
-
**There are FOUR entries and no more**: `@jterrazz/test` for everything a spec uses, `@jterrazz/test/vitest` for what `vitest.config.ts` needs, `@jterrazz/test/oxlint` for the lint plugin, `@jterrazz/test/schema` for an editor validating a `<case>.spec.yaml`. That is rule F1 rather than a convention of taste, and the exemption is derived from the manifest's own `exports` map (`src/lint/package-exports.ts`), so a subpath is exempt the moment it is published and stops being exempt the moment it is withdrawn.
|
|
80
|
-
|
|
81
|
-
**The root entry has two runtimes.** A component test renders in a real browser, and the node entry cannot load there: it wires `pg`, `better-sqlite3`, `testcontainers` and `node:fs` at import. So `exports["."]` carries a `browser` condition resolving to `dist/browser/index.js` — the same public surface, built for a page, where every node-only name (the six constructors, the services, the docker accessors, the results that walk a disk) is a stub that throws where it was called, naming the facet and saying it runs under node. The split is the RUNTIME's and never the facets': one specifier, so no spec has to remember which entry it may import.
|
|
82
|
-
|
|
83
|
-
`types` is stated once, and it is the node build's — so the two runtimes are ONE type surface and a name cannot exist on one side only. What both agree on lives in `src/surface.ts`, and the `package-exports` meta-test compares the two builds' exported names on every run.
|
|
84
|
-
|
|
85
|
-
Two committed projections leave the code and land in the corpus: the API reference under `docs/reference/` (typedoc, through `typescript docs`) and the rule catalogue spliced into [19 — Linting](19-linting.md) and `skills/jterrazz-test/references/rules.md`. A third, `schema/spec.schema.json`, is generated from the document grammar's own constants. Both `docs/` and `schema/` ship in the tarball — the reference included, since the corpus links into it — so a consumer reads the manual where the messages send them, inside the install ([04 — Operating](04-operating.md#what-leaves-the-tree)). All three are regenerated by one gesture and sync-checked — see [02 — Developing](02-developing.md).
|
|
86
|
-
|
|
87
|
-
## Pitfalls
|
|
88
|
-
|
|
89
|
-
- **Reaching for a dependency from `model/`.** A new external package belongs in a folder of `seams/` that imports it and nothing else; `model/` importing it directly is the one boundary this package cannot afford to blur, and `i1-layer-boundaries` refuses it.
|
|
90
|
-
- **Adding a seventh constructor.** The six are the closed vocabulary the conventions, the linter and the documentation are all shaped around. A new SUBJECT to specify is a design decision, and it earns a record in [`decisions/`](decisions/) before it earns a constructor — as `integration` did in [ADR-004](decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md).
|
|
91
|
-
- **Writing a rule's normative sentence into a chapter.** It belongs in `src/lint/manifest.ts` beside the implementation; a chapter that restates it is the second copy the whole design exists to prevent.
|
|
92
|
-
|
|
93
|
-
## Related
|
|
94
|
-
|
|
95
|
-
[02 — Developing](02-developing.md) · [03 — Testing](03-testing.md) · [18 — Conventions](18-conventions.md) · [19 — Linting](19-linting.md)
|