@jterrazz/test 15.3.0 → 16.1.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 +120 -427
- package/dist/appium.adapter.js +71 -15
- package/dist/artifacts.js +25 -1735
- package/dist/browser/index.js +304 -233
- package/dist/checker.d.ts +1 -1
- package/dist/checker.js +1103 -97
- package/dist/fixtures.js +110 -0
- package/dist/index.d.ts +956 -971
- package/dist/index.js +2159 -2281
- package/dist/literate.js +1747 -0
- package/dist/match.js +625 -84
- package/dist/oxlint.cjs +3585 -1480
- package/dist/oxlint.d.cts +60 -48
- package/dist/oxlint.d.ts +60 -48
- package/dist/oxlint.js +3585 -1480
- package/dist/playwright.adapter.js +51 -8
- package/dist/{intercept.js → server.js} +2 -2
- package/dist/stream.js +6 -6
- package/dist/{ambiguity.js → substring-warning.js} +106 -7
- package/dist/vitest.d.ts +67 -48
- package/dist/vitest.js +229 -93
- package/docs/01-architecture.md +95 -0
- package/docs/02-developing.md +351 -0
- package/docs/03-testing.md +495 -0
- package/docs/04-operating.md +101 -0
- package/docs/05-module-tests.md +152 -0
- package/docs/06-integration.md +180 -0
- package/docs/07-component.md +246 -0
- package/docs/08-website.md +404 -0
- package/docs/09-mobile.md +270 -0
- package/docs/10-api.md +264 -0
- package/docs/11-jobs.md +157 -0
- package/docs/12-cli.md +677 -0
- package/docs/13-elements.md +242 -0
- package/docs/14-assertions.md +314 -0
- package/docs/15-tokens.md +253 -0
- package/docs/16-contracts.md +438 -0
- package/docs/17-services.md +188 -0
- package/docs/18-conventions.md +305 -0
- package/docs/19-linting.md +510 -0
- package/docs/README.md +36 -0
- package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
- package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
- package/docs/decisions/_template.md +22 -0
- package/docs/reference/classes/Matcher.md +120 -0
- package/docs/reference/classes/ProcessHandle.md +368 -0
- package/docs/reference/functions/defineContract.md +39 -0
- package/docs/reference/functions/defineContracts.md +36 -0
- package/docs/reference/functions/disabled.md +28 -0
- package/docs/reference/functions/enabled.md +20 -0
- package/docs/reference/functions/focused.md +26 -0
- package/docs/reference/functions/mockOf.md +79 -0
- package/docs/reference/functions/postgres.md +31 -0
- package/docs/reference/functions/process.md +33 -0
- package/docs/reference/functions/redis.md +28 -0
- package/docs/reference/functions/required.md +41 -0
- package/docs/reference/functions/selected.md +26 -0
- package/docs/reference/functions/sqlite.md +33 -0
- package/docs/reference/functions/testId.md +20 -0
- package/docs/reference/functions/text.md +40 -0
- package/docs/reference/functions/valued.md +28 -0
- package/docs/reference/functions/waitUntil.md +46 -0
- package/docs/reference/functions/within.md +29 -0
- package/docs/reference/index.md +196 -0
- package/docs/reference/interfaces/BaseResult.md +87 -0
- package/docs/reference/interfaces/CallResult.md +160 -0
- package/docs/reference/interfaces/CaptureScope.md +82 -0
- package/docs/reference/interfaces/CliResult.md +253 -0
- package/docs/reference/interfaces/ContainerAccessor.md +166 -0
- package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
- package/docs/reference/interfaces/FetchResult.md +180 -0
- package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
- package/docs/reference/interfaces/HttpResult.md +126 -0
- package/docs/reference/interfaces/JsonAccessor.md +46 -0
- package/docs/reference/interfaces/PageResult.md +356 -0
- package/docs/reference/interfaces/RenderResult.md +72 -0
- package/docs/reference/interfaces/RenderedText.md +110 -0
- package/docs/reference/interfaces/ResponseAccessor.md +47 -0
- package/docs/reference/interfaces/ScreenResult.md +128 -0
- package/docs/reference/interfaces/TableAccessor.md +26 -0
- package/docs/reference/interfaces/TextAccessor.md +96 -0
- package/docs/reference/type-aliases/ApiHandle.md +68 -0
- package/docs/reference/type-aliases/ApiSpecification.md +213 -0
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
- package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
- package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
- package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
- package/docs/reference/type-aliases/BrowserPage.md +132 -0
- package/docs/reference/type-aliases/BrowserPort.md +51 -0
- package/docs/reference/type-aliases/CliEnv.md +11 -0
- package/docs/reference/type-aliases/CliHandle.md +80 -0
- package/docs/reference/type-aliases/CliInput.md +36 -0
- package/docs/reference/type-aliases/CliOutput.md +46 -0
- package/docs/reference/type-aliases/CliPort.md +58 -0
- package/docs/reference/type-aliases/CliSpecification.md +144 -0
- package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
- package/docs/reference/type-aliases/ComponentChain.md +156 -0
- package/docs/reference/type-aliases/ComponentScenario.md +20 -0
- package/docs/reference/type-aliases/ComponentUi.md +47 -0
- package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
- package/docs/reference/type-aliases/ContainerPort.md +113 -0
- package/docs/reference/type-aliases/Contract.md +68 -0
- package/docs/reference/type-aliases/ContractInput.md +13 -0
- package/docs/reference/type-aliases/ContractRequest.md +95 -0
- package/docs/reference/type-aliases/ContractResponder.md +23 -0
- package/docs/reference/type-aliases/ContractResponse.md +78 -0
- package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
- package/docs/reference/type-aliases/Contracts.md +48 -0
- package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
- package/docs/reference/type-aliases/DatabasePort.md +72 -0
- package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
- package/docs/reference/type-aliases/DevicePort.md +51 -0
- package/docs/reference/type-aliases/DeviceScreen.md +32 -0
- package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
- package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
- package/docs/reference/type-aliases/DomMount.md +21 -0
- package/docs/reference/type-aliases/ElementKind.md +27 -0
- package/docs/reference/type-aliases/ElementMatch.md +74 -0
- package/docs/reference/type-aliases/ElementOptions.md +29 -0
- package/docs/reference/type-aliases/ElementRef.md +128 -0
- package/docs/reference/type-aliases/ExecOptions.md +37 -0
- package/docs/reference/type-aliases/FileAccessor.md +54 -0
- package/docs/reference/type-aliases/HonoApp.md +31 -0
- package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
- package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
- package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
- package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
- package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
- package/docs/reference/type-aliases/Intercept.md +19 -0
- package/docs/reference/type-aliases/InterceptOptions.md +28 -0
- package/docs/reference/type-aliases/InterceptScope.md +11 -0
- package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
- package/docs/reference/type-aliases/JobHandle.md +34 -0
- package/docs/reference/type-aliases/JobsHandle.md +57 -0
- package/docs/reference/type-aliases/JobsResult.md +16 -0
- package/docs/reference/type-aliases/JobsSpecification.md +94 -0
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
- package/docs/reference/type-aliases/LandmarkKind.md +20 -0
- package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
- package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
- package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
- package/docs/reference/type-aliases/MatchableRequest.md +61 -0
- package/docs/reference/type-aliases/MatcherKind.md +34 -0
- package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
- package/docs/reference/type-aliases/MobileElementKind.md +14 -0
- package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
- package/docs/reference/type-aliases/MobileElementRef.md +20 -0
- package/docs/reference/type-aliases/MobileHandle.md +65 -0
- package/docs/reference/type-aliases/MobileScenario.md +20 -0
- package/docs/reference/type-aliases/MobileSpecification.md +51 -0
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
- package/docs/reference/type-aliases/MobileVisitor.md +81 -0
- package/docs/reference/type-aliases/MockOfOptions.md +26 -0
- package/docs/reference/type-aliases/MockPort.md +10 -0
- package/docs/reference/type-aliases/PinnedClock.md +10 -0
- package/docs/reference/type-aliases/PostgresOptions.md +32 -0
- package/docs/reference/type-aliases/ProcessOptions.md +112 -0
- package/docs/reference/type-aliases/RedisOptions.md +20 -0
- package/docs/reference/type-aliases/RenderSubject.md +12 -0
- package/docs/reference/type-aliases/ScreenNode.md +67 -0
- package/docs/reference/type-aliases/ServerPort.md +36 -0
- package/docs/reference/type-aliases/ServerResponse.md +46 -0
- package/docs/reference/type-aliases/ServerSpec.md +21 -0
- package/docs/reference/type-aliases/ServiceHandle.md +216 -0
- package/docs/reference/type-aliases/ServiceRecord.md +12 -0
- package/docs/reference/type-aliases/SpecDocument.md +84 -0
- package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
- package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
- package/docs/reference/type-aliases/SpecFixture.md +30 -0
- package/docs/reference/type-aliases/SpecKind.md +8 -0
- package/docs/reference/type-aliases/SpecRun.md +108 -0
- package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
- package/docs/reference/type-aliases/SpecStream.md +32 -0
- package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
- package/docs/reference/type-aliases/SqliteOptions.md +36 -0
- package/docs/reference/type-aliases/SseEvent.md +58 -0
- package/docs/reference/type-aliases/StreamBody.md +61 -0
- package/docs/reference/type-aliases/TextFilter.md +22 -0
- package/docs/reference/type-aliases/VisitScenario.md +20 -0
- package/docs/reference/type-aliases/Visitor.md +215 -0
- package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
- package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
- package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
- package/docs/reference/variables/anthropic.md +19 -0
- package/docs/reference/variables/banner.md +10 -0
- package/docs/reference/variables/button.md +10 -0
- package/docs/reference/variables/clock.md +16 -0
- package/docs/reference/variables/complementary.md +10 -0
- package/docs/reference/variables/component.md +10 -0
- package/docs/reference/variables/content.md +10 -0
- package/docs/reference/variables/contentinfo.md +10 -0
- package/docs/reference/variables/dialog.md +10 -0
- package/docs/reference/variables/field.md +10 -0
- package/docs/reference/variables/form.md +10 -0
- package/docs/reference/variables/heading.md +10 -0
- package/docs/reference/variables/http.md +40 -0
- package/docs/reference/variables/intercept.md +11 -0
- package/docs/reference/variables/link.md +10 -0
- package/docs/reference/variables/listitem.md +10 -0
- package/docs/reference/variables/main.md +10 -0
- package/docs/reference/variables/match.md +8 -0
- package/docs/reference/variables/navigation.md +10 -0
- package/docs/reference/variables/openai.md +21 -0
- package/docs/reference/variables/option.md +17 -0
- package/docs/reference/variables/region.md +10 -0
- package/docs/reference/variables/row.md +10 -0
- package/docs/reference/variables/search.md +10 -0
- package/docs/reference/variables/specification.md +8 -0
- package/docs/reference/variables/status.md +10 -0
- package/docs/reference/variables/table.md +10 -0
- package/package.json +32 -11
- package/dist/spec-document.js +0 -651
package/dist/browser/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { TestRunner, expect, inject, onTestFinished, vi } from "vitest";
|
|
2
2
|
import { commands, page, userEvent } from "vitest/browser";
|
|
3
3
|
import { mock, mockDeep } from "vitest-mock-extended";
|
|
4
|
-
|
|
5
|
-
//#region src/specification/contracts/contract.ts
|
|
4
|
+
//#region src/model/contracts/contract.ts
|
|
6
5
|
/**
|
|
7
6
|
* Declare one contract. Identity function — its value is the enforced shape
|
|
8
7
|
* and the naming convention:
|
|
@@ -88,7 +87,7 @@ function contractsOf(input) {
|
|
|
88
87
|
return flatten([input]);
|
|
89
88
|
}
|
|
90
89
|
//#endregion
|
|
91
|
-
//#region src/
|
|
90
|
+
//#region src/model/contracts/intercept.ts
|
|
92
91
|
/**
|
|
93
92
|
* The network double in MODULE scope — the same contracts, the same queue and
|
|
94
93
|
* the same strictness as a chain's `.intercept()`, for the one kind of test
|
|
@@ -120,6 +119,38 @@ function declared(requestOrContracts, maybeResponse) {
|
|
|
120
119
|
response: maybeResponse
|
|
121
120
|
}];
|
|
122
121
|
}
|
|
122
|
+
/** Is this argument the options object rather than a response? */
|
|
123
|
+
function isOptions(value) {
|
|
124
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "origin" in value;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve every relative request against `origin` for the life of the scope.
|
|
128
|
+
*
|
|
129
|
+
* The framework owns the seam so a test never has to: this is the one place
|
|
130
|
+
* `fetch` is wrapped, it is put back when the scope ends, and what it does is
|
|
131
|
+
* the one thing a page would have done for free — turn `/api/posts` into a URL.
|
|
132
|
+
*
|
|
133
|
+
* Validating the origin and INSTALLING the wrapper are two steps because they
|
|
134
|
+
* happen at two moments: the refusal of a bad origin belongs before anything
|
|
135
|
+
* starts, while the wrapper may only go on once the engine is listening — it
|
|
136
|
+
* has to sit ON TOP of whatever `fetch` the engine installed, and be taken off
|
|
137
|
+
* before the engine takes its own back.
|
|
138
|
+
*/
|
|
139
|
+
function resolveRelativeAgainst(origin) {
|
|
140
|
+
let base;
|
|
141
|
+
try {
|
|
142
|
+
base = new URL(origin);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`intercept(): \`origin\` states where a relative request resolves — \`${origin}\` is not an absolute URL (e.g. 'http://console.test').`);
|
|
145
|
+
}
|
|
146
|
+
return () => {
|
|
147
|
+
const engine = globalThis.fetch;
|
|
148
|
+
globalThis.fetch = async (input, init) => typeof input === "string" && input.startsWith("/") ? await engine(new URL(input, base).toString(), init) : await engine(input, init);
|
|
149
|
+
return () => {
|
|
150
|
+
globalThis.fetch = engine;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
}
|
|
123
154
|
/**
|
|
124
155
|
* Build the module-scope double on one engine. The node entry hands it msw's
|
|
125
156
|
* server, the page's entry hands it msw's worker: one shape, two transports,
|
|
@@ -128,11 +159,15 @@ function declared(requestOrContracts, maybeResponse) {
|
|
|
128
159
|
* @internal
|
|
129
160
|
*/
|
|
130
161
|
function interceptThrough(register) {
|
|
131
|
-
async function intercept(requestOrContracts,
|
|
132
|
-
const
|
|
162
|
+
async function intercept(requestOrContracts, maybeResponseOrOptions, maybeOptions) {
|
|
163
|
+
const options = isOptions(maybeOptions) ? maybeOptions : isOptions(maybeResponseOrOptions) ? maybeResponseOrOptions : void 0;
|
|
164
|
+
const contracts = declared(requestOrContracts, options === maybeResponseOrOptions ? void 0 : maybeResponseOrOptions);
|
|
133
165
|
if (contracts.length === 0) throw new Error("intercept(): declare at least one contract — an empty list guards nothing, and a subject with no network needs no intercept at all.");
|
|
166
|
+
const installOrigin = options?.origin === void 0 ? void 0 : resolveRelativeAgainst(options.origin);
|
|
134
167
|
const registration = await register(contracts);
|
|
168
|
+
const restoreOrigin = installOrigin?.();
|
|
135
169
|
return { [Symbol.asyncDispose]: async () => {
|
|
170
|
+
restoreOrigin?.();
|
|
136
171
|
const violation = registration.violation();
|
|
137
172
|
registration.cleanup();
|
|
138
173
|
await Promise.resolve();
|
|
@@ -142,7 +177,7 @@ function interceptThrough(register) {
|
|
|
142
177
|
return intercept;
|
|
143
178
|
}
|
|
144
179
|
//#endregion
|
|
145
|
-
//#region src/
|
|
180
|
+
//#region src/model/matching/match.ts
|
|
146
181
|
/** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
|
|
147
182
|
const TOKEN_KINDS = [
|
|
148
183
|
"any",
|
|
@@ -295,7 +330,94 @@ var CaptureScope = class {
|
|
|
295
330
|
}
|
|
296
331
|
};
|
|
297
332
|
//#endregion
|
|
298
|
-
//#region src/
|
|
333
|
+
//#region src/model/result/grep.ts
|
|
334
|
+
/**
|
|
335
|
+
* Extract text blocks from output that contain a pattern.
|
|
336
|
+
* Splits by blank lines (how linter/compiler output is structured),
|
|
337
|
+
* returns only blocks matching the pattern.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
|
|
341
|
+
* expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
|
|
342
|
+
*/
|
|
343
|
+
function grep(output, pattern) {
|
|
344
|
+
return output.replaceAll(/\u001B\[[0-9;]*m/gu, "").split(/\n\s*\n/u).filter((block) => block.includes(pattern)).join("\n\n");
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/model/result/text.ts
|
|
348
|
+
const ANSI_RE = /\u001B\[[0-9;]*m/gu;
|
|
349
|
+
/** Strip ANSI escape sequences from a string. */
|
|
350
|
+
function stripAnsiCodes(value) {
|
|
351
|
+
return value.replace(ANSI_RE, "");
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Read-only accessor for a captured text handle — THE universal one: stdout,
|
|
355
|
+
* stderr, container logs, and file text all surface as a `TextAccessor`.
|
|
356
|
+
*
|
|
357
|
+
* Backed by a primitive string, exposed via {@link text}, but also provides
|
|
358
|
+
* `toString()` / `valueOf()` for string coercion, so common patterns like
|
|
359
|
+
* `String(result.stdout)` and template-literal interpolation still work.
|
|
360
|
+
*
|
|
361
|
+
* ANSI escape sequences are stripped by default before every comparison
|
|
362
|
+
* (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
|
|
363
|
+
* an escape hatch for application noise not covered by the `{{token}}`
|
|
364
|
+
* grammar.
|
|
365
|
+
*
|
|
366
|
+
* Text operations are **closed** over the type: `.grep(pattern)` returns a
|
|
367
|
+
* `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
|
|
368
|
+
* context and the `transform`, so results are chainable
|
|
369
|
+
* (`result.stdout.grep(a).grep(b)`) and snapshot-able
|
|
370
|
+
* (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
|
|
371
|
+
*
|
|
372
|
+
* Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
|
|
373
|
+
* and `expect(result.stdout).toMatch('name.txt')` (resolved against
|
|
374
|
+
* `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
|
|
375
|
+
*/
|
|
376
|
+
var TextAccessor = class TextAccessor {
|
|
377
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
378
|
+
captures;
|
|
379
|
+
/** @internal Stream label used in failure messages ("stdout" / "stderr"). */
|
|
380
|
+
streamName;
|
|
381
|
+
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
382
|
+
testDir;
|
|
383
|
+
/** The raw captured text (never transformed, ANSI preserved). */
|
|
384
|
+
text;
|
|
385
|
+
/** @internal Normaliser applied before comparisons, never to fixtures. */
|
|
386
|
+
transform;
|
|
387
|
+
constructor(value, streamName, testDir, options = {}) {
|
|
388
|
+
this.text = value;
|
|
389
|
+
this.streamName = streamName;
|
|
390
|
+
this.testDir = testDir;
|
|
391
|
+
this.transform = options.transform;
|
|
392
|
+
this.captures = options.captures ?? new CaptureScope();
|
|
393
|
+
}
|
|
394
|
+
/** @internal The text as compared by matchers — ANSI stripped, transform applied. */
|
|
395
|
+
get comparableText() {
|
|
396
|
+
const stripped = stripAnsiCodes(this.text);
|
|
397
|
+
return this.transform ? this.transform(stripped) : stripped;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Keep only the blank-line-separated blocks of the text that contain
|
|
401
|
+
* `pattern` (how linter/compiler output is structured), returned as a new
|
|
402
|
+
* `TextAccessor` — the same subject type, so the result is chainable and
|
|
403
|
+
* snapshot-able. The scalpel for probing large tool outputs; snapshot the
|
|
404
|
+
* whole surface by default, reach for `.grep()` for targeted checks.
|
|
405
|
+
*/
|
|
406
|
+
grep(pattern) {
|
|
407
|
+
return new TextAccessor(grep(this.text, pattern), this.streamName, this.testDir, {
|
|
408
|
+
captures: this.captures,
|
|
409
|
+
transform: this.transform
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
toString() {
|
|
413
|
+
return this.text;
|
|
414
|
+
}
|
|
415
|
+
valueOf() {
|
|
416
|
+
return this.text;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/model/matching/structural.ts
|
|
299
421
|
/**
|
|
300
422
|
* Structural comparison engine shared by every fixture matcher.
|
|
301
423
|
*
|
|
@@ -528,7 +650,7 @@ function textEquals(expected, actual, scope) {
|
|
|
528
650
|
return placeholderStringMatches(expected, actual, scope);
|
|
529
651
|
}
|
|
530
652
|
//#endregion
|
|
531
|
-
//#region src/
|
|
653
|
+
//#region src/model/contracts/queue.ts
|
|
532
654
|
/**
|
|
533
655
|
* The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
|
|
534
656
|
* Both engines consume it: the MSW integration on api/jobs chains, and the
|
|
@@ -701,7 +823,7 @@ var ContractQueue = class {
|
|
|
701
823
|
}
|
|
702
824
|
};
|
|
703
825
|
//#endregion
|
|
704
|
-
//#region src/
|
|
826
|
+
//#region src/model/timing/pause.ts
|
|
705
827
|
/**
|
|
706
828
|
* One answer to "wait" — the only `setTimeout` the model owns.
|
|
707
829
|
*
|
|
@@ -716,7 +838,7 @@ async function pause(ms) {
|
|
|
716
838
|
});
|
|
717
839
|
}
|
|
718
840
|
//#endregion
|
|
719
|
-
//#region src/
|
|
841
|
+
//#region src/model/contracts/stream.ts
|
|
720
842
|
/**
|
|
721
843
|
* A declared stream, made real — ONE serialisation for all three engines.
|
|
722
844
|
*
|
|
@@ -746,7 +868,7 @@ function toReadableStream(body) {
|
|
|
746
868
|
} });
|
|
747
869
|
}
|
|
748
870
|
//#endregion
|
|
749
|
-
//#region src/
|
|
871
|
+
//#region src/model/contracts/types.ts
|
|
750
872
|
/**
|
|
751
873
|
* The marker that tells a STREAMED body from a value that happens to be an
|
|
752
874
|
* object. A body reaches three different engines (msw's server, msw's worker,
|
|
@@ -760,7 +882,7 @@ function isStreamBody(body) {
|
|
|
760
882
|
return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
|
|
761
883
|
}
|
|
762
884
|
//#endregion
|
|
763
|
-
//#region src/
|
|
885
|
+
//#region src/seams/msw/handlers.ts
|
|
764
886
|
/**
|
|
765
887
|
* A contract answers the request that was made, never a copy of an earlier one.
|
|
766
888
|
*
|
|
@@ -866,7 +988,7 @@ async function observe(request) {
|
|
|
866
988
|
};
|
|
867
989
|
}
|
|
868
990
|
//#endregion
|
|
869
|
-
//#region src/
|
|
991
|
+
//#region src/seams/msw/worker.ts
|
|
870
992
|
/**
|
|
871
993
|
* What the runner itself fetches. Everything else a page asks for is the
|
|
872
994
|
* subject's own traffic and answers to its contracts (D7).
|
|
@@ -935,7 +1057,7 @@ function resetWorkerContracts() {
|
|
|
935
1057
|
if (workerInstance !== null) workerInstance.resetHandlers();
|
|
936
1058
|
}
|
|
937
1059
|
//#endregion
|
|
938
|
-
//#region src/
|
|
1060
|
+
//#region src/model/chain/ground.ts
|
|
939
1061
|
/**
|
|
940
1062
|
* The GROUND a row of specs stands on — the naming law of a spec tree.
|
|
941
1063
|
*
|
|
@@ -958,7 +1080,7 @@ function resetWorkerContracts() {
|
|
|
958
1080
|
/** Goldens: every expected fixture, flat, extension included. */
|
|
959
1081
|
const GROUND_EXPECTED = "_expected";
|
|
960
1082
|
//#endregion
|
|
961
|
-
//#region src/
|
|
1083
|
+
//#region src/model/chain/reporter.ts
|
|
962
1084
|
const GREEN = "\x1B[32m";
|
|
963
1085
|
const RED = "\x1B[31m";
|
|
964
1086
|
const RESET = "\x1B[0m";
|
|
@@ -1009,7 +1131,7 @@ function formatStdoutDiff(file, expected, actual, options) {
|
|
|
1009
1131
|
return [...lines, ...rendered].join("\n");
|
|
1010
1132
|
}
|
|
1011
1133
|
//#endregion
|
|
1012
|
-
//#region src/
|
|
1134
|
+
//#region src/model/result/text-assertions.ts
|
|
1013
1135
|
/** The fixture name carries its extension — the name IS the file. */
|
|
1014
1136
|
function requireExtension(name, subject) {
|
|
1015
1137
|
if (!/\.[A-Za-z0-9]+$/u.test(name)) throw new Error(`toMatch("${name}"): the extension is part of the name and is required for ${subject} subjects (e.g. "help.txt").`);
|
|
@@ -1091,94 +1213,7 @@ function isIterable(value) {
|
|
|
1091
1213
|
return value !== null && value !== void 0 && typeof value[Symbol.iterator] === "function";
|
|
1092
1214
|
}
|
|
1093
1215
|
//#endregion
|
|
1094
|
-
//#region src/
|
|
1095
|
-
/**
|
|
1096
|
-
* Extract text blocks from output that contain a pattern.
|
|
1097
|
-
* Splits by blank lines (how linter/compiler output is structured),
|
|
1098
|
-
* returns only blocks matching the pattern.
|
|
1099
|
-
*
|
|
1100
|
-
* @example
|
|
1101
|
-
* expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
|
|
1102
|
-
* expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
|
|
1103
|
-
*/
|
|
1104
|
-
function grep(output, pattern) {
|
|
1105
|
-
return output.replaceAll(/\u001B\[[0-9;]*m/gu, "").split(/\n\s*\n/u).filter((block) => block.includes(pattern)).join("\n\n");
|
|
1106
|
-
}
|
|
1107
|
-
//#endregion
|
|
1108
|
-
//#region src/specification/facets/_common/result/text.ts
|
|
1109
|
-
const ANSI_RE = /\u001B\[[0-9;]*m/gu;
|
|
1110
|
-
/** Strip ANSI escape sequences from a string. */
|
|
1111
|
-
function stripAnsiCodes(value) {
|
|
1112
|
-
return value.replace(ANSI_RE, "");
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Read-only accessor for a captured text handle — THE universal one: stdout,
|
|
1116
|
-
* stderr, container logs, and file text all surface as a `TextAccessor`.
|
|
1117
|
-
*
|
|
1118
|
-
* Backed by a primitive string, exposed via {@link text}, but also provides
|
|
1119
|
-
* `toString()` / `valueOf()` for string coercion, so common patterns like
|
|
1120
|
-
* `String(result.stdout)` and template-literal interpolation still work.
|
|
1121
|
-
*
|
|
1122
|
-
* ANSI escape sequences are stripped by default before every comparison
|
|
1123
|
-
* (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
|
|
1124
|
-
* an escape hatch for application noise not covered by the `{{token}}`
|
|
1125
|
-
* grammar.
|
|
1126
|
-
*
|
|
1127
|
-
* Text operations are **closed** over the type: `.grep(pattern)` returns a
|
|
1128
|
-
* `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
|
|
1129
|
-
* context and the `transform`, so results are chainable
|
|
1130
|
-
* (`result.stdout.grep(a).grep(b)`) and snapshot-able
|
|
1131
|
-
* (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
|
|
1132
|
-
*
|
|
1133
|
-
* Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
|
|
1134
|
-
* and `expect(result.stdout).toMatch('name.txt')` (resolved against
|
|
1135
|
-
* `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
|
|
1136
|
-
*/
|
|
1137
|
-
var TextAccessor = class TextAccessor {
|
|
1138
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
1139
|
-
captures;
|
|
1140
|
-
/** @internal Stream label used in failure messages ("stdout" / "stderr"). */
|
|
1141
|
-
streamName;
|
|
1142
|
-
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
1143
|
-
testDir;
|
|
1144
|
-
/** The raw captured text (never transformed, ANSI preserved). */
|
|
1145
|
-
text;
|
|
1146
|
-
/** @internal Normaliser applied before comparisons, never to fixtures. */
|
|
1147
|
-
transform;
|
|
1148
|
-
constructor(value, streamName, testDir, options = {}) {
|
|
1149
|
-
this.text = value;
|
|
1150
|
-
this.streamName = streamName;
|
|
1151
|
-
this.testDir = testDir;
|
|
1152
|
-
this.transform = options.transform;
|
|
1153
|
-
this.captures = options.captures ?? new CaptureScope();
|
|
1154
|
-
}
|
|
1155
|
-
/** @internal The text as compared by matchers — ANSI stripped, transform applied. */
|
|
1156
|
-
get comparableText() {
|
|
1157
|
-
const stripped = stripAnsiCodes(this.text);
|
|
1158
|
-
return this.transform ? this.transform(stripped) : stripped;
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* Keep only the blank-line-separated blocks of the text that contain
|
|
1162
|
-
* `pattern` (how linter/compiler output is structured), returned as a new
|
|
1163
|
-
* `TextAccessor` — the same subject type, so the result is chainable and
|
|
1164
|
-
* snapshot-able. The scalpel for probing large tool outputs; snapshot the
|
|
1165
|
-
* whole surface by default, reach for `.grep()` for targeted checks.
|
|
1166
|
-
*/
|
|
1167
|
-
grep(pattern) {
|
|
1168
|
-
return new TextAccessor(grep(this.text, pattern), this.streamName, this.testDir, {
|
|
1169
|
-
captures: this.captures,
|
|
1170
|
-
transform: this.transform
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
toString() {
|
|
1174
|
-
return this.text;
|
|
1175
|
-
}
|
|
1176
|
-
valueOf() {
|
|
1177
|
-
return this.text;
|
|
1178
|
-
}
|
|
1179
|
-
};
|
|
1180
|
-
//#endregion
|
|
1181
|
-
//#region src/specification/facets/website/ambiguity.ts
|
|
1216
|
+
//#region src/model/elements/ambiguity.ts
|
|
1182
1217
|
/**
|
|
1183
1218
|
* The ambiguity refusal — CONVENTIONS W3.
|
|
1184
1219
|
*
|
|
@@ -1232,8 +1267,9 @@ const CONTEXT_LANDMARKS = {
|
|
|
1232
1267
|
};
|
|
1233
1268
|
/**
|
|
1234
1269
|
* Render a descriptor as the source that would build it — `link('Articles')`,
|
|
1235
|
-
* `within(navigation(), link('Articles', { exact:
|
|
1236
|
-
* the vocabulary the author wrote, never playwright's
|
|
1270
|
+
* `within(navigation(), link('Articles', { exact: false }))`. The error speaks
|
|
1271
|
+
* the vocabulary the author wrote, never playwright's — so the option appears
|
|
1272
|
+
* only when the AUTHOR stated one, whichever way.
|
|
1237
1273
|
*/
|
|
1238
1274
|
function formatElement(element) {
|
|
1239
1275
|
const bare = formatBare(element);
|
|
@@ -1242,7 +1278,7 @@ function formatElement(element) {
|
|
|
1242
1278
|
function formatBare(element) {
|
|
1243
1279
|
const args = [];
|
|
1244
1280
|
if (element.name !== void 0) args.push(JSON.stringify(element.name));
|
|
1245
|
-
if (element.exact) args.push(
|
|
1281
|
+
if (element.exact !== void 0) args.push(`{ exact: ${String(element.exact)} }`);
|
|
1246
1282
|
return `${CONSTRUCTORS[element.kind]}(${args.join(", ")})`;
|
|
1247
1283
|
}
|
|
1248
1284
|
/** `1. <a href="/articles">Articles</a> in <nav>` — one evidence line per candidate. */
|
|
@@ -1278,7 +1314,7 @@ function formatFixes(element, matches) {
|
|
|
1278
1314
|
const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
|
|
1279
1315
|
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}`);
|
|
1280
1316
|
}
|
|
1281
|
-
if (
|
|
1317
|
+
if (element.exact === false && element.name !== void 0) {
|
|
1282
1318
|
const remaining = matches.filter((match) => match.text === element.name).length;
|
|
1283
1319
|
if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatBare({
|
|
1284
1320
|
...element,
|
|
@@ -1318,11 +1354,109 @@ function describeAmbiguity(options) {
|
|
|
1318
1354
|
"Disambiguate with one of:",
|
|
1319
1355
|
...fixes,
|
|
1320
1356
|
"",
|
|
1321
|
-
"Docs: docs/
|
|
1357
|
+
"Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
|
|
1322
1358
|
].join("\n");
|
|
1323
1359
|
}
|
|
1324
1360
|
//#endregion
|
|
1325
|
-
//#region src/
|
|
1361
|
+
//#region src/model/elements/substring-warning.ts
|
|
1362
|
+
/**
|
|
1363
|
+
* The transitional warning for a name that matches only as a SUBSTRING.
|
|
1364
|
+
*
|
|
1365
|
+
* Until 16.0 a descriptor's name matched any element whose accessible name
|
|
1366
|
+
* CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
|
|
1367
|
+
* test could pass for years against the wrong element without anyone noticing,
|
|
1368
|
+
* because the wrong element was usually next to the right one. Exact is the
|
|
1369
|
+
* default now.
|
|
1370
|
+
*
|
|
1371
|
+
* A lint rule cannot carry this: whether a name matched whole or in part is a
|
|
1372
|
+
* fact about a RUN, and a verdict fed by a previous run's artefact is not
|
|
1373
|
+
* deterministic. So it is a runtime warning, printed by the adapter that did
|
|
1374
|
+
* the looking, at the moment it has both answers in hand — nothing found under
|
|
1375
|
+
* exact, something found under substring.
|
|
1376
|
+
*
|
|
1377
|
+
* It is printed ONCE per descriptor per process: a scenario that clicks the
|
|
1378
|
+
* same link in ten tests has one thing to fix, not ten lines to read.
|
|
1379
|
+
*/
|
|
1380
|
+
/** How long the window lasts, stated in the message itself. */
|
|
1381
|
+
const WINDOW = "through 16.x";
|
|
1382
|
+
/** Descriptors already reported in this process. */
|
|
1383
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1384
|
+
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
1385
|
+
function identityOf(element) {
|
|
1386
|
+
const own = `${element.kind}:${element.name ?? ""}`;
|
|
1387
|
+
return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Report that `element` designated nothing as a whole name, but would have
|
|
1391
|
+
* designated something as a substring. Returns whether a line was printed,
|
|
1392
|
+
* which is what the adapters' own tests assert on.
|
|
1393
|
+
*/
|
|
1394
|
+
function warnSubstringOnly(element, where, found) {
|
|
1395
|
+
const identity = identityOf(element);
|
|
1396
|
+
if (reported.has(identity)) return false;
|
|
1397
|
+
reported.add(identity);
|
|
1398
|
+
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
1399
|
+
console.warn(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
1404
|
+
* would change.
|
|
1405
|
+
*
|
|
1406
|
+
* Every level of the chain is asked the same question the target is asked —
|
|
1407
|
+
* `within(button('Proof of authorship'), content('Verified'))` against a
|
|
1408
|
+
* button named "Proof of authorship Verified" failed on the SCOPE, and a
|
|
1409
|
+
* warning that only ever widened the target printed nothing at all. The chain
|
|
1410
|
+
* is walked outside-in and rebuilt as it goes, since an inner level's answer
|
|
1411
|
+
* is meaningless under a scope that designates nothing.
|
|
1412
|
+
*
|
|
1413
|
+
* A level that states an `exact` of its own is left alone: an author who wrote
|
|
1414
|
+
* `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
|
|
1415
|
+
* was already exact.
|
|
1416
|
+
*
|
|
1417
|
+
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
1418
|
+
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
1419
|
+
* never runs — the test is already over. The probes are counts, which wait for
|
|
1420
|
+
* nothing, and the common case (the name matches whole) costs one per level.
|
|
1421
|
+
*/
|
|
1422
|
+
async function widenedForWindow(element, where, probe) {
|
|
1423
|
+
const chain = [];
|
|
1424
|
+
for (let level = element; level; level = level.scope) chain.unshift(level);
|
|
1425
|
+
let rebuilt;
|
|
1426
|
+
let widened = false;
|
|
1427
|
+
for (const level of chain) {
|
|
1428
|
+
const candidate = {
|
|
1429
|
+
...level,
|
|
1430
|
+
...rebuilt === void 0 ? {} : { scope: rebuilt }
|
|
1431
|
+
};
|
|
1432
|
+
const loose = await widenedLevel(candidate, where, probe);
|
|
1433
|
+
widened ||= loose !== void 0;
|
|
1434
|
+
rebuilt = loose ?? candidate;
|
|
1435
|
+
}
|
|
1436
|
+
return widened ? rebuilt : void 0;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* The one level, widened and warned about, or `undefined` when it designates
|
|
1440
|
+
* something already, states its own `exact`, carries no name, or would
|
|
1441
|
+
* designate nothing either way.
|
|
1442
|
+
*/
|
|
1443
|
+
async function widenedLevel(candidate, where, probe) {
|
|
1444
|
+
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
1445
|
+
try {
|
|
1446
|
+
if (await probe.count(candidate) > 0) return;
|
|
1447
|
+
const loose = {
|
|
1448
|
+
...candidate,
|
|
1449
|
+
exact: false
|
|
1450
|
+
};
|
|
1451
|
+
if (await probe.count(loose) !== 1) return;
|
|
1452
|
+
warnSubstringOnly(candidate, where, await probe.nameOf(loose));
|
|
1453
|
+
return loose;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
//#endregion
|
|
1459
|
+
//#region src/seams/vitest-browser/vitest-browser.adapter.ts
|
|
1326
1460
|
/**
|
|
1327
1461
|
* The browser-mode adapter — the component facet's half of the one element
|
|
1328
1462
|
* vocabulary.
|
|
@@ -1343,6 +1477,29 @@ const MAX_REPORTED_MATCHES = 10;
|
|
|
1343
1477
|
/** The landmark ancestors the ambiguity evidence names. */
|
|
1344
1478
|
const LANDMARKS = "nav, header, footer, main, aside, section, form, [role]";
|
|
1345
1479
|
/**
|
|
1480
|
+
* The roles a FIELD can carry — what `field('Name')` designates.
|
|
1481
|
+
*
|
|
1482
|
+
* A field is named by its ACCESSIBLE name, not by the text of the label
|
|
1483
|
+
* element that happens to wrap it: `<label><span>Channel</span><select>…` has
|
|
1484
|
+
* a label whose text carries every option of the select, while the control's
|
|
1485
|
+
* accessible name is `Channel` — which is what the ARIA tree shows and what a
|
|
1486
|
+
* screen reader reads. Resolving by label text alone made the ordinary
|
|
1487
|
+
* wrapping-label form unreachable under the 16.0 whole-name default.
|
|
1488
|
+
*
|
|
1489
|
+
* The label-text locator stays in the union for the controls ARIA gives no
|
|
1490
|
+
* role at all (a date or colour input), which nothing else would find.
|
|
1491
|
+
*/
|
|
1492
|
+
const FIELD_ROLES = [
|
|
1493
|
+
"checkbox",
|
|
1494
|
+
"combobox",
|
|
1495
|
+
"radio",
|
|
1496
|
+
"searchbox",
|
|
1497
|
+
"slider",
|
|
1498
|
+
"spinbutton",
|
|
1499
|
+
"switch",
|
|
1500
|
+
"textbox"
|
|
1501
|
+
];
|
|
1502
|
+
/**
|
|
1346
1503
|
* Translate a user-facing descriptor into a Browser Mode locator, resolving
|
|
1347
1504
|
* `scope` outside-in so `within(navigation(), link('X'))` searches the nav
|
|
1348
1505
|
* subtree. No `.first()` anywhere: designating exactly one element is
|
|
@@ -1351,9 +1508,16 @@ const LANDMARKS = "nav, header, footer, main, aside, section, form, [role]";
|
|
|
1351
1508
|
*/
|
|
1352
1509
|
function locate(root, element) {
|
|
1353
1510
|
const scope = element.scope ? locate(root, element.scope) : root;
|
|
1354
|
-
const exact = element.exact ??
|
|
1511
|
+
const exact = element.exact ?? true;
|
|
1355
1512
|
const name = element.name ?? "";
|
|
1356
|
-
if (element.kind === "field")
|
|
1513
|
+
if (element.kind === "field") {
|
|
1514
|
+
let field = scope.getByLabelText(name, { exact });
|
|
1515
|
+
for (const role of FIELD_ROLES) field = field.or(scope.getByRole(role, {
|
|
1516
|
+
exact,
|
|
1517
|
+
name
|
|
1518
|
+
}));
|
|
1519
|
+
return field;
|
|
1520
|
+
}
|
|
1357
1521
|
if (element.kind === "testId") return scope.getByTestId(name);
|
|
1358
1522
|
if (element.kind === "text") return scope.getByText(name, { exact });
|
|
1359
1523
|
return scope.getByRole(element.kind, element.name === void 0 ? { exact } : {
|
|
@@ -1385,6 +1549,14 @@ function ambiguousLevel(element) {
|
|
|
1385
1549
|
for (const level of chain.slice(0, -1)) if (locate(page, level).elements().length > 1) return level;
|
|
1386
1550
|
return element;
|
|
1387
1551
|
}
|
|
1552
|
+
/** What the transitional window may ask of the mounted page. */
|
|
1553
|
+
const PROBE = {
|
|
1554
|
+
count: async (element) => await Promise.resolve(locate(page, element).elements().length),
|
|
1555
|
+
nameOf: async (element) => {
|
|
1556
|
+
const [match] = candidates(locate(page, element));
|
|
1557
|
+
return await Promise.resolve(match?.accessibleName ?? match?.text);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1388
1560
|
/**
|
|
1389
1561
|
* Run one visitor action, turning "more than one match" into the W3 refusal.
|
|
1390
1562
|
* The ambiguous LEVEL is identified first: a scope matching several landmarks
|
|
@@ -1392,12 +1564,13 @@ function ambiguousLevel(element) {
|
|
|
1392
1564
|
* wrong descriptor.
|
|
1393
1565
|
*/
|
|
1394
1566
|
async function act(element, action) {
|
|
1395
|
-
const
|
|
1567
|
+
const target = await widenedForWindow(element, globalThis.location.href, PROBE) ?? element;
|
|
1568
|
+
const locator = locate(page, target);
|
|
1396
1569
|
try {
|
|
1397
1570
|
return await action(locator);
|
|
1398
1571
|
} catch (error) {
|
|
1399
1572
|
if (locator.elements().length <= 1) throw error;
|
|
1400
|
-
const culprit = ambiguousLevel(
|
|
1573
|
+
const culprit = ambiguousLevel(target);
|
|
1401
1574
|
const matches = candidates(locate(page, culprit));
|
|
1402
1575
|
const url = globalThis.location.href;
|
|
1403
1576
|
throw new AmbiguousElementError(describeAmbiguity({
|
|
@@ -1584,7 +1757,7 @@ function mountDom(mount) {
|
|
|
1584
1757
|
};
|
|
1585
1758
|
}
|
|
1586
1759
|
//#endregion
|
|
1587
|
-
//#region src/
|
|
1760
|
+
//#region src/seams/vitest-browser/golden.ts
|
|
1588
1761
|
/** Update mode crosses the seam as a provided value, never as an env read. */
|
|
1589
1762
|
function updating() {
|
|
1590
1763
|
return inject("update") === true;
|
|
@@ -1679,7 +1852,7 @@ const nodeOnlyClass = (name) => function NodeOnly() {
|
|
|
1679
1852
|
return refuse(name);
|
|
1680
1853
|
};
|
|
1681
1854
|
//#endregion
|
|
1682
|
-
//#region src/
|
|
1855
|
+
//#region src/seams/vitest-browser/page-runtime.ts
|
|
1683
1856
|
/**
|
|
1684
1857
|
* The runner, as the page reaches it.
|
|
1685
1858
|
*
|
|
@@ -1728,7 +1901,7 @@ function projectWrapper() {
|
|
|
1728
1901
|
return typeof wrap === "function" ? wrap : void 0;
|
|
1729
1902
|
}
|
|
1730
1903
|
//#endregion
|
|
1731
|
-
//#region src/
|
|
1904
|
+
//#region src/facets/component/rendered-text.ts
|
|
1732
1905
|
/**
|
|
1733
1906
|
* A stream captured INSIDE the page.
|
|
1734
1907
|
*
|
|
@@ -1743,7 +1916,7 @@ var RenderedText = class extends TextAccessor {
|
|
|
1743
1916
|
capturedInPage = true;
|
|
1744
1917
|
};
|
|
1745
1918
|
//#endregion
|
|
1746
|
-
//#region src/
|
|
1919
|
+
//#region src/facets/component/component.result.ts
|
|
1747
1920
|
/**
|
|
1748
1921
|
* What a render resolves to — the component as the page saw it, after the
|
|
1749
1922
|
* scenario ran.
|
|
@@ -1773,7 +1946,7 @@ var RenderResult = class {
|
|
|
1773
1946
|
}
|
|
1774
1947
|
};
|
|
1775
1948
|
//#endregion
|
|
1776
|
-
//#region src/
|
|
1949
|
+
//#region src/facets/component/component.chain.ts
|
|
1777
1950
|
/** The console methods a render records; everything else is left alone. */
|
|
1778
1951
|
const CONSOLE_METHODS = [
|
|
1779
1952
|
"debug",
|
|
@@ -1936,91 +2109,11 @@ function chainOf(state) {
|
|
|
1936
2109
|
/** The handle a component test imports. */
|
|
1937
2110
|
const component = chainOf(EMPTY);
|
|
1938
2111
|
//#endregion
|
|
1939
|
-
//#region src/
|
|
1940
|
-
/**
|
|
1941
|
-
* Read-only accessor for a JSON payload parsed from a text stream (stdout).
|
|
1942
|
-
*
|
|
1943
|
-
* Lazily parses the JSON on first use; parse errors are deferred until the
|
|
1944
|
-
* value is read so that callers can still read the raw stream text without
|
|
1945
|
-
* triggering a throw.
|
|
1946
|
-
*
|
|
1947
|
-
* ANSI escape sequences are stripped by default before `JSON.parse`
|
|
1948
|
-
* (CONVENTIONS D6). When a `transform` is configured on the spec runner, it
|
|
1949
|
-
* runs on the stripped text — an escape hatch for application noise not
|
|
1950
|
-
* covered by the `{{token}}` grammar.
|
|
1951
|
-
*
|
|
1952
|
-
* Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
|
|
1953
|
-
* (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
|
|
1954
|
-
*/
|
|
1955
|
-
var JsonAccessor = class {
|
|
1956
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
1957
|
-
captures;
|
|
1958
|
-
/** @internal */
|
|
1959
|
-
rawText;
|
|
1960
|
-
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
1961
|
-
testDir;
|
|
1962
|
-
/** @internal */
|
|
1963
|
-
transform;
|
|
1964
|
-
constructor(rawText, testDir, transform, captures) {
|
|
1965
|
-
this.rawText = rawText;
|
|
1966
|
-
this.testDir = testDir;
|
|
1967
|
-
this.transform = transform;
|
|
1968
|
-
this.captures = captures ?? new CaptureScope();
|
|
1969
|
-
}
|
|
1970
|
-
/**
|
|
1971
|
-
* The parsed JSON value. Throws if the text is not valid JSON.
|
|
1972
|
-
*
|
|
1973
|
-
* `Value` is `unknown` unless the subject that produced this accessor
|
|
1974
|
-
* knows better — `.call<T>()` does, and hands its T straight through so a
|
|
1975
|
-
* one-field reading needs no cast in the test.
|
|
1976
|
-
*/
|
|
1977
|
-
get value() {
|
|
1978
|
-
const stripped = stripAnsiCodes(this.rawText);
|
|
1979
|
-
const source = this.transform ? this.transform(stripped) : stripped;
|
|
1980
|
-
try {
|
|
1981
|
-
return JSON.parse(source);
|
|
1982
|
-
} catch {
|
|
1983
|
-
const preview = source.slice(0, 200);
|
|
1984
|
-
throw new Error(`stdout is not valid JSON: ${preview}`);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
};
|
|
1988
|
-
//#endregion
|
|
1989
|
-
//#region src/specification/facets/_common/result/table.ts
|
|
1990
|
-
/**
|
|
1991
|
-
* Read-only accessor for a database table after a specification run.
|
|
1992
|
-
*
|
|
1993
|
-
* Assertions go through `expect()` (async — they query the database):
|
|
1994
|
-
*
|
|
1995
|
-
* await expect(result.table('users', { database: 'db' })).toMatchRows({
|
|
1996
|
-
* columns: ['name'],
|
|
1997
|
-
* rows: [['Alice']],
|
|
1998
|
-
* });
|
|
1999
|
-
* await expect(result.table('users', { database: 'db' })).toBeEmpty();
|
|
2000
|
-
*/
|
|
2001
|
-
var TableAccessor = class {
|
|
2002
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
2003
|
-
captures;
|
|
2004
|
-
/** @internal */
|
|
2005
|
-
db;
|
|
2006
|
-
/** The table name this accessor reads. */
|
|
2007
|
-
name;
|
|
2008
|
-
constructor(name, db, captures) {
|
|
2009
|
-
this.name = name;
|
|
2010
|
-
this.db = db;
|
|
2011
|
-
this.captures = captures ?? new CaptureScope();
|
|
2012
|
-
}
|
|
2013
|
-
/** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
|
|
2014
|
-
async query(columns) {
|
|
2015
|
-
return await this.db.query(this.name, columns);
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
//#endregion
|
|
2019
|
-
//#region src/specification/facets/website/elements.ts
|
|
2112
|
+
//#region src/model/elements/elements.ts
|
|
2020
2113
|
const named = (kind) => (name, options) => ({
|
|
2021
2114
|
kind,
|
|
2022
2115
|
name,
|
|
2023
|
-
...options?.exact ? {
|
|
2116
|
+
...options?.exact === void 0 ? {} : { exact: options.exact }
|
|
2024
2117
|
});
|
|
2025
2118
|
/** A button (or element with the button role), by accessible name. */
|
|
2026
2119
|
const button = named("button");
|
|
@@ -2040,7 +2133,7 @@ const testId = (id) => ({
|
|
|
2040
2133
|
const landmark = (kind) => (name, options) => ({
|
|
2041
2134
|
kind,
|
|
2042
2135
|
...name === void 0 ? {} : { name },
|
|
2043
|
-
...options?.exact ? {
|
|
2136
|
+
...options?.exact === void 0 ? {} : { exact: options.exact }
|
|
2044
2137
|
});
|
|
2045
2138
|
/** The `banner` landmark — the page header. */
|
|
2046
2139
|
const banner = landmark("banner");
|
|
@@ -2158,7 +2251,7 @@ const valued = (element, value) => ({
|
|
|
2158
2251
|
value
|
|
2159
2252
|
});
|
|
2160
2253
|
//#endregion
|
|
2161
|
-
//#region src/
|
|
2254
|
+
//#region src/model/contracts/filters.ts
|
|
2162
2255
|
/** Does an observed text satisfy a declared {@link TextFilter}? */
|
|
2163
2256
|
function matchesText(filter, actual) {
|
|
2164
2257
|
if (filter === void 0) return true;
|
|
@@ -2167,7 +2260,7 @@ function matchesText(filter, actual) {
|
|
|
2167
2260
|
return structuralEquals(filter, actual, new CaptureScope());
|
|
2168
2261
|
}
|
|
2169
2262
|
//#endregion
|
|
2170
|
-
//#region src/
|
|
2263
|
+
//#region src/seams/anthropic/anthropic.ts
|
|
2171
2264
|
const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
2172
2265
|
function matchesFilter(body, filter) {
|
|
2173
2266
|
if (!matchesText(filter.model, body?.model ?? "")) return false;
|
|
@@ -2258,7 +2351,7 @@ const anthropic = {
|
|
|
2258
2351
|
}
|
|
2259
2352
|
};
|
|
2260
2353
|
//#endregion
|
|
2261
|
-
//#region src/
|
|
2354
|
+
//#region src/model/contracts/http.ts
|
|
2262
2355
|
function wrapJson(data) {
|
|
2263
2356
|
return {
|
|
2264
2357
|
status: 200,
|
|
@@ -2447,7 +2540,7 @@ const http = {
|
|
|
2447
2540
|
}
|
|
2448
2541
|
};
|
|
2449
2542
|
//#endregion
|
|
2450
|
-
//#region src/
|
|
2543
|
+
//#region src/seams/openai/openai.ts
|
|
2451
2544
|
const OPENAI_CHAT_URL = "https://api.openai.com/v1/chat/completions";
|
|
2452
2545
|
const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
|
|
2453
2546
|
function matchesChatFilter(body, filter) {
|
|
@@ -2604,19 +2697,12 @@ const openai = {
|
|
|
2604
2697
|
}
|
|
2605
2698
|
};
|
|
2606
2699
|
//#endregion
|
|
2607
|
-
//#region src/
|
|
2700
|
+
//#region src/model/doubles/mock-of.ts
|
|
2608
2701
|
function mockOf(options) {
|
|
2609
2702
|
return options?.deep === false ? mock() : mockDeep();
|
|
2610
2703
|
}
|
|
2611
2704
|
//#endregion
|
|
2612
|
-
//#region src/
|
|
2613
|
-
/**
|
|
2614
|
-
* Freeze or reset the global Date for deterministic time-dependent tests.
|
|
2615
|
-
* Wraps the `mockdate` package.
|
|
2616
|
-
*/
|
|
2617
|
-
const mockOfDate = MockDatePackage;
|
|
2618
|
-
//#endregion
|
|
2619
|
-
//#region src/vitest/clock.ts
|
|
2705
|
+
//#region src/model/clock/clock.ts
|
|
2620
2706
|
/** The instant `iso` names, or a refusal that says what an instant looks like. */
|
|
2621
2707
|
function instantOf(method, iso) {
|
|
2622
2708
|
const at = new Date(iso);
|
|
@@ -2683,7 +2769,7 @@ const clock = {
|
|
|
2683
2769
|
}
|
|
2684
2770
|
};
|
|
2685
2771
|
//#endregion
|
|
2686
|
-
//#region src/
|
|
2772
|
+
//#region src/model/assertions/required.ts
|
|
2687
2773
|
/**
|
|
2688
2774
|
* The value, or a failure that says what was missing and why it mattered.
|
|
2689
2775
|
*
|
|
@@ -2703,7 +2789,7 @@ function required(value, why) {
|
|
|
2703
2789
|
return value;
|
|
2704
2790
|
}
|
|
2705
2791
|
//#endregion
|
|
2706
|
-
//#region src/
|
|
2792
|
+
//#region src/model/assertions/wait-until.ts
|
|
2707
2793
|
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
2708
2794
|
const DEFAULT_INTERVAL_MS = 50;
|
|
2709
2795
|
/**
|
|
@@ -2768,21 +2854,6 @@ const ProcessHandle = nodeOnlyClass("ProcessHandle");
|
|
|
2768
2854
|
const postgres = () => refuse("postgres()");
|
|
2769
2855
|
const redis = () => refuse("redis()");
|
|
2770
2856
|
const sqlite = () => refuse("sqlite()");
|
|
2771
|
-
const ContainerAccessor = nodeOnlyClass("ContainerAccessor");
|
|
2772
|
-
const findContainersByLabel = () => refuse("findContainersByLabel()");
|
|
2773
|
-
const inspectContainer = () => refuse("inspectContainer()");
|
|
2774
|
-
const removeContainers = () => refuse("removeContainers()");
|
|
2775
|
-
const Orchestrator = nodeOnlyClass("Orchestrator");
|
|
2776
|
-
const BaseResult = nodeOnlyClass("BaseResult");
|
|
2777
|
-
const CallResult = nodeOnlyClass("CallResult");
|
|
2778
|
-
const CliResult = nodeOnlyClass("CliResult");
|
|
2779
|
-
const DirectoryAccessor = nodeOnlyClass("DirectoryAccessor");
|
|
2780
|
-
const FetchResult = nodeOnlyClass("FetchResult");
|
|
2781
|
-
const FilesystemAccessor = nodeOnlyClass("FilesystemAccessor");
|
|
2782
|
-
const HttpResult = nodeOnlyClass("HttpResult");
|
|
2783
|
-
const PageResult = nodeOnlyClass("PageResult");
|
|
2784
|
-
const ResponseAccessor = nodeOnlyClass("ResponseAccessor");
|
|
2785
|
-
const ScreenResult = nodeOnlyClass("ScreenResult");
|
|
2786
2857
|
/**
|
|
2787
2858
|
* `text()` is real here: only its anchor differs. Under node the accessor
|
|
2788
2859
|
* remembers the caller's directory so a golden resolves against it; in a page
|
|
@@ -2793,4 +2864,4 @@ function text(value) {
|
|
|
2793
2864
|
return new TextAccessor(value, "text", "");
|
|
2794
2865
|
}
|
|
2795
2866
|
//#endregion
|
|
2796
|
-
export {
|
|
2867
|
+
export { Matcher, ProcessHandle, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineContract, defineContracts, dialog, disabled, enabled, field, focused, form, heading, http, intercept, link, listitem, main, match, mockOf, navigation, openai, option, postgres, process, redis, region, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
|