@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
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,282 +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
|
-
/** The node side's printer — the default every adapter but the page's uses. */
|
|
168
|
-
function toStandardError(line) {
|
|
169
|
-
if (typeof process === "undefined") {
|
|
170
|
-
console.warn(line);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
process.stderr.write(`${line}\n`);
|
|
174
|
-
}
|
|
175
|
-
/** Descriptors already reported in this process. */
|
|
176
|
-
const reported = /* @__PURE__ */ new Set();
|
|
177
|
-
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
178
|
-
function identityOf(element) {
|
|
179
|
-
const own = `${element.kind}:${element.name ?? ""}`;
|
|
180
|
-
return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Report that `element` designated nothing as a whole name, but would have
|
|
184
|
-
* designated something as a substring. Returns whether a line was printed,
|
|
185
|
-
* which is what the adapters' own tests assert on.
|
|
186
|
-
*/
|
|
187
|
-
async function warnSubstringOnly(element, where, found, print = toStandardError) {
|
|
188
|
-
const identity = identityOf(element);
|
|
189
|
-
if (reported.has(identity)) return false;
|
|
190
|
-
reported.add(identity);
|
|
191
|
-
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
192
|
-
await print(`@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.`);
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* How long a descriptor is given to ARRIVE before the window concludes it is
|
|
197
|
-
* absent — the budget `settles` spends and no more.
|
|
198
|
-
*
|
|
199
|
-
* A count is one observation, and a verb called right after a navigating
|
|
200
|
-
* `click()` takes it on the page being LEFT. The window may not decide
|
|
201
|
-
* "nothing matches this whole name" on that reading: the destination has not
|
|
202
|
-
* been parsed yet. Long enough for a navigation to commit, far short of the
|
|
203
|
-
* verb's own 30 s, and paid only when there is a substring to widen to.
|
|
204
|
-
*/
|
|
205
|
-
const WINDOW_SETTLE_MS = 5e3;
|
|
206
|
-
/**
|
|
207
|
-
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
208
|
-
* would change.
|
|
209
|
-
*
|
|
210
|
-
* Every level of the chain is asked the same question the target is asked —
|
|
211
|
-
* `within(button('Proof of authorship'), content('Verified'))` against a
|
|
212
|
-
* button named "Proof of authorship Verified" failed on the SCOPE, and a
|
|
213
|
-
* warning that only ever widened the target printed nothing at all. The chain
|
|
214
|
-
* is walked outside-in and rebuilt as it goes, since an inner level's answer
|
|
215
|
-
* is meaningless under a scope that designates nothing.
|
|
216
|
-
*
|
|
217
|
-
* A level that states an `exact` of its own is left alone: an author who wrote
|
|
218
|
-
* `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
|
|
219
|
-
* was already exact.
|
|
220
|
-
*
|
|
221
|
-
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
222
|
-
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
223
|
-
* never runs — the test is already over. The common case (the name matches
|
|
224
|
-
* whole) costs one count per level, which waits for nothing.
|
|
225
|
-
*/
|
|
226
|
-
async function widenedForWindow(element, where, probe) {
|
|
227
|
-
const chain = [];
|
|
228
|
-
for (let level = element; level; level = level.scope) chain.unshift(level);
|
|
229
|
-
let rebuilt;
|
|
230
|
-
let widened = false;
|
|
231
|
-
for (const level of chain) {
|
|
232
|
-
const candidate = {
|
|
233
|
-
...level,
|
|
234
|
-
...rebuilt === void 0 ? {} : { scope: rebuilt }
|
|
235
|
-
};
|
|
236
|
-
const loose = await widenedLevel(candidate, where, probe);
|
|
237
|
-
widened ||= loose !== void 0;
|
|
238
|
-
rebuilt = loose ?? candidate;
|
|
239
|
-
}
|
|
240
|
-
return widened ? rebuilt : void 0;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* The one level, widened and warned about, or `undefined` when it designates
|
|
244
|
-
* something already, states its own `exact`, carries no name, would designate
|
|
245
|
-
* nothing either way, or turns out to be exact on the surface that ARRIVES.
|
|
246
|
-
*
|
|
247
|
-
* The last is the navigation case, and it is the reason the verdict is not two
|
|
248
|
-
* counts: a `see()` fired right after a navigating `click()` counts the page
|
|
249
|
-
* being left, so a descriptor that is perfectly exact on the DESTINATION read
|
|
250
|
-
* as substring-only — non-deterministically, since which page each count
|
|
251
|
-
* landed on was a race. A widening is therefore only decided once the exact
|
|
252
|
-
* descriptor has been given the settle budget and still designates nothing.
|
|
253
|
-
* The tell of the old fault was the printed name equalling the name written.
|
|
254
|
-
*/
|
|
255
|
-
async function widenedLevel(candidate, where, probe) {
|
|
256
|
-
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
257
|
-
try {
|
|
258
|
-
if (await probe.count(candidate) > 0) return;
|
|
259
|
-
const loose = {
|
|
260
|
-
...candidate,
|
|
261
|
-
exact: false
|
|
262
|
-
};
|
|
263
|
-
if (await probe.count(loose) !== 1) return;
|
|
264
|
-
if (!alreadyJudged(candidate) && await probe.settles(candidate)) return;
|
|
265
|
-
await warnSubstringOnly(candidate, where, await probe.nameOf(loose), probe.print);
|
|
266
|
-
return loose;
|
|
267
|
-
} catch {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Has this descriptor already been judged substring-only in this run?
|
|
273
|
-
*
|
|
274
|
-
* The verdict is made once: a scenario that clicks the same link in ten tests
|
|
275
|
-
* pays the settle budget on the first of them and is widened straight away on
|
|
276
|
-
* the nine after it.
|
|
277
|
-
*/
|
|
278
|
-
function alreadyJudged(element) {
|
|
279
|
-
return reported.has(identityOf(element));
|
|
280
|
-
}
|
|
281
|
-
//#endregion
|
|
282
|
-
export { describeAmbiguity as a, AmbiguousElementError as i, warnSubstringOnly as n, formatElement as o, widenedForWindow as r, WINDOW_SETTLE_MS as t };
|