@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.
Files changed (211) hide show
  1. package/dist/appium.adapter.js +59 -4
  2. package/dist/browser/index.js +203 -42
  3. package/dist/checker.js +305 -63
  4. package/dist/index.d.ts +35 -4
  5. package/dist/index.js +47 -24
  6. package/dist/oxlint.cjs +123 -28
  7. package/dist/oxlint.js +123 -28
  8. package/dist/playwright.adapter.js +88 -50
  9. package/dist/substring-warning.js +282 -0
  10. package/dist/vitest.js +91 -12
  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 +248 -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 +332 -0
  29. package/docs/19-linting.md +512 -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 +3 -2
  211. package/dist/ambiguity.js +0 -146
@@ -1,4 +1,4 @@
1
- import { r as formatElement, t as AmbiguousElementError } from "./ambiguity.js";
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, 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
+ 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);
@@ -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
  } };
@@ -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 two-release window lasts, stated in the message itself. */
1378
- const WINDOW = "16.0 and 16.1";
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
- 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.`);
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 locator = locate(page, element);
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
- reportSubstringOnly(element);
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 };