@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
package/dist/oxlint.js CHANGED
@@ -267,7 +267,7 @@ const RULE_DOCS = {
267
267
  },
268
268
  "c1-domain-structure": {
269
269
  channel: "statique",
270
- convention: "A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.",
270
+ convention: "A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. A first-level folder that is no facet — a repository suite, `surface/` or `lint/` — is judged by the same depth as a facet is. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.",
271
271
  facet: "model",
272
272
  family: "C",
273
273
  fix: "Move the file to the depth the tree declares, or declare the depth the tree actually has.",
@@ -437,7 +437,7 @@ const RULE_DOCS = {
437
437
  },
438
438
  "d8w-text-bypass": {
439
439
  channel: "statique",
440
- convention: "`expect(x.text).toContain/toMatch` short-circuits the typed accessor subject → warning.",
440
+ convention: "`expect(x.text).toContain/toMatch` short-circuits the typed accessor subject → warning. A RegExp argument is exempt: `toMatch` on an accessor takes a fixture NAME and throws on a regex, so reaching through to `.text` is the sanctioned form for a raw pattern (chapter 14).",
441
441
  facet: "model",
442
442
  family: "D",
443
443
  fix: "Assert on the accessor itself (`expect(x)`), which carries the tokens and the fixture resolution.",
@@ -647,7 +647,7 @@ const RULE_DOCS = {
647
647
  },
648
648
  "i4-no-module-doubles": {
649
649
  channel: "statique",
650
- convention: "A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (a config comment states why each entry is there), a `__mocks__/` or `__fixtures__/` directory, and — in a `module`-role test — importing a data asset (`.json`, `.sql`, `.yaml`, …) are all errors. A dotted specifier (`./dashboard.post`) is still code. The `vi.stubGlobal` clause moved to M3 in 16.0: one owner per convention.",
650
+ convention: "A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (a config comment states why each entry is there), a `__mocks__/` or `__fixtures__/` directory, and — in a `module`-role test — importing a data asset (`.json`, `.sql`, `.yaml`, …) are all errors. The specifier is read in BOTH spellings — `vi.mock('<spec>')` and the `vi.mock(import('<spec>'))` form `vitest/prefer-import-in-mock` requires — so an allow-listed module has a green spelling under both rules. A dotted specifier (`./dashboard.post`) is still code. The `vi.stubGlobal` clause moved to M3 in 16.0: one owner per convention.",
651
651
  facet: "model",
652
652
  family: "I",
653
653
  fix: "Double the port with `mockOf<Port>()`, keep a payload in a `*.fixtures.ts` neighbour, and allow-list a native module with its reason (chapter 05 § Doubles).",
@@ -657,7 +657,7 @@ const RULE_DOCS = {
657
657
  },
658
658
  "j2-no-sleep": {
659
659
  channel: "statique",
660
- convention: "No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else.",
660
+ convention: "No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation is out of reach: the double is staging the world, not the test waiting on it. A double is a factory call (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler) or a function-valued PROPERTY of an object literal, which is the plainest double there is — `const git: GitGateway = { cloneRepository: async () => … }`, where the object itself is the double.",
661
661
  facet: "model",
662
662
  family: "J",
663
663
  fix: "Wait for the condition, not for a duration.",
@@ -725,10 +725,10 @@ const CHECKER_PASSES = [
725
725
  },
726
726
  {
727
727
  channel: "checker",
728
- convention: "A test under `specs/<facet>/`, where the facet root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself.",
728
+ convention: "A test under `specs/<facet>/`, where the facet root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself. Ground is out of reach — nothing under a leading-underscore folder is a spec, a `.test.ts` under `_expected/` being the GOLDEN a fixer writes. So is any first-level folder that is not a facet: a specification with no module to sit beside and no runner to reach lives in a repository suite there.",
729
729
  facet: "model",
730
730
  family: "C",
731
- fix: "Import `{ <facet> }` from `<facet>.specification.js`, or move a module test beside its module.",
731
+ fix: "Import `{ <facet> }` from `<facet>.specification.js`, move a module test beside its module, or move a repository suite to a first-level folder of its own under `specs/`.",
732
732
  id: "C18",
733
733
  name: "c18-module-test-under-facet",
734
734
  rationale: "A test reaching no runner under a facet is a module test paying the facet's budget and services to prove something that belongs beside its module.",
@@ -802,7 +802,7 @@ const CHECKER_PASSES = [
802
802
  },
803
803
  {
804
804
  channel: "checker",
805
- convention: "A member declares no dependency `@jterrazz/test` already carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only — as a production dependency it is the product’s own library) and no retired seam (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`) — except `@testing-library/react-native` in a member that runs jest, where it is the only vocabulary the runtime has until the React Native answer lands. The optional peers — `playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers`… — are declared by the consumer, by design.",
805
+ convention: "A member declares no dependency `@jterrazz/test` already carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only — as a production dependency it is the product’s own library) and no retired seam (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`) — except `@testing-library/react-native` where jest runs the tests of the member — the only vocabulary that runtime has until the React Native answer lands. Jest runs them when the member DECLARES it (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it) or when a `jest.config.*` elsewhere in the workspace ROOTS into the member, which is the shape a library co-locating its render tests takes when the app beside it owns the only working toolchain. The optional peers — `playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers`… — are declared by the consumer, by design.",
806
806
  facet: "model",
807
807
  family: "F",
808
808
  fix: "Remove the declaration: a transitive is already resolved by the package; a retired seam has a facet that replaces it (`component()`, `website()`, `clock`, `intercept()`). The diagnostic carries a sentence for each case.",
@@ -879,10 +879,10 @@ const CHECKER_PASSES = [
879
879
  },
880
880
  {
881
881
  channel: "checker",
882
- convention: "The suffix says the kind, and the tree agrees with it. A `.spec.ts` — the ASSEMBLED product — lives under `specs/<facet>/`; anywhere else is an error. A `.test.ts` — the UNIT — lives beside its module; under a facet folder (`api`, `cli`, `integration`, `jobs`, `mobile`, `website`) it is an error, fixable by `--fix` (a `git mv`). A `.test.ts` that reaches no runner is C18’s, whose fix is the opposite move, and the mover leaves it alone. A first level that is NOT a facet is a repository suite: it covers a tree, keeps `.test.ts`, and C1’s declared depth is what judges it. A document is named `<case>.spec.yaml`: `<case>` in kebab-case, without the words `test`/`spec`/`cli` the suffix already carries, and never the bare name of its directory.",
882
+ convention: "The suffix says the kind, and the tree agrees with it. A `.spec.ts` — the ASSEMBLED product — lives under `specs/<facet>/`; anywhere else is an error. A `.test.ts` — the UNIT — lives beside its module; under a facet folder (`api`, `cli`, `integration`, `jobs`, `mobile`, `website`) it is an error, fixable by `--fix` (a `git mv`). A `.test.ts` that reaches no runner is C18’s, whose fix is the opposite move, and the mover leaves it alone. A first level that is NOT a facet is a repository suite: it covers a tree, keeps `.test.ts`, and C1’s declared depth is what judges it — unless the tree carries its `*.specification.ts` at its own ROOT, which makes the whole tree one facet’s and every `.test.ts` below it a spec. A document is named `<case>.spec.yaml`: `<case>` in kebab-case, without the words `test`/`spec`/`cli` the suffix already carries, and never the bare name of its directory.",
883
883
  facet: "model",
884
884
  family: "C",
885
- fix: "Move the `.spec.ts` under `specs/<facet>/` (or rename it `.test.ts` beside its module); rename the facet `.test.ts` to `.spec.ts` — `jterrazz-test-check <root> --fix` does it with `git mv`, and names every include glob of the member that still says `.test.ts`.",
885
+ fix: "Move the `.spec.ts` under `specs/<facet>/` (or rename it `.test.ts` beside its module); rename the facet `.test.ts` to `.spec.ts` — `jterrazz-test-check <root> --fix` does it with `git mv`, and names every `include`/`exclude` entry of the member the rename broke.",
886
886
  id: "C12",
887
887
  name: "c12-spec-file-name",
888
888
  rationale: "With no agreement between the suffix and the tree, a reader cannot say what a test proves without opening it; and the filename is the first sentence they read — `rm/rm.spec.yaml` says nothing twice.",
@@ -1400,7 +1400,7 @@ const RUNTIME_RULES = [
1400
1400
  },
1401
1401
  {
1402
1402
  channel: "runtime",
1403
- convention: "An element descriptor must designate exactly ONE element when a verb ACTS on it (`click`/`tap`/`fill`): if several match, the action is refused with an error enumerating the candidates and offering the rewrites (`within(...)`, another descriptor). The framework never acts on \"the first\". A name designates the WHOLE accessible name since 16.0 — `{ exact: false }` is the opt-out — and a descriptor that finds nothing whole but would have found something as a substring raises a runtime warning, once per descriptor, through 16.0 and 16.1. It holds on both scenario facets, website and mobile; on mobile, `see()` — which acts on nothing — is satisfied by any visible match (the XCUITest tree legitimately duplicates a label between a container and its child).",
1403
+ convention: "An element descriptor must designate exactly ONE element when a verb ACTS on it (`click`/`tap`/`fill`): if several match, the action is refused with an error enumerating the candidates and offering the rewrites (`within(...)`, another descriptor). The framework never acts on \"the first\". A name designates the WHOLE accessible name since 16.0 — `{ exact: false }` is the opt-out — and a descriptor that finds nothing whole but would have found something as a substring is still RESOLVED as a substring through 16.x, level by level, with a runtime warning naming the whole name to write, once per descriptor; in 17.0 it is simply not found. It holds on both scenario facets, website and mobile; on mobile, `see()` — which acts on nothing — is satisfied by any visible match (the XCUITest tree legitimately duplicates a label between a container and its child).",
1404
1404
  facet: "model",
1405
1405
  family: "W",
1406
1406
  fix: "Scope it with `within(<landmark>, …)`, or name the element with a descriptor that designates one thing.",
@@ -1692,6 +1692,13 @@ function identifierName(node) {
1692
1692
  const { name } = node;
1693
1693
  return typeof name === "string" ? name : void 0;
1694
1694
  }
1695
+ /** The source end offset of a node or comment (oxlint `end`, else `range[1]`). */
1696
+ function nodeEnd(node) {
1697
+ if (node === void 0) return -1;
1698
+ if (typeof node.end === "number") return node.end;
1699
+ const range = node.range;
1700
+ return Array.isArray(range) && typeof range[1] === "number" ? range[1] : -1;
1701
+ }
1695
1702
  /** The three words a test narrates with, in the order they may appear. */
1696
1703
  const MARKERS = [
1697
1704
  "Given",
@@ -3145,8 +3152,8 @@ function testsItsNeighbour(filename) {
3145
3152
  * product command/area with one or more `<aspect>.test.ts` files plus their
3146
3153
  * shared asset dirs.
3147
3154
  *
3148
- * - a `*.test.ts` sits at facet/domain depth — exactly
3149
- * `specs/<facet>/<domain>/<file>.test.ts`. A test directly at the facet root
3155
+ * - a spec sits at facet/domain depth — exactly
3156
+ * `specs/<facet>/<domain>/<aspect>.spec.ts`. A test directly at the facet root
3150
3157
  * (or nested deeper than a domain) is rejected.
3151
3158
  * - a `*.specification.ts` sits at the facet root — exactly
3152
3159
  * `specs/<facet>/<file>.specification.ts`, never inside a domain.
@@ -3154,8 +3161,8 @@ function testsItsNeighbour(filename) {
3154
3161
  * `depth: 'facet'`
3155
3162
  *
3156
3163
  * The same facet root, one degree looser: the folder follows the assets. A
3157
- * `*.test.ts` sits EITHER at the facet root (`specs/<facet>/<aspect>.test.ts`)
3158
- * or one domain folder down (`specs/<facet>/<domain>/<aspect>.test.ts`) — a
3164
+ * spec sits EITHER at the facet root (`specs/<facet>/<aspect>.spec.ts`)
3165
+ * or one domain folder down (`specs/<facet>/<domain>/<aspect>.spec.ts`) — a
3159
3166
  * test earns a domain folder when it carries assets of its own, and sits
3160
3167
  * beside its siblings when it does not. Anything deeper is rejected, and so is
3161
3168
  * a test loose at the specs root. A `*.specification.ts` sits at the facet
@@ -3164,9 +3171,10 @@ function testsItsNeighbour(filename) {
3164
3171
  * `depth: 'mirror'`
3165
3172
  *
3166
3173
  * The tree mirrors something outside itself (a command tree, a source tree), so
3167
- * the depth is that structure's and no fixed number can name it. A `*.test.ts`
3174
+ * the depth is that structure's and no fixed number can name it. A spec
3168
3175
  * sits at ANY depth of at least one directory under `specs/`, and is NAMED
3169
- * AFTER the directory holding it — `<dir>/<dir>.test.ts`. That naming is what
3176
+ * AFTER the directory holding it — `<dir>/<dir>.spec.ts`, the suffix C12 gives
3177
+ * every file of a facet tree. That naming is what
3170
3178
  * keeps the shape checkable: one test per mirrored node, no test loose at the
3171
3179
  * specs root. Specification files are unconstrained: a mirror has no facet
3172
3180
  * level to anchor them to.
@@ -3267,12 +3275,12 @@ const c1DomainStructure = {
3267
3275
  messages: {
3268
3276
  specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain. Ground that is CODE keeps its own unit test: `<module>.test.ts` NEXT to the `<module>.ts` it tests (I2), and nothing else.",
3269
3277
  specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts`.",
3270
- testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden.",
3271
- testAtSpecsRoot: "A `*.test.ts` must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing.",
3272
- testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"`.",
3273
- testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests.",
3274
- testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting.",
3275
- testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper."
3278
+ testAtFacetRoot: "A spec must live in a domain folder: `specs/<facet>/<domain>/<aspect>.spec.ts` — tests directly at the facet root are forbidden.",
3279
+ testAtSpecsRoot: "A spec must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing.",
3280
+ testNotMirroringDirectory: "A spec must be named after the directory holding it — `{{directory}}/{{directory}}.spec.ts` — with `depth: \"mirror\"`.",
3281
+ testOutsideFacet: "A spec must live in a facet folder — `specs/<facet>/<aspect>.spec.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests.",
3282
+ testTooDeep: "A spec must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.spec.ts` — no deeper nesting.",
3283
+ testTooDeepForFacet: "A spec must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.spec.ts` or `specs/<facet>/<domain>/<aspect>.spec.ts`: with `depth: \"facet\"` nothing nests deeper."
3276
3284
  },
3277
3285
  schema: [{
3278
3286
  additionalProperties: false,
@@ -4011,11 +4019,22 @@ const d6wTransformTokenEquivalent = {
4011
4019
  //#region src/lint/rules/model/d8w-text-bypass.ts
4012
4020
  /** Text-oriented matchers that tempt a raw `.text` bypass (D8). */
4013
4021
  const TEXT_MATCHERS = /* @__PURE__ */ new Set(["toContain", "toMatch"]);
4022
+ /** A regex literal, or `new RegExp(…)` — what an accessor subject refuses. */
4023
+ function isRegExp(node) {
4024
+ if (node === void 0) return false;
4025
+ if (node.type === "Literal") return "regex" in node;
4026
+ return node.type === "NewExpression" && identifierName(child(node, "callee")) === "RegExp";
4027
+ }
4014
4028
  /**
4015
4029
  * CONVENTIONS D8 (warning) — `.text` is the raw stream escape hatch. Asserting
4016
4030
  * `expect(result.text).toContain(…)` throws away the typed subject (its token
4017
4031
  * grammar, its `toMatch('file.txt')` fixture resolution) for a substring check.
4018
4032
  * Prefer asserting on the accessor subject itself.
4033
+ *
4034
+ * A RegExp argument is exempt, because it is the one thing the typed subject
4035
+ * cannot take: `toMatch` on an accessor resolves a FIXTURE NAME and throws on a
4036
+ * regex, and chapter 14 names `expect(x.text).toMatch(/re/)` as the way through.
4037
+ * The rule warned on exactly the spelling its own manual prescribes.
4019
4038
  */
4020
4039
  const d8wTextBypass = {
4021
4040
  create(context) {
@@ -4027,6 +4046,7 @@ const d8wTextBypass = {
4027
4046
  if (expectCall?.type !== "CallExpression") return;
4028
4047
  const expectCallee = expectCall.callee;
4029
4048
  if (expectCallee?.type !== "Identifier" || expectCallee.name !== "expect") return;
4049
+ if (isRegExp(childList(node, "arguments")[0])) return;
4030
4050
  const subject = expectCall.arguments?.[0];
4031
4051
  if (subject !== void 0 && memberPropertyName(subject) === "text") context.report({
4032
4052
  data: { matcher },
@@ -4408,7 +4428,7 @@ const d16wAmbientValue = {
4408
4428
  //#endregion
4409
4429
  //#region src/lint/rules/model/d17w-double-only-oracle.ts
4410
4430
  /** The three ways this vocabulary makes a double. */
4411
- const DOUBLE_FACTORIES = /* @__PURE__ */ new Set([
4431
+ const DOUBLE_FACTORIES$1 = /* @__PURE__ */ new Set([
4412
4432
  "mockOf",
4413
4433
  "vi.fn",
4414
4434
  "vi.spyOn"
@@ -4423,7 +4443,7 @@ function makesADouble(init) {
4423
4443
  if (init.type === "AwaitExpression") return makesADouble(child(init, "argument"));
4424
4444
  if (init.type !== "CallExpression") return false;
4425
4445
  const path = memberPath(child(init, "callee"));
4426
- return path !== void 0 && DOUBLE_FACTORIES.has(path);
4446
+ return path !== void 0 && DOUBLE_FACTORIES$1.has(path);
4427
4447
  }
4428
4448
  /** Every binding in the file whose value is a double this test built. */
4429
4449
  function doubleBindings(root) {
@@ -5494,6 +5514,21 @@ function isModuleDouble(node) {
5494
5514
  const property = child(callee, "property");
5495
5515
  return identifierName(object) === "vi" && (identifierName(property) === "mock" || identifierName(property) === "doMock");
5496
5516
  }
5517
+ /**
5518
+ * The specifier a module double names, in either spelling.
5519
+ *
5520
+ * `vi.mock('./x.js', …)` states it as a string; `vi.mock(import('./x.js'), …)`
5521
+ * states it as a dynamic import, which is the form `vitest/prefer-import-in-mock`
5522
+ * requires. Reading only the string left an allow-listed native module with no
5523
+ * green spelling: satisfy this rule and the upstream one fires, satisfy that one
5524
+ * and this fires.
5525
+ */
5526
+ function mockedSpecifier(argument) {
5527
+ const literal = stringValue(argument);
5528
+ if (literal !== void 0) return literal;
5529
+ if (argument?.type !== "ImportExpression") return;
5530
+ return stringValue(child(argument, "source"));
5531
+ }
5497
5532
  /** A bundler suffix (`./payload.json?raw`, `./doc.md#frag`) is not part of the extension. */
5498
5533
  function withoutSuffix(source) {
5499
5534
  return source.replace(/[?#].*$/u, "");
@@ -5536,7 +5571,7 @@ const i4NoModuleDoubles = {
5536
5571
  const visitor = {
5537
5572
  CallExpression(node) {
5538
5573
  if (!isModuleDouble(node)) return;
5539
- const named = stringValue(childList(node, "arguments")[0]);
5574
+ const named = mockedSpecifier(childList(node, "arguments")[0]);
5540
5575
  if (named === void 0 || !allowed.has(named)) context.report({
5541
5576
  messageId: "viMock",
5542
5577
  node
@@ -5579,6 +5614,40 @@ const i4NoModuleDoubles = {
5579
5614
  };
5580
5615
  //#endregion
5581
5616
  //#region src/lint/rules/model/j2-no-sleep.ts
5617
+ /** The doubles whose implementation STAGES the world rather than waits on it. */
5618
+ const DOUBLE_FACTORIES = /* @__PURE__ */ new Set([
5619
+ "fn",
5620
+ "mockImplementation",
5621
+ "mockImplementationOnce",
5622
+ "mockOf"
5623
+ ]);
5624
+ /**
5625
+ * Is this call a test double taking an implementation — `vi.fn(…)`,
5626
+ * `x.mockImplementation(…)`, `mockOf<Port>({ … })`?
5627
+ *
5628
+ * The member forms are read by their PROPERTY alone, because the object is
5629
+ * whatever the test called its double.
5630
+ */
5631
+ function isDoubleImplementation(node) {
5632
+ const callee = child(node, "callee");
5633
+ if (callee === void 0) return false;
5634
+ const name = callee.type === "Identifier" ? identifierName(callee) : memberPropertyName(callee);
5635
+ return name !== void 0 && DOUBLE_FACTORIES.has(name);
5636
+ }
5637
+ /**
5638
+ * The FUNCTION a property of an object literal holds, or `undefined`.
5639
+ *
5640
+ * A double is not always built by a factory: the plainest one there is, in a
5641
+ * test file, is an object literal whose properties are the port's methods —
5642
+ * `const git: GitGateway = { cloneRepository: async () => … }`. The object IS
5643
+ * the double, so a timer in one of its methods stages the world exactly as
5644
+ * `vi.fn(() => setTimeout(…))` does, and judging it as the test sleeping asked
5645
+ * an author to remove the very thing under test.
5646
+ */
5647
+ function implementationOf(node) {
5648
+ const value = child(node, "value");
5649
+ return value?.type === "ArrowFunctionExpression" || value?.type === "FunctionExpression" ? value : void 0;
5650
+ }
5582
5651
  /**
5583
5652
  * CONVENTIONS J2 — a test contains no arbitrary sleep, wherever it sits:
5584
5653
  * synchronisation is `see()`/`gone()` inside a scenario and `waitUntil()`
@@ -5588,15 +5657,41 @@ const i4NoModuleDoubles = {
5588
5657
  * The reach is every test file — a sleep in a module test beside `src/` waits
5589
5658
  * exactly as blindly as one under `specs/`, and went unseen while the rule
5590
5659
  * gated on the folder.
5660
+ *
5661
+ * What it does NOT reach is a timer inside a test DOUBLE's implementation. A
5662
+ * `vi.fn(() => setTimeout(…))` is the world being staged — a clone that settles
5663
+ * late, a handler that answers out of order — and the assertion that follows is
5664
+ * about the ORDER results come back in, which no predicate can state: a
5665
+ * `waitUntil` waits for something to become true, while what this test needs is
5666
+ * for something to happen late. The test is not the one waiting, so the rule is
5667
+ * not about it. A double written as a plain object literal is the same thing
5668
+ * said without a factory, and it counts too.
5591
5669
  */
5592
5670
  const j2NoSleep = {
5593
5671
  create(context) {
5594
5672
  if (!isTestRole(roleOf(context.filename).role)) return {};
5673
+ const doubles = [];
5674
+ const insideADouble = (node) => doubles.some((span) => nodeStart(node) >= span.start && nodeEnd(node) <= span.end);
5595
5675
  return {
5676
+ Property(node) {
5677
+ const implementation = implementationOf(node);
5678
+ if (implementation !== void 0) doubles.push({
5679
+ end: nodeEnd(implementation),
5680
+ start: nodeStart(implementation)
5681
+ });
5682
+ },
5596
5683
  CallExpression(node) {
5597
- const callee = node.callee;
5684
+ if (isDoubleImplementation(node)) {
5685
+ doubles.push({
5686
+ end: nodeEnd(node),
5687
+ start: nodeStart(node)
5688
+ });
5689
+ return;
5690
+ }
5691
+ if (insideADouble(node)) return;
5692
+ const callee = child(node, "callee");
5598
5693
  if (callee === void 0) return;
5599
- const name = callee.type === "Identifier" ? callee.name : memberPropertyName(callee);
5694
+ const name = callee.type === "Identifier" ? identifierName(callee) : memberPropertyName(callee);
5600
5695
  if (name === "setTimeout" || name === "setInterval") {
5601
5696
  context.report({
5602
5697
  messageId: "sleep",
@@ -5604,7 +5699,7 @@ const j2NoSleep = {
5604
5699
  });
5605
5700
  return;
5606
5701
  }
5607
- if (name === "wait" && callee.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "Atomics") context.report({
5702
+ if (name === "wait" && callee.type === "MemberExpression" && identifierName(child(callee, "object")) === "Atomics") context.report({
5608
5703
  messageId: "sleep",
5609
5704
  node
5610
5705
  });
@@ -1,47 +1,45 @@
1
- import { n as describeAmbiguity, r as formatElement, t as AmbiguousElementError } from "./ambiguity.js";
1
+ import { a as describeAmbiguity, i as AmbiguousElementError, o as formatElement, r as widenedForWindow, t as WINDOW_SETTLE_MS } from "./substring-warning.js";
2
2
  import { mkdtempSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { setTimeout } from "node:timers/promises";
6
- //#region src/model/elements/substring-warning.ts
6
+ //#region src/model/elements/aria-name.ts
7
7
  /**
8
- * The transitional warning for a name that matches only as a SUBSTRING.
8
+ * The accessible name an ARIA snapshot states.
9
9
  *
10
- * Until 16.0 a descriptor's name matched any element whose accessible name
11
- * CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
12
- * test could pass for years against the wrong element without anyone noticing,
13
- * because the wrong element was usually next to the right one. Exact is the
14
- * default now.
10
+ * A descriptor matches the name the BROWSER computes, and that name is not the
11
+ * element's text: `<button><span>Experiments</span><span>9</span></button>`
12
+ * reads as `Experiments9` through `textContent` and is named "Experiments 9",
13
+ * a field's name comes from its label and is nowhere in its text at all, and a
14
+ * name cut at eighty characters resolves to nothing. Reading the name off the
15
+ * snapshot is what makes the spelling the window prints a spelling that can be
16
+ * written back into a spec.
15
17
  *
16
- * A lint rule cannot carry this: whether a name matched whole or in part is a
17
- * fact about a RUN, and a verdict fed by a previous run's artefact is not
18
- * deterministic. So it is a runtime warning, printed by the adapter that did
19
- * the looking, at the moment it has both answers in hand — nothing found under
20
- * exact, something found under substring.
18
+ * The dialect is the one both surfaces already speak — playwright's, on the
19
+ * page directly and through `server.commands` for a mounted component — so the
20
+ * projection is stated once, here, and neither adapter computes a name itself.
21
+ */
22
+ /**
23
+ * The name of the FIRST node a snapshot describes: `- button "Experiments 9"`,
24
+ * optionally followed by `:` and its children.
21
25
  *
22
- * It is printed ONCE per descriptor per process: a scenario that clicks the
23
- * same link in ten tests has one thing to fix, not ten lines to read.
26
+ * A quote must follow the role across a SPACE. `- paragraph: "…"` is a node
27
+ * with text and no name, and reading that text as a name would hand the author
28
+ * a spelling no role descriptor matches on.
24
29
  */
25
- /** How long the two-release window lasts, stated in the message itself. */
26
- const WINDOW = "16.0 and 16.1";
27
- /** Descriptors already reported in this process. */
28
- const reported = /* @__PURE__ */ new Set();
29
- /** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
30
- function identityOf(element) {
31
- const own = `${element.kind}:${element.name ?? ""}`;
32
- return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
30
+ const NAMED = /^-\s+[\w-]+\s+"(?<name>(?:[^"\\]|\\.)*)"/u;
31
+ /** Undo the escaping the snapshot applies to a quote or a backslash inside a name. */
32
+ function unescaped(name) {
33
+ return name.replaceAll(/\\(?<char>.)/gu, "$<char>");
33
34
  }
34
35
  /**
35
- * Report that `element` designated nothing as a whole name, but would have
36
- * designated something as a substring. Returns whether a line was printed,
37
- * which is what the adapters' own tests assert on.
36
+ * The accessible name the snapshot gives the node it describes, or `undefined`
37
+ * when that node has none — an unnamed landmark, a paragraph of text.
38
38
  */
39
- function warnSubstringOnly(element, where) {
40
- const identity = identityOf(element);
41
- if (reported.has(identity)) return false;
42
- reported.add(identity);
43
- 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.`);
44
- return true;
39
+ function accessibleNameIn(snapshot) {
40
+ const [first] = snapshot.split("\n");
41
+ const name = (first === void 0 ? null : NAMED.exec(first.trim()))?.groups?.name;
42
+ return name === void 0 ? void 0 : unescaped(name);
45
43
  }
46
44
  //#endregion
47
45
  //#region src/seams/playwright/playwright.adapter.ts
@@ -130,14 +128,12 @@ async function captureMatches(locator) {
130
128
  * would send the author to fix the wrong descriptor.
131
129
  */
132
130
  async function act(page, element, action) {
131
+ const target = await widenedForWindow(element, page.url(), probeOf(page)) ?? element;
133
132
  try {
134
- return await action(locate(page, element));
133
+ return await action(locate(page, target));
135
134
  } catch (error) {
136
- if (!isStrictViolation(error)) {
137
- await reportSubstringOnly(page, element);
138
- throw error;
139
- }
140
- const culprit = await findAmbiguousLevel(page, element);
135
+ if (!isStrictViolation(error)) throw error;
136
+ const culprit = await findAmbiguousLevel(page, target);
141
137
  const matches = await captureMatches(locate(page, culprit));
142
138
  throw new AmbiguousElementError(describeAmbiguity({
143
139
  element: culprit,
@@ -146,23 +142,65 @@ async function act(page, element, action) {
146
142
  }));
147
143
  }
148
144
  }
145
+ /** What the transitional window may ask of the page. */
146
+ function probeOf(page) {
147
+ return {
148
+ count: async (element) => await locate(page, element).count(),
149
+ nameOf: async (element) => await wholeNameOf(page, element),
150
+ settles: async (element) => await settles(page, element)
151
+ };
152
+ }
149
153
  /**
150
- * Say so when a descriptor found NOTHING as a whole name but would have found
151
- * something as a substring — the one shape the whole-name default changes.
154
+ * Does the descriptor arrive if the page is given the settle budget?
152
155
  *
153
- * Asked only on the failure path, and only for a descriptor that stated no
154
- * `exact` of its own: an author who wrote `{ exact: false }` chose the
155
- * substring, and one who wrote `{ exact: true }` was already exact.
156
+ * A poll rather than `waitFor`, for the reason the rest of this adapter has no
157
+ * `.first()`: waiting on the locator itself would have to narrow a strict
158
+ * locator to one element, and a count answers the same question without
159
+ * touching the mechanism W3 rests on. The locator is rebuilt every round, so a
160
+ * navigation that replaced the document is read on the new one.
156
161
  */
157
- async function reportSubstringOnly(page, element) {
158
- if (element.exact !== void 0 || element.name === void 0) return;
162
+ async function settles(page, element) {
163
+ const deadline = Date.now() + WINDOW_SETTLE_MS;
164
+ for (;;) {
165
+ if (await countOrZero(page, element) > 0) return true;
166
+ if (Date.now() >= deadline) return false;
167
+ await setTimeout(VALUE_POLL_MS);
168
+ }
169
+ }
170
+ /** A count taken mid-navigation throws; the page is simply not answering yet. */
171
+ async function countOrZero(page, element) {
159
172
  try {
160
- if (await locate(page, element).count() > 0) return;
161
- if (await locate(page, {
162
- ...element,
163
- exact: false
164
- }).count() > 0) warnSubstringOnly(element, page.url());
173
+ return await locate(page, element).count();
174
+ } catch {
175
+ return 0;
176
+ }
177
+ }
178
+ /**
179
+ * The name to WRITE for this descriptor — the whole one, never truncated.
180
+ *
181
+ * A role descriptor matches the accessible name the browser computed, so the
182
+ * name is read off the ARIA snapshot, which is that same computation: the text
183
+ * content glues inline children with no separator (`Experiments9` for a button
184
+ * named "Experiments 9") and a field's name is its label, which its text never
185
+ * carries at all. A `text` descriptor matches text, so text is what it is told
186
+ * to write.
187
+ */
188
+ async function wholeNameOf(page, element) {
189
+ const locator = locate(page, element);
190
+ if (element.kind !== "text") try {
191
+ const named = accessibleNameIn(await locator.ariaSnapshot());
192
+ if (named !== void 0) return named;
165
193
  } catch {}
194
+ return await wholeTextOf(locator);
195
+ }
196
+ /** The element's text, whitespace-collapsed and WHOLE — a name cut mid-word resolves to nothing. */
197
+ async function wholeTextOf(locator) {
198
+ try {
199
+ const collapsed = (await locator.evaluate((node) => node.textContent ?? "")).replaceAll(/\s+/gu, " ").trim();
200
+ return collapsed === "" ? void 0 : collapsed;
201
+ } catch {
202
+ return;
203
+ }
166
204
  }
167
205
  /** Walk the scope chain outside-in; the outermost ambiguous level is the fault. */
168
206
  async function findAmbiguousLevel(page, element) {