@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
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { a as describeAmbiguity, i as AmbiguousElementError, o as formatElement, r as widenedForWindow, t as WINDOW_SETTLE_MS } from "./substring-warning.js";
|
|
2
|
-
import { mkdtempSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { setTimeout } from "node:timers/promises";
|
|
6
|
-
//#region src/model/elements/aria-name.ts
|
|
7
|
-
/**
|
|
8
|
-
* The accessible name an ARIA snapshot states.
|
|
9
|
-
*
|
|
10
|
-
* A descriptor matches the name the BROWSER computes, and that name is not the
|
|
11
|
-
* element's text: `<button><span>Experiments</span><span>9</span></button>`
|
|
12
|
-
* reads as `Experiments9` through `textContent` and is named "Experiments 9",
|
|
13
|
-
* a field's name comes from its label and is nowhere in its text at all, and a
|
|
14
|
-
* name cut at eighty characters resolves to nothing. Reading the name off the
|
|
15
|
-
* snapshot is what makes the spelling the window prints a spelling that can be
|
|
16
|
-
* written back into a spec.
|
|
17
|
-
*
|
|
18
|
-
* The dialect is the one both surfaces already speak — playwright's, on the
|
|
19
|
-
* page directly and through `server.commands` for a mounted component — so the
|
|
20
|
-
* projection is stated once, here, and neither adapter computes a name itself.
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* The name of the FIRST node a snapshot describes: `- button "Experiments 9"`,
|
|
24
|
-
* optionally followed by `:` and its children.
|
|
25
|
-
*
|
|
26
|
-
* A quote must follow the role across a SPACE. `- paragraph: "…"` is a node
|
|
27
|
-
* with text and no name, and reading that text as a name would hand the author
|
|
28
|
-
* a spelling no role descriptor matches on.
|
|
29
|
-
*/
|
|
30
|
-
const NAMED = /^-\s+[\w-]+\s+"(?<name>(?:[^"\\]|\\.)*)"/u;
|
|
31
|
-
/** Undo the escaping the snapshot applies to a quote or a backslash inside a name. */
|
|
32
|
-
function unescaped(name) {
|
|
33
|
-
return name.replaceAll(/\\(?<char>.)/gu, "$<char>");
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* The accessible name the snapshot gives the node it describes, or `undefined`
|
|
37
|
-
* when that node has none — an unnamed landmark, a paragraph of text.
|
|
38
|
-
*/
|
|
39
|
-
function accessibleNameIn(snapshot) {
|
|
40
|
-
const [first] = snapshot.split("\n");
|
|
41
|
-
const name = (first === void 0 ? null : NAMED.exec(first.trim()))?.groups?.name;
|
|
42
|
-
return name === void 0 ? void 0 : unescaped(name);
|
|
43
|
-
}
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/seams/playwright/playwright.adapter.ts
|
|
46
|
-
/** How many candidates the ambiguity error enumerates before truncating. */
|
|
47
|
-
const MAX_REPORTED_MATCHES = 10;
|
|
48
|
-
const VALUE_TIMEOUT_MS = 3e4;
|
|
49
|
-
const VALUE_POLL_MS = 100;
|
|
50
|
-
/**
|
|
51
|
-
* The roles a FIELD can carry — what `field('Name')` designates.
|
|
52
|
-
*
|
|
53
|
-
* A field is named by its ACCESSIBLE name, not by the text of the label
|
|
54
|
-
* element that happens to wrap it: `<label><span>Channel</span><select>…` has
|
|
55
|
-
* a label whose text carries every option of the select, while the control's
|
|
56
|
-
* accessible name is `Channel` — which is what the ARIA tree shows and what a
|
|
57
|
-
* screen reader reads. Resolving by label text alone made the ordinary
|
|
58
|
-
* wrapping-label form unreachable under the 16.0 whole-name default.
|
|
59
|
-
*
|
|
60
|
-
* The label-text locator stays in the union for the controls ARIA gives no
|
|
61
|
-
* role at all (a date or colour input), which nothing else would find.
|
|
62
|
-
*/
|
|
63
|
-
const FIELD_ROLES = [
|
|
64
|
-
"checkbox",
|
|
65
|
-
"combobox",
|
|
66
|
-
"radio",
|
|
67
|
-
"searchbox",
|
|
68
|
-
"slider",
|
|
69
|
-
"spinbutton",
|
|
70
|
-
"switch",
|
|
71
|
-
"textbox"
|
|
72
|
-
];
|
|
73
|
-
/**
|
|
74
|
-
* Translate a user-facing descriptor into a playwright locator, resolving
|
|
75
|
-
* `scope` outside-in so `within(navigation(), link('X'))` searches the nav
|
|
76
|
-
* subtree. No `.first()` anywhere: playwright's strict mode is the mechanism
|
|
77
|
-
* behind CONVENTIONS W3, and swallowing it would reintroduce the silent
|
|
78
|
-
* wrong-element bug the rule exists to prevent.
|
|
79
|
-
*/
|
|
80
|
-
function locate(root, element) {
|
|
81
|
-
const scope = element.scope ? locate(root, element.scope) : root;
|
|
82
|
-
const exact = element.exact ?? true;
|
|
83
|
-
const name = element.name ?? "";
|
|
84
|
-
if (element.kind === "field") {
|
|
85
|
-
let field = scope.getByLabel(name, { exact });
|
|
86
|
-
for (const role of FIELD_ROLES) field = field.or(scope.getByRole(role, {
|
|
87
|
-
exact,
|
|
88
|
-
name
|
|
89
|
-
}));
|
|
90
|
-
return field;
|
|
91
|
-
}
|
|
92
|
-
if (element.kind === "testId") return scope.getByTestId(name);
|
|
93
|
-
if (element.kind === "text") return scope.getByText(name, { exact });
|
|
94
|
-
return scope.getByRole(element.kind, element.name === void 0 ? { exact } : {
|
|
95
|
-
exact,
|
|
96
|
-
name: element.name
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
/** Playwright signals "more than one match" through this error text. */
|
|
100
|
-
function isStrictViolation(error) {
|
|
101
|
-
return error instanceof Error && error.message.includes("strict mode violation");
|
|
102
|
-
}
|
|
103
|
-
/** Capture the candidates in-page — the evidence the refusal enumerates. */
|
|
104
|
-
async function captureMatches(locator) {
|
|
105
|
-
return await locator.evaluateAll((nodes, limit) => {
|
|
106
|
-
const LANDMARKS = "nav, header, footer, main, aside, section, form, [role]";
|
|
107
|
-
return nodes.slice(0, limit).map((node) => {
|
|
108
|
-
const element = node;
|
|
109
|
-
const landmark = element.parentElement?.closest(LANDMARKS);
|
|
110
|
-
const context = landmark ? landmark.getAttribute("role") ?? landmark.tagName.toLowerCase() : null;
|
|
111
|
-
const detail = element.getAttribute("href") ?? element.getAttribute("name");
|
|
112
|
-
const text = (element.textContent ?? "").replaceAll(/\s+/gu, " ").trim().slice(0, 80);
|
|
113
|
-
const label = element.getAttribute("aria-label")?.trim();
|
|
114
|
-
return {
|
|
115
|
-
accessibleName: label && label !== text ? label : void 0,
|
|
116
|
-
context: context ?? void 0,
|
|
117
|
-
detail: detail ?? void 0,
|
|
118
|
-
tag: element.tagName.toLowerCase(),
|
|
119
|
-
text
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
}, MAX_REPORTED_MATCHES);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Run one visitor action, converting a strict-mode violation into the W3
|
|
126
|
-
* refusal. The ambiguous level is identified before reporting: a scope that
|
|
127
|
-
* matches several landmarks is the real fault, and naming the target instead
|
|
128
|
-
* would send the author to fix the wrong descriptor.
|
|
129
|
-
*/
|
|
130
|
-
async function act(page, element, action) {
|
|
131
|
-
const target = await widenedForWindow(element, page.url(), probeOf(page)) ?? element;
|
|
132
|
-
try {
|
|
133
|
-
return await action(locate(page, target));
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (!isStrictViolation(error)) throw error;
|
|
136
|
-
const culprit = await findAmbiguousLevel(page, target);
|
|
137
|
-
const matches = await captureMatches(locate(page, culprit));
|
|
138
|
-
throw new AmbiguousElementError(describeAmbiguity({
|
|
139
|
-
element: culprit,
|
|
140
|
-
matches,
|
|
141
|
-
url: page.url()
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/** What the transitional window may ask of the page. */
|
|
146
|
-
function probeOf(page) {
|
|
147
|
-
return {
|
|
148
|
-
count: async (element) => await locate(page, element).count(),
|
|
149
|
-
nameOf: async (element) => await wholeNameOf(page, element),
|
|
150
|
-
settles: async (element) => await settles(page, element)
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Does the descriptor arrive if the page is given the settle budget?
|
|
155
|
-
*
|
|
156
|
-
* A poll rather than `waitFor`, for the reason the rest of this adapter has no
|
|
157
|
-
* `.first()`: waiting on the locator itself would have to narrow a strict
|
|
158
|
-
* locator to one element, and a count answers the same question without
|
|
159
|
-
* touching the mechanism W3 rests on. The locator is rebuilt every round, so a
|
|
160
|
-
* navigation that replaced the document is read on the new one.
|
|
161
|
-
*/
|
|
162
|
-
async function settles(page, element) {
|
|
163
|
-
const deadline = Date.now() + WINDOW_SETTLE_MS;
|
|
164
|
-
for (;;) {
|
|
165
|
-
if (await countOrZero(page, element) > 0) return true;
|
|
166
|
-
if (Date.now() >= deadline) return false;
|
|
167
|
-
await setTimeout(VALUE_POLL_MS);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
/** A count taken mid-navigation throws; the page is simply not answering yet. */
|
|
171
|
-
async function countOrZero(page, element) {
|
|
172
|
-
try {
|
|
173
|
-
return await locate(page, element).count();
|
|
174
|
-
} catch {
|
|
175
|
-
return 0;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* The name to WRITE for this descriptor — the whole one, never truncated.
|
|
180
|
-
*
|
|
181
|
-
* A role descriptor matches the accessible name the browser computed, so the
|
|
182
|
-
* name is read off the ARIA snapshot, which is that same computation: the text
|
|
183
|
-
* content glues inline children with no separator (`Experiments9` for a button
|
|
184
|
-
* named "Experiments 9") and a field's name is its label, which its text never
|
|
185
|
-
* carries at all. A `text` descriptor matches text, so text is what it is told
|
|
186
|
-
* to write.
|
|
187
|
-
*/
|
|
188
|
-
async function wholeNameOf(page, element) {
|
|
189
|
-
const locator = locate(page, element);
|
|
190
|
-
if (element.kind !== "text") try {
|
|
191
|
-
const named = accessibleNameIn(await locator.ariaSnapshot());
|
|
192
|
-
if (named !== void 0) return named;
|
|
193
|
-
} catch {}
|
|
194
|
-
return await wholeTextOf(locator);
|
|
195
|
-
}
|
|
196
|
-
/** The element's text, whitespace-collapsed and WHOLE — a name cut mid-word resolves to nothing. */
|
|
197
|
-
async function wholeTextOf(locator) {
|
|
198
|
-
try {
|
|
199
|
-
const collapsed = (await locator.evaluate((node) => node.textContent ?? "")).replaceAll(/\s+/gu, " ").trim();
|
|
200
|
-
return collapsed === "" ? void 0 : collapsed;
|
|
201
|
-
} catch {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
/** Walk the scope chain outside-in; the outermost ambiguous level is the fault. */
|
|
206
|
-
async function findAmbiguousLevel(page, element) {
|
|
207
|
-
const chain = [];
|
|
208
|
-
for (let current = element; current; current = current.scope) chain.unshift(current);
|
|
209
|
-
for (const level of chain.slice(0, -1)) if (await locate(page, level).count() > 1) return level;
|
|
210
|
-
return element;
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* The same element, narrowed by whatever STATE the descriptor carries — focus,
|
|
214
|
-
* or accepting input — or `null` when it carries none.
|
|
215
|
-
*
|
|
216
|
-
* A second condition on the locator rather than a one-shot probe: the locator
|
|
217
|
-
* keeps playwright's own retry, so `see(focused(x))` waits for the keyboard to
|
|
218
|
-
* arrive instead of asking once and failing on a frame of animation. It also
|
|
219
|
-
* needs nothing but `playwright` — `expect(...).toBeFocused()` lives in
|
|
220
|
-
* `@playwright/test`, which this package deliberately does not carry.
|
|
221
|
-
*/
|
|
222
|
-
function stateOnly(page, locator, element) {
|
|
223
|
-
if (element.focused === true) return locator.and(page.locator(":focus"));
|
|
224
|
-
if (element.disabled !== void 0) return locator.and(page.locator(element.disabled ? ":disabled" : ":enabled"));
|
|
225
|
-
if (element.selected !== void 0) return locator.and(page.locator(element.selected ? ":checked" : ":not(:checked)"));
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Is this descriptor answered by PRESENCE rather than by visibility? The
|
|
230
|
-
* options of a collapsed `<select>` are in the document and in the tree, and
|
|
231
|
-
* none of them has a box on the screen until a visitor opens it.
|
|
232
|
-
*/
|
|
233
|
-
function byPresence(element) {
|
|
234
|
-
return element.kind === "option";
|
|
235
|
-
}
|
|
236
|
-
/** The field's live value, or `null` when nothing (unambiguous) is there yet. */
|
|
237
|
-
async function currentValue(locator) {
|
|
238
|
-
try {
|
|
239
|
-
return await locator.inputValue({ timeout: VALUE_POLL_MS });
|
|
240
|
-
} catch (error) {
|
|
241
|
-
if (isStrictViolation(error)) throw error;
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Wait until the field holds the value the descriptor carries — or, for
|
|
247
|
-
* `gone()`, until it no longer does.
|
|
248
|
-
*
|
|
249
|
-
* A poll rather than a narrowed locator: a value is a PROPERTY of the node,
|
|
250
|
-
* and the CSS a locator narrows with reads ATTRIBUTES — a controlled input
|
|
251
|
-
* whose `value` attribute never moves would answer for a value it no longer
|
|
252
|
-
* holds. The locator is re-read every round, so a re-rendered field is read
|
|
253
|
-
* again rather than held as a stale handle.
|
|
254
|
-
*/
|
|
255
|
-
async function waitForValue(locator, element, wanted) {
|
|
256
|
-
const value = element.value ?? "";
|
|
257
|
-
const deadline = Date.now() + VALUE_TIMEOUT_MS;
|
|
258
|
-
for (;;) {
|
|
259
|
-
const actual = await currentValue(locator);
|
|
260
|
-
if (actual === null ? !wanted : actual === value === wanted) return;
|
|
261
|
-
if (Date.now() >= deadline) throw new Error(`${formatElement(element)} ${wanted ? "never held" : "still holds"} ${JSON.stringify(value)}${actual === null ? " (it is not on the page)" : `, it holds ${JSON.stringify(actual)}`}`);
|
|
262
|
-
await setTimeout(VALUE_POLL_MS);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/** The visitor implementation — every action auto-waits via playwright actionability. */
|
|
266
|
-
function createVisitor(page, baseUrl) {
|
|
267
|
-
return {
|
|
268
|
-
check: async (element) => {
|
|
269
|
-
await act(page, element, async (locator) => {
|
|
270
|
-
await locator.check();
|
|
271
|
-
});
|
|
272
|
-
},
|
|
273
|
-
click: async (element) => {
|
|
274
|
-
await act(page, element, async (locator) => {
|
|
275
|
-
await locator.click();
|
|
276
|
-
});
|
|
277
|
-
},
|
|
278
|
-
fill: async (element, value) => {
|
|
279
|
-
await act(page, element, async (locator) => {
|
|
280
|
-
await locator.fill(value);
|
|
281
|
-
});
|
|
282
|
-
},
|
|
283
|
-
goto: async (path) => {
|
|
284
|
-
await page.goto(`${baseUrl}${path}`, { waitUntil: "load" });
|
|
285
|
-
},
|
|
286
|
-
hover: async (element) => {
|
|
287
|
-
await act(page, element, async (locator) => {
|
|
288
|
-
await locator.hover();
|
|
289
|
-
});
|
|
290
|
-
},
|
|
291
|
-
press: async (key) => {
|
|
292
|
-
await page.keyboard.press(key);
|
|
293
|
-
},
|
|
294
|
-
gone: async (element) => {
|
|
295
|
-
await act(page, element, async (locator) => {
|
|
296
|
-
if (element.value !== void 0) {
|
|
297
|
-
await waitForValue(locator, element, false);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const narrowed = stateOnly(page, locator, element);
|
|
301
|
-
if (narrowed !== null) {
|
|
302
|
-
await narrowed.waitFor({ state: "detached" });
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await locator.waitFor({ state: byPresence(element) ? "detached" : "hidden" });
|
|
306
|
-
});
|
|
307
|
-
},
|
|
308
|
-
see: async (element) => {
|
|
309
|
-
await act(page, element, async (locator) => {
|
|
310
|
-
if (element.value !== void 0) {
|
|
311
|
-
await waitForValue(locator, element, true);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const narrowed = stateOnly(page, locator, element);
|
|
315
|
-
if (narrowed !== null) {
|
|
316
|
-
await narrowed.waitFor({ state: "attached" });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
await locator.waitFor({ state: byPresence(element) ? "attached" : "visible" });
|
|
320
|
-
});
|
|
321
|
-
},
|
|
322
|
-
select: async (element, option) => {
|
|
323
|
-
await act(page, element, async (locator) => await locator.selectOption(option));
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Browser adapter backed by playwright chromium.
|
|
329
|
-
*
|
|
330
|
-
* ONE browser process per adapter (= per runner = per vitest worker),
|
|
331
|
-
* launched lazily on the first `open()`. Each visit gets a fresh
|
|
332
|
-
* `BrowserContext` — isolation without paying a browser launch per spec.
|
|
333
|
-
*
|
|
334
|
-
* Playwright is an optional peer dependency: it is only imported here, and
|
|
335
|
-
* this module is only loaded when a spec calls `.visit()`.
|
|
336
|
-
*/
|
|
337
|
-
var PlaywrightAdapter = class {
|
|
338
|
-
browser = null;
|
|
339
|
-
async close() {
|
|
340
|
-
if (this.browser) {
|
|
341
|
-
await this.browser.close();
|
|
342
|
-
this.browser = null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
async open(url, options) {
|
|
346
|
-
const context = await (await this.launch()).newContext({ extraHTTPHeaders: options.headers ?? {} });
|
|
347
|
-
if (options.external === "block") {
|
|
348
|
-
const allowed = /* @__PURE__ */ new Set([new URL(options.baseUrl).origin, ...options.allowedOrigins ?? []]);
|
|
349
|
-
await context.route("**/*", (route) => {
|
|
350
|
-
if (allowed.has(new URL(route.request().url()).origin)) route.continue();
|
|
351
|
-
else route.abort();
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
try {
|
|
355
|
-
const page = await context.newPage();
|
|
356
|
-
if (options.clock !== void 0) await page.clock.setFixedTime(new Date(options.clock));
|
|
357
|
-
const consoleMessages = [];
|
|
358
|
-
page.on("console", (message) => {
|
|
359
|
-
consoleMessages.push({
|
|
360
|
-
text: message.text(),
|
|
361
|
-
type: message.type()
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
const response = await page.goto(url, { waitUntil: "load" });
|
|
365
|
-
if (options.scenario) try {
|
|
366
|
-
await options.scenario(createVisitor(page, options.baseUrl));
|
|
367
|
-
} catch (error) {
|
|
368
|
-
const evidence = await this.captureEvidence(page);
|
|
369
|
-
const suffix = evidence ? `\nEvidence: ${evidence}` : "";
|
|
370
|
-
if (error instanceof AmbiguousElementError) {
|
|
371
|
-
error.message += suffix;
|
|
372
|
-
throw error;
|
|
373
|
-
}
|
|
374
|
-
throw new Error(`visit scenario failed: ${error instanceof Error ? error.message : String(error)}${suffix}`, { cause: error });
|
|
375
|
-
}
|
|
376
|
-
const extraction = await page.evaluate(() => {
|
|
377
|
-
const links = [...document.querySelectorAll("link")].map((link) => ({
|
|
378
|
-
href: link.href,
|
|
379
|
-
hreflang: link.hreflang || void 0,
|
|
380
|
-
rel: link.rel,
|
|
381
|
-
type: link.type || void 0
|
|
382
|
-
}));
|
|
383
|
-
const metas = [...document.querySelectorAll("meta")].map((meta) => ({
|
|
384
|
-
content: meta.content,
|
|
385
|
-
name: meta.name || void 0,
|
|
386
|
-
property: meta.getAttribute("property") ?? void 0
|
|
387
|
-
}));
|
|
388
|
-
const jsonLdBlocks = [...document.querySelectorAll("script[type=\"application/ld+json\"]")].map((script) => script.textContent ?? "");
|
|
389
|
-
return {
|
|
390
|
-
html: document.documentElement.outerHTML,
|
|
391
|
-
jsonLdBlocks,
|
|
392
|
-
links,
|
|
393
|
-
metas,
|
|
394
|
-
text: document.body?.innerText ?? "",
|
|
395
|
-
title: document.title
|
|
396
|
-
};
|
|
397
|
-
});
|
|
398
|
-
const tree = await page.locator("body").ariaSnapshot();
|
|
399
|
-
return {
|
|
400
|
-
consoleMessages,
|
|
401
|
-
status: response?.status() ?? 0,
|
|
402
|
-
tree,
|
|
403
|
-
url: page.url(),
|
|
404
|
-
...extraction
|
|
405
|
-
};
|
|
406
|
-
} finally {
|
|
407
|
-
await context.close();
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/** Screenshot the failing state into a temp file; never masks the original error. */
|
|
411
|
-
async captureEvidence(page) {
|
|
412
|
-
try {
|
|
413
|
-
const dir = mkdtempSync(resolve(tmpdir(), "spec-website-"));
|
|
414
|
-
const path = resolve(dir, "failure.png");
|
|
415
|
-
await page.screenshot({
|
|
416
|
-
fullPage: true,
|
|
417
|
-
path
|
|
418
|
-
});
|
|
419
|
-
return path;
|
|
420
|
-
} catch {
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/** Launch the shared chromium instance (once), with an actionable error when playwright is absent. */
|
|
425
|
-
async launch() {
|
|
426
|
-
if (this.browser) return this.browser;
|
|
427
|
-
let chromium;
|
|
428
|
-
try {
|
|
429
|
-
({chromium} = await import("playwright"));
|
|
430
|
-
} catch {
|
|
431
|
-
throw new Error(".visit() requires playwright (optional peer dependency): npm install -D playwright && npx playwright install chromium");
|
|
432
|
-
}
|
|
433
|
-
this.browser = await chromium.launch();
|
|
434
|
-
return this.browser;
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
//#endregion
|
|
438
|
-
export { PlaywrightAdapter };
|
package/dist/server.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { a as isStreamBody, n as ContractQueue, t as toReadableStream } from "./stream.js";
|
|
2
|
-
//#region src/seams/msw/handlers.ts
|
|
3
|
-
/**
|
|
4
|
-
* What a chain with zero contracts gets where the facet's scope is KNOWN but
|
|
5
|
-
* not total — the node engine, which shares a process with everything else the
|
|
6
|
-
* test does. Nothing was declared, so nothing is guarded and nothing has to be
|
|
7
|
-
* torn down (CONVENTIONS D7, known scope).
|
|
8
|
-
*/
|
|
9
|
-
const NO_CONTRACTS = {
|
|
10
|
-
cleanup: () => {},
|
|
11
|
-
violation: () => null
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* A contract answers the request that was made, never a copy of an earlier one.
|
|
15
|
-
*
|
|
16
|
-
* In a page the reply travels through a real service worker and a real HTTP
|
|
17
|
-
* cache, so a second render of the same component can be served the FIRST
|
|
18
|
-
* test's answer and never reach the queue at all — a contract that was
|
|
19
|
-
* exhausted, or replaced, silently keeps answering. Stated as a default, so a
|
|
20
|
-
* spec whose subject IS caching can still say otherwise.
|
|
21
|
-
*/
|
|
22
|
-
const NEVER_CACHED = { "cache-control": "no-store" };
|
|
23
|
-
/** Turn a contract response into the msw reply, body kind by body kind. */
|
|
24
|
-
function toMswResponse(msw, response) {
|
|
25
|
-
const { body, headers, status = 200 } = response;
|
|
26
|
-
if (response.transport === "network-error") return msw.HttpResponse.error();
|
|
27
|
-
if (body === null || body === void 0) return new msw.HttpResponse(null, {
|
|
28
|
-
headers: {
|
|
29
|
-
...NEVER_CACHED,
|
|
30
|
-
...headers
|
|
31
|
-
},
|
|
32
|
-
status
|
|
33
|
-
});
|
|
34
|
-
if (isStreamBody(body)) return new msw.HttpResponse(toReadableStream(body), {
|
|
35
|
-
headers: {
|
|
36
|
-
...NEVER_CACHED,
|
|
37
|
-
"content-type": body.contentType,
|
|
38
|
-
...headers
|
|
39
|
-
},
|
|
40
|
-
status
|
|
41
|
-
});
|
|
42
|
-
if (typeof body === "string") return new msw.HttpResponse(body, {
|
|
43
|
-
headers: {
|
|
44
|
-
...NEVER_CACHED,
|
|
45
|
-
"content-type": "text/plain; charset=utf-8",
|
|
46
|
-
...headers
|
|
47
|
-
},
|
|
48
|
-
status
|
|
49
|
-
});
|
|
50
|
-
return msw.HttpResponse.json(body, {
|
|
51
|
-
headers: {
|
|
52
|
-
...NEVER_CACHED,
|
|
53
|
-
...headers
|
|
54
|
-
},
|
|
55
|
-
status
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* The handlers one chain's contracts become, plus the trailing catch-all that
|
|
60
|
-
* records any request no contract accepted (CONVENTIONS D7). The caller decides
|
|
61
|
-
* what to do with the recorded violation — the api chain rethrows it, the
|
|
62
|
-
* component chain fails the render. An EMPTY contract list is still a guard:
|
|
63
|
-
* the catch-all alone says "this subject has no network".
|
|
64
|
-
*
|
|
65
|
-
* `bypass` names the traffic that is not the subject's: under node there is
|
|
66
|
-
* none, in a page it is everything the runner fetches to BE a page.
|
|
67
|
-
*/
|
|
68
|
-
function buildContractHandlers(msw, contracts, bypass) {
|
|
69
|
-
const queue = new ContractQueue(contracts);
|
|
70
|
-
let violation = null;
|
|
71
|
-
const recordViolation = (method, url) => {
|
|
72
|
-
violation ??= queue.unmatchedError(method, url);
|
|
73
|
-
return msw.HttpResponse.json({ error: `@jterrazz/test strict contracts: unmatched request ${method} ${url}` }, {
|
|
74
|
-
headers: NEVER_CACHED,
|
|
75
|
-
status: 501
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
const handlers = [];
|
|
79
|
-
for (const route of queue.routes) for (const method of route.methods) {
|
|
80
|
-
const handlerFn = method === "*" ? msw.http.all : msw.http[method.toLowerCase()];
|
|
81
|
-
if (!handlerFn) continue;
|
|
82
|
-
handlers.push(handlerFn(route.url, async ({ request }) => {
|
|
83
|
-
const observed = await observe(request);
|
|
84
|
-
const contract = queue.take(observed);
|
|
85
|
-
if (!contract) return recordViolation(request.method, request.url);
|
|
86
|
-
const response = typeof contract.response === "function" ? contract.response(observed) : contract.response;
|
|
87
|
-
if (response.delay) await new Promise((resolve) => setTimeout(resolve, response.delay));
|
|
88
|
-
return toMswResponse(msw, response);
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
|
|
92
|
-
return {
|
|
93
|
-
handlers,
|
|
94
|
-
violation: () => violation ?? queue.requiredError()
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
/** The request as the queue matches it — body parsed when it is JSON. */
|
|
98
|
-
async function observe(request) {
|
|
99
|
-
let body = null;
|
|
100
|
-
const rawText = await request.clone().text();
|
|
101
|
-
if (rawText) try {
|
|
102
|
-
body = JSON.parse(rawText);
|
|
103
|
-
} catch {
|
|
104
|
-
body = rawText;
|
|
105
|
-
}
|
|
106
|
-
const headers = {};
|
|
107
|
-
request.headers.forEach((value, key) => {
|
|
108
|
-
headers[key.toLowerCase()] = value;
|
|
109
|
-
});
|
|
110
|
-
return {
|
|
111
|
-
body,
|
|
112
|
-
headers,
|
|
113
|
-
method: request.method.toUpperCase(),
|
|
114
|
-
url: request.url
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/seams/msw/server.ts
|
|
119
|
-
let mswModule = null;
|
|
120
|
-
let mswHttp = null;
|
|
121
|
-
async function loadMsw() {
|
|
122
|
-
if (mswModule === null) {
|
|
123
|
-
mswModule = await import("msw/node");
|
|
124
|
-
mswHttp = await import("msw");
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
msw: mswHttp,
|
|
128
|
-
node: mswModule
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
let serverInstance = null;
|
|
132
|
-
/**
|
|
133
|
-
* Start the MSW server (once per process).
|
|
134
|
-
*/
|
|
135
|
-
async function ensureContractServer() {
|
|
136
|
-
if (serverInstance !== null) return;
|
|
137
|
-
const { node } = await loadMsw();
|
|
138
|
-
serverInstance = node.setupServer();
|
|
139
|
-
serverInstance.listen({ onUnhandledRequest: "bypass" });
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Register a chain's contracts as MSW handlers. A trailing catch-all handler
|
|
143
|
-
* records any request no contract accepted — the builder rethrows it as the
|
|
144
|
-
* spec failure (rejecting the action promise, never an unhandled rejection).
|
|
145
|
-
*/
|
|
146
|
-
async function registerContracts(contracts) {
|
|
147
|
-
if (contracts.length === 0) return NO_CONTRACTS;
|
|
148
|
-
await ensureContractServer();
|
|
149
|
-
const { msw } = await loadMsw();
|
|
150
|
-
const { handlers, violation } = buildContractHandlers(msw, contracts);
|
|
151
|
-
serverInstance.use(...handlers);
|
|
152
|
-
return {
|
|
153
|
-
cleanup: () => {
|
|
154
|
-
serverInstance.resetHandlers();
|
|
155
|
-
},
|
|
156
|
-
violation
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
//#endregion
|
|
160
|
-
export { registerContracts };
|