@jterrazz/test 16.0.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/dist/appium.adapter.js +59 -4
- package/dist/browser/index.js +88 -40
- package/dist/checker.js +272 -63
- package/dist/index.d.ts +33 -4
- package/dist/index.js +47 -24
- package/dist/oxlint.cjs +100 -27
- package/dist/oxlint.js +100 -27
- package/dist/playwright.adapter.js +14 -65
- package/dist/{ambiguity.js → substring-warning.js} +99 -1
- package/dist/vitest.js +71 -11
- 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 +2 -1
package/dist/appium.adapter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as formatElement, r as AmbiguousElementError, t as warnSubstringOnly } 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
|
+
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
|
} };
|
|
@@ -1374,8 +1377,8 @@ function describeAmbiguity(options) {
|
|
|
1374
1377
|
* It is printed ONCE per descriptor per process: a scenario that clicks the
|
|
1375
1378
|
* same link in ten tests has one thing to fix, not ten lines to read.
|
|
1376
1379
|
*/
|
|
1377
|
-
/** How long the
|
|
1378
|
-
const WINDOW = "
|
|
1380
|
+
/** How long the window lasts, stated in the message itself. */
|
|
1381
|
+
const WINDOW = "through 16.x";
|
|
1379
1382
|
/** Descriptors already reported in this process. */
|
|
1380
1383
|
const reported = /* @__PURE__ */ new Set();
|
|
1381
1384
|
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
@@ -1388,13 +1391,70 @@ function identityOf(element) {
|
|
|
1388
1391
|
* designated something as a substring. Returns whether a line was printed,
|
|
1389
1392
|
* which is what the adapters' own tests assert on.
|
|
1390
1393
|
*/
|
|
1391
|
-
function warnSubstringOnly(element, where) {
|
|
1394
|
+
function warnSubstringOnly(element, where, found) {
|
|
1392
1395
|
const identity = identityOf(element);
|
|
1393
1396
|
if (reported.has(identity)) return false;
|
|
1394
1397
|
reported.add(identity);
|
|
1395
|
-
|
|
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.`);
|
|
1396
1400
|
return true;
|
|
1397
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
|
+
}
|
|
1398
1458
|
//#endregion
|
|
1399
1459
|
//#region src/seams/vitest-browser/vitest-browser.adapter.ts
|
|
1400
1460
|
/**
|
|
@@ -1489,6 +1549,14 @@ function ambiguousLevel(element) {
|
|
|
1489
1549
|
for (const level of chain.slice(0, -1)) if (locate(page, level).elements().length > 1) return level;
|
|
1490
1550
|
return element;
|
|
1491
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
|
+
};
|
|
1492
1560
|
/**
|
|
1493
1561
|
* Run one visitor action, turning "more than one match" into the W3 refusal.
|
|
1494
1562
|
* The ambiguous LEVEL is identified first: a scope matching several landmarks
|
|
@@ -1496,15 +1564,13 @@ function ambiguousLevel(element) {
|
|
|
1496
1564
|
* wrong descriptor.
|
|
1497
1565
|
*/
|
|
1498
1566
|
async function act(element, action) {
|
|
1499
|
-
const
|
|
1567
|
+
const target = await widenedForWindow(element, globalThis.location.href, PROBE) ?? element;
|
|
1568
|
+
const locator = locate(page, target);
|
|
1500
1569
|
try {
|
|
1501
1570
|
return await action(locator);
|
|
1502
1571
|
} catch (error) {
|
|
1503
|
-
if (locator.elements().length <= 1)
|
|
1504
|
-
|
|
1505
|
-
throw error;
|
|
1506
|
-
}
|
|
1507
|
-
const culprit = ambiguousLevel(element);
|
|
1572
|
+
if (locator.elements().length <= 1) throw error;
|
|
1573
|
+
const culprit = ambiguousLevel(target);
|
|
1508
1574
|
const matches = candidates(locate(page, culprit));
|
|
1509
1575
|
const url = globalThis.location.href;
|
|
1510
1576
|
throw new AmbiguousElementError(describeAmbiguity({
|
|
@@ -1515,24 +1581,6 @@ async function act(element, action) {
|
|
|
1515
1581
|
}
|
|
1516
1582
|
}
|
|
1517
1583
|
/**
|
|
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
1584
|
* Is this descriptor answered by PRESENCE rather than by visibility?
|
|
1537
1585
|
*
|
|
1538
1586
|
* The options of a collapsed `<select>` are in the document and in the
|