@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.
Files changed (210) hide show
  1. package/dist/appium.adapter.js +59 -4
  2. package/dist/browser/index.js +88 -40
  3. package/dist/checker.js +272 -63
  4. package/dist/index.d.ts +33 -4
  5. package/dist/index.js +47 -24
  6. package/dist/oxlint.cjs +100 -27
  7. package/dist/oxlint.js +100 -27
  8. package/dist/playwright.adapter.js +14 -65
  9. package/dist/{ambiguity.js → substring-warning.js} +99 -1
  10. package/dist/vitest.js +71 -11
  11. package/docs/01-architecture.md +95 -0
  12. package/docs/02-developing.md +351 -0
  13. package/docs/03-testing.md +495 -0
  14. package/docs/04-operating.md +101 -0
  15. package/docs/05-module-tests.md +152 -0
  16. package/docs/06-integration.md +180 -0
  17. package/docs/07-component.md +246 -0
  18. package/docs/08-website.md +404 -0
  19. package/docs/09-mobile.md +270 -0
  20. package/docs/10-api.md +264 -0
  21. package/docs/11-jobs.md +157 -0
  22. package/docs/12-cli.md +677 -0
  23. package/docs/13-elements.md +242 -0
  24. package/docs/14-assertions.md +314 -0
  25. package/docs/15-tokens.md +253 -0
  26. package/docs/16-contracts.md +438 -0
  27. package/docs/17-services.md +188 -0
  28. package/docs/18-conventions.md +305 -0
  29. package/docs/19-linting.md +510 -0
  30. package/docs/README.md +36 -0
  31. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  32. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  33. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  34. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  35. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  36. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  37. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  38. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  39. package/docs/decisions/_template.md +22 -0
  40. package/docs/reference/classes/Matcher.md +120 -0
  41. package/docs/reference/classes/ProcessHandle.md +368 -0
  42. package/docs/reference/functions/defineContract.md +39 -0
  43. package/docs/reference/functions/defineContracts.md +36 -0
  44. package/docs/reference/functions/disabled.md +28 -0
  45. package/docs/reference/functions/enabled.md +20 -0
  46. package/docs/reference/functions/focused.md +26 -0
  47. package/docs/reference/functions/mockOf.md +79 -0
  48. package/docs/reference/functions/postgres.md +31 -0
  49. package/docs/reference/functions/process.md +33 -0
  50. package/docs/reference/functions/redis.md +28 -0
  51. package/docs/reference/functions/required.md +41 -0
  52. package/docs/reference/functions/selected.md +26 -0
  53. package/docs/reference/functions/sqlite.md +33 -0
  54. package/docs/reference/functions/testId.md +20 -0
  55. package/docs/reference/functions/text.md +40 -0
  56. package/docs/reference/functions/valued.md +28 -0
  57. package/docs/reference/functions/waitUntil.md +46 -0
  58. package/docs/reference/functions/within.md +29 -0
  59. package/docs/reference/index.md +196 -0
  60. package/docs/reference/interfaces/BaseResult.md +87 -0
  61. package/docs/reference/interfaces/CallResult.md +160 -0
  62. package/docs/reference/interfaces/CaptureScope.md +82 -0
  63. package/docs/reference/interfaces/CliResult.md +253 -0
  64. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  65. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  66. package/docs/reference/interfaces/FetchResult.md +180 -0
  67. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  68. package/docs/reference/interfaces/HttpResult.md +126 -0
  69. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  70. package/docs/reference/interfaces/PageResult.md +356 -0
  71. package/docs/reference/interfaces/RenderResult.md +72 -0
  72. package/docs/reference/interfaces/RenderedText.md +110 -0
  73. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  74. package/docs/reference/interfaces/ScreenResult.md +128 -0
  75. package/docs/reference/interfaces/TableAccessor.md +26 -0
  76. package/docs/reference/interfaces/TextAccessor.md +96 -0
  77. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  78. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  79. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  80. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  81. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  82. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  83. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  84. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  85. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  86. package/docs/reference/type-aliases/CliEnv.md +11 -0
  87. package/docs/reference/type-aliases/CliHandle.md +80 -0
  88. package/docs/reference/type-aliases/CliInput.md +36 -0
  89. package/docs/reference/type-aliases/CliOutput.md +46 -0
  90. package/docs/reference/type-aliases/CliPort.md +58 -0
  91. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  92. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  93. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  94. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  95. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  96. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  97. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  98. package/docs/reference/type-aliases/Contract.md +68 -0
  99. package/docs/reference/type-aliases/ContractInput.md +13 -0
  100. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  101. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  102. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  103. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  104. package/docs/reference/type-aliases/Contracts.md +48 -0
  105. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  106. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  107. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  108. package/docs/reference/type-aliases/DevicePort.md +51 -0
  109. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  110. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  111. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  112. package/docs/reference/type-aliases/DomMount.md +21 -0
  113. package/docs/reference/type-aliases/ElementKind.md +27 -0
  114. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  115. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  116. package/docs/reference/type-aliases/ElementRef.md +128 -0
  117. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  118. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  119. package/docs/reference/type-aliases/HonoApp.md +31 -0
  120. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  121. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  122. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  123. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  124. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  125. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  126. package/docs/reference/type-aliases/Intercept.md +19 -0
  127. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  128. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  129. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  130. package/docs/reference/type-aliases/JobHandle.md +34 -0
  131. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  132. package/docs/reference/type-aliases/JobsResult.md +16 -0
  133. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  134. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  135. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  136. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  137. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  138. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  139. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  140. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  141. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  142. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  143. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  144. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  145. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  146. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  147. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  148. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  149. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  150. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  151. package/docs/reference/type-aliases/MockPort.md +10 -0
  152. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  153. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  154. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  155. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  156. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  157. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  158. package/docs/reference/type-aliases/ServerPort.md +36 -0
  159. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  160. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  161. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  162. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  163. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  164. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  165. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  166. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  167. package/docs/reference/type-aliases/SpecKind.md +8 -0
  168. package/docs/reference/type-aliases/SpecRun.md +108 -0
  169. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  170. package/docs/reference/type-aliases/SpecStream.md +32 -0
  171. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  172. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  173. package/docs/reference/type-aliases/SseEvent.md +58 -0
  174. package/docs/reference/type-aliases/StreamBody.md +61 -0
  175. package/docs/reference/type-aliases/TextFilter.md +22 -0
  176. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  177. package/docs/reference/type-aliases/Visitor.md +215 -0
  178. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  179. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  180. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  181. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  182. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  183. package/docs/reference/variables/anthropic.md +19 -0
  184. package/docs/reference/variables/banner.md +10 -0
  185. package/docs/reference/variables/button.md +10 -0
  186. package/docs/reference/variables/clock.md +16 -0
  187. package/docs/reference/variables/complementary.md +10 -0
  188. package/docs/reference/variables/component.md +10 -0
  189. package/docs/reference/variables/content.md +10 -0
  190. package/docs/reference/variables/contentinfo.md +10 -0
  191. package/docs/reference/variables/dialog.md +10 -0
  192. package/docs/reference/variables/field.md +10 -0
  193. package/docs/reference/variables/form.md +10 -0
  194. package/docs/reference/variables/heading.md +10 -0
  195. package/docs/reference/variables/http.md +40 -0
  196. package/docs/reference/variables/intercept.md +11 -0
  197. package/docs/reference/variables/link.md +10 -0
  198. package/docs/reference/variables/listitem.md +10 -0
  199. package/docs/reference/variables/main.md +10 -0
  200. package/docs/reference/variables/match.md +8 -0
  201. package/docs/reference/variables/navigation.md +10 -0
  202. package/docs/reference/variables/openai.md +21 -0
  203. package/docs/reference/variables/option.md +17 -0
  204. package/docs/reference/variables/region.md +10 -0
  205. package/docs/reference/variables/row.md +10 -0
  206. package/docs/reference/variables/search.md +10 -0
  207. package/docs/reference/variables/specification.md +8 -0
  208. package/docs/reference/variables/status.md +10 -0
  209. package/docs/reference/variables/table.md +10 -0
  210. package/package.json +2 -1
@@ -1,4 +1,4 @@
1
- import { r as formatElement, t as AmbiguousElementError } from "./ambiguity.js";
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, chain, cardinality, { tryScroll: !scrollAttempted });
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.$$(`-ios predicate string:${compilePredicate(level)}`);
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.$$(`-ios predicate string:${compilePredicate(level, { anyVisibility: true })}`))[0];
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);
@@ -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 = original;
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 restoreOrigin = options?.origin === void 0 ? void 0 : resolveRelativeAgainst(options.origin);
159
- let registration;
160
- try {
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 two-release window lasts, stated in the message itself. */
1378
- const WINDOW = "16.0 and 16.1";
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
- 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}). Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. This warning ships for ${WINDOW}; after that the substring match is gone and this is simply not found.`);
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 locator = locate(page, element);
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
- reportSubstringOnly(element);
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