@jterrazz/test 16.0.0 → 16.1.1
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/dist/appium.adapter.js +59 -4
- package/dist/browser/index.js +203 -42
- package/dist/checker.js +305 -63
- package/dist/index.d.ts +35 -4
- package/dist/index.js +47 -24
- package/dist/oxlint.cjs +123 -28
- package/dist/oxlint.js +123 -28
- package/dist/playwright.adapter.js +88 -50
- package/dist/substring-warning.js +282 -0
- package/dist/vitest.js +91 -12
- 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 +248 -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 +332 -0
- package/docs/19-linting.md +512 -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 +3 -2
- package/dist/ambiguity.js +0 -146
package/dist/appium.adapter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as AmbiguousElementError, n as warnSubstringOnly, o as formatElement } from "./substring-warning.js";
|
|
2
2
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -211,6 +211,16 @@ function compilePredicate(element, options) {
|
|
|
211
211
|
default: throw new Error(`${formatElement(element)}: landmarks are a website concept — an iOS screen has no '${element.kind}' region. Scope with within(testId('…'), …) instead.`);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Every element a predicate designates under `scope`.
|
|
216
|
+
*
|
|
217
|
+
* ONE place awaits webdriverio's query: `$$` answers a chainable typed as the
|
|
218
|
+
* array it resolves to, so the await that makes the array real reads to the
|
|
219
|
+
* typechecker as an await of a non-promise.
|
|
220
|
+
*/
|
|
221
|
+
async function matching(scope, element, options) {
|
|
222
|
+
return await scope.$$(`-ios predicate string:${compilePredicate(element, options)}`);
|
|
223
|
+
}
|
|
214
224
|
/** The outside-in scope chain of a descriptor, ending on the target itself. */
|
|
215
225
|
function scopeChain(element) {
|
|
216
226
|
const chain = [];
|
|
@@ -341,10 +351,16 @@ var AppiumAdapter = class {
|
|
|
341
351
|
const chain = scopeChain(element);
|
|
342
352
|
const deadline = Date.now() + this.actionTimeoutMs;
|
|
343
353
|
let scrollAttempted = false;
|
|
354
|
+
let windowAsked = false;
|
|
355
|
+
let looking = chain;
|
|
344
356
|
for (;;) {
|
|
345
|
-
const resolved = await this.resolveChain(driver,
|
|
357
|
+
const resolved = await this.resolveChain(driver, looking, cardinality, { tryScroll: !scrollAttempted });
|
|
346
358
|
scrollAttempted = true;
|
|
347
359
|
if (resolved) return resolved;
|
|
360
|
+
if (!windowAsked) {
|
|
361
|
+
windowAsked = true;
|
|
362
|
+
looking = await this.widenedChain(driver, chain) ?? chain;
|
|
363
|
+
}
|
|
348
364
|
if (Date.now() > deadline) throw await this.timeoutError(driver, element, verb);
|
|
349
365
|
await delay(POLL_INTERVAL_MS);
|
|
350
366
|
}
|
|
@@ -353,11 +369,11 @@ var AppiumAdapter = class {
|
|
|
353
369
|
async resolveChain(driver, chain, cardinality, options) {
|
|
354
370
|
let scope = driver;
|
|
355
371
|
for (const [index, level] of chain.entries()) {
|
|
356
|
-
const matches = await scope
|
|
372
|
+
const matches = await matching(scope, level);
|
|
357
373
|
const count = await matches.length;
|
|
358
374
|
if (count === 0) {
|
|
359
375
|
if (options?.tryScroll) {
|
|
360
|
-
const first = (await scope
|
|
376
|
+
const first = (await matching(scope, level, { anyVisibility: true }))[0];
|
|
361
377
|
if (first !== void 0) try {
|
|
362
378
|
await driver.executeScript("mobile: scroll", [{
|
|
363
379
|
elementId: first.elementId,
|
|
@@ -378,6 +394,45 @@ var AppiumAdapter = class {
|
|
|
378
394
|
}
|
|
379
395
|
return scope;
|
|
380
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* The chain to keep polling with, level by level: a descriptor that
|
|
399
|
+
* designated nothing as a whole label but designates exactly one thing as a
|
|
400
|
+
* SUBSTRING is widened, with the warning that names the label to write. The
|
|
401
|
+
* window is the vocabulary's, not one surface's — chapter 13 — so the
|
|
402
|
+
* screen answers it exactly as a page does.
|
|
403
|
+
*
|
|
404
|
+
* `null` when nothing would change, which is the ordinary wait.
|
|
405
|
+
*/
|
|
406
|
+
async widenedChain(driver, chain) {
|
|
407
|
+
let scope = driver;
|
|
408
|
+
const widened = [];
|
|
409
|
+
let changed = false;
|
|
410
|
+
for (const level of chain) {
|
|
411
|
+
const found = await this.widenedLevel(scope, level);
|
|
412
|
+
changed ||= found !== null;
|
|
413
|
+
const candidate = found ?? level;
|
|
414
|
+
const next = (await matching(scope, candidate))[0];
|
|
415
|
+
if (next === void 0) return null;
|
|
416
|
+
scope = next;
|
|
417
|
+
widened.push(candidate);
|
|
418
|
+
}
|
|
419
|
+
return changed ? widened : null;
|
|
420
|
+
}
|
|
421
|
+
/** One level of the chain, widened and warned about, or `null`. */
|
|
422
|
+
async widenedLevel(scope, level) {
|
|
423
|
+
if (level.exact !== void 0 || level.name === void 0) return null;
|
|
424
|
+
if (await (await matching(scope, level)).length > 0) return null;
|
|
425
|
+
const loose = {
|
|
426
|
+
...level,
|
|
427
|
+
exact: false
|
|
428
|
+
};
|
|
429
|
+
const matches = await matching(scope, loose);
|
|
430
|
+
const first = matches[0];
|
|
431
|
+
if (first === void 0 || await matches.length !== 1) return null;
|
|
432
|
+
const evidence = await captureMatch(first);
|
|
433
|
+
await warnSubstringOnly(level, this.options.udid, evidence.label);
|
|
434
|
+
return loose;
|
|
435
|
+
}
|
|
381
436
|
/** Enumerate the candidates' evidence, truncated. */
|
|
382
437
|
async captureMatches(matches) {
|
|
383
438
|
const count = Math.min(await matches.length, MAX_REPORTED_MATCHES);
|
package/dist/browser/index.js
CHANGED
|
@@ -129,6 +129,12 @@ function isOptions(value) {
|
|
|
129
129
|
* The framework owns the seam so a test never has to: this is the one place
|
|
130
130
|
* `fetch` is wrapped, it is put back when the scope ends, and what it does is
|
|
131
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.
|
|
132
138
|
*/
|
|
133
139
|
function resolveRelativeAgainst(origin) {
|
|
134
140
|
let base;
|
|
@@ -137,10 +143,12 @@ function resolveRelativeAgainst(origin) {
|
|
|
137
143
|
} catch {
|
|
138
144
|
throw new Error(`intercept(): \`origin\` states where a relative request resolves — \`${origin}\` is not an absolute URL (e.g. 'http://console.test').`);
|
|
139
145
|
}
|
|
140
|
-
const original = globalThis.fetch;
|
|
141
|
-
globalThis.fetch = async (input, init) => typeof input === "string" && input.startsWith("/") ? await original(new URL(input, base).toString(), init) : await original(input, init);
|
|
142
146
|
return () => {
|
|
143
|
-
globalThis.fetch
|
|
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
|
+
};
|
|
144
152
|
};
|
|
145
153
|
}
|
|
146
154
|
/**
|
|
@@ -155,18 +163,13 @@ function interceptThrough(register) {
|
|
|
155
163
|
const options = isOptions(maybeOptions) ? maybeOptions : isOptions(maybeResponseOrOptions) ? maybeResponseOrOptions : void 0;
|
|
156
164
|
const contracts = declared(requestOrContracts, options === maybeResponseOrOptions ? void 0 : maybeResponseOrOptions);
|
|
157
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.");
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
registration = await register(contracts);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
restoreOrigin?.();
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
+
const installOrigin = options?.origin === void 0 ? void 0 : resolveRelativeAgainst(options.origin);
|
|
167
|
+
const registration = await register(contracts);
|
|
168
|
+
const restoreOrigin = installOrigin?.();
|
|
166
169
|
return { [Symbol.asyncDispose]: async () => {
|
|
170
|
+
restoreOrigin?.();
|
|
167
171
|
const violation = registration.violation();
|
|
168
172
|
registration.cleanup();
|
|
169
|
-
restoreOrigin?.();
|
|
170
173
|
await Promise.resolve();
|
|
171
174
|
if (violation) throw violation;
|
|
172
175
|
} };
|
|
@@ -1355,6 +1358,45 @@ function describeAmbiguity(options) {
|
|
|
1355
1358
|
].join("\n");
|
|
1356
1359
|
}
|
|
1357
1360
|
//#endregion
|
|
1361
|
+
//#region src/model/elements/aria-name.ts
|
|
1362
|
+
/**
|
|
1363
|
+
* The accessible name an ARIA snapshot states.
|
|
1364
|
+
*
|
|
1365
|
+
* A descriptor matches the name the BROWSER computes, and that name is not the
|
|
1366
|
+
* element's text: `<button><span>Experiments</span><span>9</span></button>`
|
|
1367
|
+
* reads as `Experiments9` through `textContent` and is named "Experiments 9",
|
|
1368
|
+
* a field's name comes from its label and is nowhere in its text at all, and a
|
|
1369
|
+
* name cut at eighty characters resolves to nothing. Reading the name off the
|
|
1370
|
+
* snapshot is what makes the spelling the window prints a spelling that can be
|
|
1371
|
+
* written back into a spec.
|
|
1372
|
+
*
|
|
1373
|
+
* The dialect is the one both surfaces already speak — playwright's, on the
|
|
1374
|
+
* page directly and through `server.commands` for a mounted component — so the
|
|
1375
|
+
* projection is stated once, here, and neither adapter computes a name itself.
|
|
1376
|
+
*/
|
|
1377
|
+
/**
|
|
1378
|
+
* The name of the FIRST node a snapshot describes: `- button "Experiments 9"`,
|
|
1379
|
+
* optionally followed by `:` and its children.
|
|
1380
|
+
*
|
|
1381
|
+
* A quote must follow the role across a SPACE. `- paragraph: "…"` is a node
|
|
1382
|
+
* with text and no name, and reading that text as a name would hand the author
|
|
1383
|
+
* a spelling no role descriptor matches on.
|
|
1384
|
+
*/
|
|
1385
|
+
const NAMED = /^-\s+[\w-]+\s+"(?<name>(?:[^"\\]|\\.)*)"/u;
|
|
1386
|
+
/** Undo the escaping the snapshot applies to a quote or a backslash inside a name. */
|
|
1387
|
+
function unescaped(name) {
|
|
1388
|
+
return name.replaceAll(/\\(?<char>.)/gu, "$<char>");
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* The accessible name the snapshot gives the node it describes, or `undefined`
|
|
1392
|
+
* when that node has none — an unnamed landmark, a paragraph of text.
|
|
1393
|
+
*/
|
|
1394
|
+
function accessibleNameIn(snapshot) {
|
|
1395
|
+
const [first] = snapshot.split("\n");
|
|
1396
|
+
const name = (first === void 0 ? null : NAMED.exec(first.trim()))?.groups?.name;
|
|
1397
|
+
return name === void 0 ? void 0 : unescaped(name);
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
1358
1400
|
//#region src/model/elements/substring-warning.ts
|
|
1359
1401
|
/**
|
|
1360
1402
|
* The transitional warning for a name that matches only as a SUBSTRING.
|
|
@@ -1374,8 +1416,16 @@ function describeAmbiguity(options) {
|
|
|
1374
1416
|
* It is printed ONCE per descriptor per process: a scenario that clicks the
|
|
1375
1417
|
* same link in ten tests has one thing to fix, not ten lines to read.
|
|
1376
1418
|
*/
|
|
1377
|
-
/** How long the
|
|
1378
|
-
const WINDOW = "
|
|
1419
|
+
/** How long the window lasts, stated in the message itself. */
|
|
1420
|
+
const WINDOW = "through 16.x";
|
|
1421
|
+
/** The node side's printer — the default every adapter but the page's uses. */
|
|
1422
|
+
function toStandardError(line) {
|
|
1423
|
+
if (typeof process === "undefined") {
|
|
1424
|
+
console.warn(line);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
process.stderr.write(`${line}\n`);
|
|
1428
|
+
}
|
|
1379
1429
|
/** Descriptors already reported in this process. */
|
|
1380
1430
|
const reported = /* @__PURE__ */ new Set();
|
|
1381
1431
|
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
@@ -1388,13 +1438,100 @@ function identityOf(element) {
|
|
|
1388
1438
|
* designated something as a substring. Returns whether a line was printed,
|
|
1389
1439
|
* which is what the adapters' own tests assert on.
|
|
1390
1440
|
*/
|
|
1391
|
-
function warnSubstringOnly(element, where) {
|
|
1441
|
+
async function warnSubstringOnly(element, where, found, print = toStandardError) {
|
|
1392
1442
|
const identity = identityOf(element);
|
|
1393
1443
|
if (reported.has(identity)) return false;
|
|
1394
1444
|
reported.add(identity);
|
|
1395
|
-
|
|
1445
|
+
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
1446
|
+
await print(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
|
|
1396
1447
|
return true;
|
|
1397
1448
|
}
|
|
1449
|
+
/**
|
|
1450
|
+
* How long a descriptor is given to ARRIVE before the window concludes it is
|
|
1451
|
+
* absent — the budget `settles` spends and no more.
|
|
1452
|
+
*
|
|
1453
|
+
* A count is one observation, and a verb called right after a navigating
|
|
1454
|
+
* `click()` takes it on the page being LEFT. The window may not decide
|
|
1455
|
+
* "nothing matches this whole name" on that reading: the destination has not
|
|
1456
|
+
* been parsed yet. Long enough for a navigation to commit, far short of the
|
|
1457
|
+
* verb's own 30 s, and paid only when there is a substring to widen to.
|
|
1458
|
+
*/
|
|
1459
|
+
const WINDOW_SETTLE_MS = 5e3;
|
|
1460
|
+
/**
|
|
1461
|
+
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
1462
|
+
* would change.
|
|
1463
|
+
*
|
|
1464
|
+
* Every level of the chain is asked the same question the target is asked —
|
|
1465
|
+
* `within(button('Proof of authorship'), content('Verified'))` against a
|
|
1466
|
+
* button named "Proof of authorship Verified" failed on the SCOPE, and a
|
|
1467
|
+
* warning that only ever widened the target printed nothing at all. The chain
|
|
1468
|
+
* is walked outside-in and rebuilt as it goes, since an inner level's answer
|
|
1469
|
+
* is meaningless under a scope that designates nothing.
|
|
1470
|
+
*
|
|
1471
|
+
* A level that states an `exact` of its own is left alone: an author who wrote
|
|
1472
|
+
* `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
|
|
1473
|
+
* was already exact.
|
|
1474
|
+
*
|
|
1475
|
+
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
1476
|
+
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
1477
|
+
* never runs — the test is already over. The common case (the name matches
|
|
1478
|
+
* whole) costs one count per level, which waits for nothing.
|
|
1479
|
+
*/
|
|
1480
|
+
async function widenedForWindow(element, where, probe) {
|
|
1481
|
+
const chain = [];
|
|
1482
|
+
for (let level = element; level; level = level.scope) chain.unshift(level);
|
|
1483
|
+
let rebuilt;
|
|
1484
|
+
let widened = false;
|
|
1485
|
+
for (const level of chain) {
|
|
1486
|
+
const candidate = {
|
|
1487
|
+
...level,
|
|
1488
|
+
...rebuilt === void 0 ? {} : { scope: rebuilt }
|
|
1489
|
+
};
|
|
1490
|
+
const loose = await widenedLevel(candidate, where, probe);
|
|
1491
|
+
widened ||= loose !== void 0;
|
|
1492
|
+
rebuilt = loose ?? candidate;
|
|
1493
|
+
}
|
|
1494
|
+
return widened ? rebuilt : void 0;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* The one level, widened and warned about, or `undefined` when it designates
|
|
1498
|
+
* something already, states its own `exact`, carries no name, would designate
|
|
1499
|
+
* nothing either way, or turns out to be exact on the surface that ARRIVES.
|
|
1500
|
+
*
|
|
1501
|
+
* The last is the navigation case, and it is the reason the verdict is not two
|
|
1502
|
+
* counts: a `see()` fired right after a navigating `click()` counts the page
|
|
1503
|
+
* being left, so a descriptor that is perfectly exact on the DESTINATION read
|
|
1504
|
+
* as substring-only — non-deterministically, since which page each count
|
|
1505
|
+
* landed on was a race. A widening is therefore only decided once the exact
|
|
1506
|
+
* descriptor has been given the settle budget and still designates nothing.
|
|
1507
|
+
* The tell of the old fault was the printed name equalling the name written.
|
|
1508
|
+
*/
|
|
1509
|
+
async function widenedLevel(candidate, where, probe) {
|
|
1510
|
+
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
1511
|
+
try {
|
|
1512
|
+
if (await probe.count(candidate) > 0) return;
|
|
1513
|
+
const loose = {
|
|
1514
|
+
...candidate,
|
|
1515
|
+
exact: false
|
|
1516
|
+
};
|
|
1517
|
+
if (await probe.count(loose) !== 1) return;
|
|
1518
|
+
if (!alreadyJudged(candidate) && await probe.settles(candidate)) return;
|
|
1519
|
+
await warnSubstringOnly(candidate, where, await probe.nameOf(loose), probe.print);
|
|
1520
|
+
return loose;
|
|
1521
|
+
} catch {
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Has this descriptor already been judged substring-only in this run?
|
|
1527
|
+
*
|
|
1528
|
+
* The verdict is made once: a scenario that clicks the same link in ten tests
|
|
1529
|
+
* pays the settle budget on the first of them and is widened straight away on
|
|
1530
|
+
* the nine after it.
|
|
1531
|
+
*/
|
|
1532
|
+
function alreadyJudged(element) {
|
|
1533
|
+
return reported.has(identityOf(element));
|
|
1534
|
+
}
|
|
1398
1535
|
//#endregion
|
|
1399
1536
|
//#region src/seams/vitest-browser/vitest-browser.adapter.ts
|
|
1400
1537
|
/**
|
|
@@ -1490,21 +1627,63 @@ function ambiguousLevel(element) {
|
|
|
1490
1627
|
return element;
|
|
1491
1628
|
}
|
|
1492
1629
|
/**
|
|
1630
|
+
* The name to WRITE for this descriptor — the whole one, never truncated.
|
|
1631
|
+
*
|
|
1632
|
+
* A role descriptor matches the accessible name the browser computed, and a
|
|
1633
|
+
* page cannot compute one: the snapshot comes back from the node side, of the
|
|
1634
|
+
* one node the locator's own selector names. Text content would be the wrong
|
|
1635
|
+
* answer — it glues inline children with no separator and carries nothing of a
|
|
1636
|
+
* field's label — except for a `text` descriptor, which matches text.
|
|
1637
|
+
*/
|
|
1638
|
+
async function wholeNameOf(element) {
|
|
1639
|
+
const locator = locate(page, element);
|
|
1640
|
+
if (element.kind !== "text") try {
|
|
1641
|
+
const named = accessibleNameIn(await commands.ariaNode(locator.selector));
|
|
1642
|
+
if (named !== void 0) return named;
|
|
1643
|
+
} catch {}
|
|
1644
|
+
const collapsed = (locator.query()?.textContent ?? "").replaceAll(/\s+/gu, " ").trim();
|
|
1645
|
+
return collapsed === "" ? void 0 : collapsed;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Does the descriptor arrive if the subject is given the settle budget?
|
|
1649
|
+
*
|
|
1650
|
+
* A mounted component never navigates, but it does re-render: a subject whose
|
|
1651
|
+
* name arrives with the state after a click is the same question a page asks
|
|
1652
|
+
* across a navigation, and one window answers both. The poll is vitest's own,
|
|
1653
|
+
* with the window's budget rather than the test's — what is being waited for
|
|
1654
|
+
* here is a surface settling, not an assertion coming true.
|
|
1655
|
+
*/
|
|
1656
|
+
async function settles(element) {
|
|
1657
|
+
try {
|
|
1658
|
+
await expect.poll(() => locate(page, element).elements().length, { timeout: WINDOW_SETTLE_MS }).toBeGreaterThan(0);
|
|
1659
|
+
return true;
|
|
1660
|
+
} catch {
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
/** What the transitional window may ask of the mounted page. */
|
|
1665
|
+
const PROBE = {
|
|
1666
|
+
count: async (element) => await Promise.resolve(locate(page, element).elements().length),
|
|
1667
|
+
nameOf: async (element) => await wholeNameOf(element),
|
|
1668
|
+
print: async (line) => {
|
|
1669
|
+
await commands.notify(line);
|
|
1670
|
+
},
|
|
1671
|
+
settles: async (element) => await settles(element)
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1493
1674
|
* Run one visitor action, turning "more than one match" into the W3 refusal.
|
|
1494
1675
|
* The ambiguous LEVEL is identified first: a scope matching several landmarks
|
|
1495
1676
|
* is the real fault, and naming the target would send the author to fix the
|
|
1496
1677
|
* wrong descriptor.
|
|
1497
1678
|
*/
|
|
1498
1679
|
async function act(element, action) {
|
|
1499
|
-
const
|
|
1680
|
+
const target = await widenedForWindow(element, globalThis.location.href, PROBE) ?? element;
|
|
1681
|
+
const locator = locate(page, target);
|
|
1500
1682
|
try {
|
|
1501
1683
|
return await action(locator);
|
|
1502
1684
|
} catch (error) {
|
|
1503
|
-
if (locator.elements().length <= 1)
|
|
1504
|
-
|
|
1505
|
-
throw error;
|
|
1506
|
-
}
|
|
1507
|
-
const culprit = ambiguousLevel(element);
|
|
1685
|
+
if (locator.elements().length <= 1) throw error;
|
|
1686
|
+
const culprit = ambiguousLevel(target);
|
|
1508
1687
|
const matches = candidates(locate(page, culprit));
|
|
1509
1688
|
const url = globalThis.location.href;
|
|
1510
1689
|
throw new AmbiguousElementError(describeAmbiguity({
|
|
@@ -1515,24 +1694,6 @@ async function act(element, action) {
|
|
|
1515
1694
|
}
|
|
1516
1695
|
}
|
|
1517
1696
|
/**
|
|
1518
|
-
* Say so when a descriptor found NOTHING as a whole name but would have found
|
|
1519
|
-
* something as a substring — the one shape the whole-name default changes.
|
|
1520
|
-
*
|
|
1521
|
-
* Asked only on the failure path, and only for a descriptor that stated no
|
|
1522
|
-
* `exact` of its own: an author who wrote `{ exact: false }` chose the
|
|
1523
|
-
* substring, and one who wrote `{ exact: true }` was already exact.
|
|
1524
|
-
*/
|
|
1525
|
-
function reportSubstringOnly(element) {
|
|
1526
|
-
if (element.exact !== void 0 || element.name === void 0) return;
|
|
1527
|
-
try {
|
|
1528
|
-
if (locate(page, element).elements().length > 0) return;
|
|
1529
|
-
if (locate(page, {
|
|
1530
|
-
...element,
|
|
1531
|
-
exact: false
|
|
1532
|
-
}).elements().length > 0) warnSubstringOnly(element, globalThis.location.href);
|
|
1533
|
-
} catch {}
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
1697
|
* Is this descriptor answered by PRESENCE rather than by visibility?
|
|
1537
1698
|
*
|
|
1538
1699
|
* The options of a collapsed `<select>` are in the document and in the
|
|
@@ -2801,7 +2962,7 @@ const specification = {
|
|
|
2801
2962
|
mobile: () => refuse("specification.mobile()"),
|
|
2802
2963
|
website: () => refuse("specification.website()")
|
|
2803
2964
|
};
|
|
2804
|
-
const process = () => refuse("process()");
|
|
2965
|
+
const process$1 = () => refuse("process()");
|
|
2805
2966
|
const ProcessHandle = nodeOnlyClass("ProcessHandle");
|
|
2806
2967
|
const postgres = () => refuse("postgres()");
|
|
2807
2968
|
const redis = () => refuse("redis()");
|
|
@@ -2816,4 +2977,4 @@ function text(value) {
|
|
|
2816
2977
|
return new TextAccessor(value, "text", "");
|
|
2817
2978
|
}
|
|
2818
2979
|
//#endregion
|
|
2819
|
-
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 };
|
|
2980
|
+
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$1 as process, redis, region, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
|