@jterrazz/test 16.1.0 → 17.0.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 (304) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +2 -2867
  3. package/dist/browser/services.js +2 -0
  4. package/dist/browser/stubs.js +2 -0
  5. package/dist/checker.d.ts +6 -1
  6. package/dist/checker.js +4551 -2612
  7. package/dist/config.d.ts +40 -0
  8. package/dist/config.js +309 -0
  9. package/dist/in-place.global-setup.d.ts +11 -0
  10. package/dist/in-place.global-setup.js +21 -0
  11. package/dist/index.d.ts +1653 -3002
  12. package/dist/index.js +5226 -3845
  13. package/dist/mobile-session.global-setup.d.ts +14 -0
  14. package/dist/mobile-session.global-setup.js +31 -0
  15. package/dist/oxlint.cjs +6073 -5616
  16. package/dist/oxlint.d.cts +43 -7212
  17. package/dist/oxlint.d.ts +43 -7212
  18. package/dist/oxlint.js +6065 -5609
  19. package/dist/route-coverage.global-setup.d.ts +42 -0
  20. package/dist/route-coverage.global-setup.js +2 -0
  21. package/dist/services.d.ts +3 -0
  22. package/dist/services.global-setup.d.ts +43 -0
  23. package/dist/services.global-setup.js +2 -0
  24. package/dist/services.js +597 -0
  25. package/dist/shared-appium.adapter.js +428 -0
  26. package/dist/shared-artifacts.js +32 -0
  27. package/dist/shared-chapter.js +7 -0
  28. package/dist/shared-container.port.d.ts +31 -0
  29. package/dist/shared-derived-projects.js +58 -0
  30. package/dist/shared-device.port.d.ts +189 -0
  31. package/dist/shared-exchange.js +447 -0
  32. package/dist/shared-in-place.js +172 -0
  33. package/dist/shared-index.browser.js +3191 -0
  34. package/dist/shared-index.services.d.ts +346 -0
  35. package/dist/shared-index.stubs.d.ts +287 -0
  36. package/dist/shared-json-shape.js +17 -0
  37. package/dist/shared-landmarks.js +71 -0
  38. package/dist/shared-ledger.js +68 -0
  39. package/dist/shared-literate-plugin.js +183 -0
  40. package/dist/shared-manifest.js +1321 -0
  41. package/dist/shared-pause.js +11 -0
  42. package/dist/shared-peer.js +88 -0
  43. package/dist/shared-playwright.adapter.js +330 -0
  44. package/dist/shared-process.adapter.js +74 -0
  45. package/dist/shared-refusal.js +35 -0
  46. package/dist/shared-resolve.js +24 -0
  47. package/dist/shared-route-coverage.global-setup.js +187 -0
  48. package/dist/shared-scenario-failure.js +198 -0
  49. package/dist/shared-serve.js +365 -0
  50. package/dist/shared-service-images.js +10 -0
  51. package/dist/shared-services.global-setup.js +179 -0
  52. package/dist/shared-session-ledger.js +301 -0
  53. package/dist/shared-setup-entry.js +12 -0
  54. package/dist/shared-source-scan.js +133 -0
  55. package/dist/shared-spec-document.js +365 -0
  56. package/dist/shared-specifications.js +26 -0
  57. package/dist/shared-stream-body.js +130 -0
  58. package/dist/shared-stub-server.js +190 -0
  59. package/dist/shared-temp-root.global-setup.js +80 -0
  60. package/dist/shared-test-baseline.js +26 -0
  61. package/dist/shared-test-section.js +58 -0
  62. package/dist/shared-testcontainers.adapter.js +203 -0
  63. package/dist/shared-tokens.js +55 -0
  64. package/dist/shared-yaml-document.js +119 -0
  65. package/dist/stubs.d.ts +2 -0
  66. package/dist/stubs.js +657 -0
  67. package/dist/temp-root.global-setup.d.ts +7 -0
  68. package/dist/temp-root.global-setup.js +2 -0
  69. package/dist/vitest.d.ts +97 -163
  70. package/dist/vitest.js +374 -507
  71. package/docs/README.md +82 -36
  72. package/docs/concepts/architecture.md +118 -0
  73. package/docs/concepts/conventions.md +333 -0
  74. package/docs/guides/api-specs.md +374 -0
  75. package/docs/guides/cli-specs.md +564 -0
  76. package/docs/guides/component-specs.md +252 -0
  77. package/docs/guides/develop.md +94 -0
  78. package/docs/guides/integration-specs.md +181 -0
  79. package/docs/guides/job-specs.md +180 -0
  80. package/docs/guides/lint-specs.md +174 -0
  81. package/docs/guides/mobile-specs.md +319 -0
  82. package/docs/guides/module-tests.md +149 -0
  83. package/docs/guides/release.md +43 -0
  84. package/docs/guides/spec-documents.md +189 -0
  85. package/docs/guides/website-specs.md +413 -0
  86. package/docs/reference/assertions.md +345 -0
  87. package/docs/reference/glossary.md +20 -0
  88. package/docs/reference/linting.md +483 -0
  89. package/docs/reference/services.md +236 -0
  90. package/docs/reference/stubs.md +498 -0
  91. package/docs/reference/time-and-doubles.md +137 -0
  92. package/docs/reference/tokens.md +258 -0
  93. package/docs/reference/visitor.md +221 -0
  94. package/docs/start/getting-started.md +187 -0
  95. package/docs/start/set-up-a-project.md +214 -0
  96. package/package.json +50 -30
  97. package/schema/spec.schema.json +43 -17
  98. package/dist/appium.adapter.js +0 -489
  99. package/dist/artifacts.js +0 -32
  100. package/dist/fixtures.js +0 -110
  101. package/dist/literate.js +0 -1747
  102. package/dist/match.js +0 -804
  103. package/dist/playwright.adapter.js +0 -349
  104. package/dist/server.js +0 -160
  105. package/dist/stream.js +0 -321
  106. package/dist/substring-warning.js +0 -244
  107. package/docs/01-architecture.md +0 -95
  108. package/docs/02-developing.md +0 -351
  109. package/docs/03-testing.md +0 -495
  110. package/docs/04-operating.md +0 -101
  111. package/docs/05-module-tests.md +0 -152
  112. package/docs/06-integration.md +0 -180
  113. package/docs/07-component.md +0 -246
  114. package/docs/08-website.md +0 -404
  115. package/docs/09-mobile.md +0 -270
  116. package/docs/10-api.md +0 -264
  117. package/docs/11-jobs.md +0 -157
  118. package/docs/12-cli.md +0 -677
  119. package/docs/13-elements.md +0 -242
  120. package/docs/14-assertions.md +0 -314
  121. package/docs/15-tokens.md +0 -253
  122. package/docs/16-contracts.md +0 -438
  123. package/docs/17-services.md +0 -188
  124. package/docs/18-conventions.md +0 -305
  125. package/docs/19-linting.md +0 -510
  126. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  127. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  128. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  129. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  130. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  131. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  132. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  133. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  134. package/docs/decisions/_template.md +0 -22
  135. package/docs/reference/classes/Matcher.md +0 -120
  136. package/docs/reference/classes/ProcessHandle.md +0 -368
  137. package/docs/reference/functions/defineContract.md +0 -39
  138. package/docs/reference/functions/defineContracts.md +0 -36
  139. package/docs/reference/functions/disabled.md +0 -28
  140. package/docs/reference/functions/enabled.md +0 -20
  141. package/docs/reference/functions/focused.md +0 -26
  142. package/docs/reference/functions/mockOf.md +0 -79
  143. package/docs/reference/functions/postgres.md +0 -31
  144. package/docs/reference/functions/process.md +0 -33
  145. package/docs/reference/functions/redis.md +0 -28
  146. package/docs/reference/functions/required.md +0 -41
  147. package/docs/reference/functions/selected.md +0 -26
  148. package/docs/reference/functions/sqlite.md +0 -33
  149. package/docs/reference/functions/testId.md +0 -20
  150. package/docs/reference/functions/text.md +0 -40
  151. package/docs/reference/functions/valued.md +0 -28
  152. package/docs/reference/functions/waitUntil.md +0 -46
  153. package/docs/reference/functions/within.md +0 -29
  154. package/docs/reference/index.md +0 -196
  155. package/docs/reference/interfaces/BaseResult.md +0 -87
  156. package/docs/reference/interfaces/CallResult.md +0 -160
  157. package/docs/reference/interfaces/CaptureScope.md +0 -82
  158. package/docs/reference/interfaces/CliResult.md +0 -253
  159. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  160. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  161. package/docs/reference/interfaces/FetchResult.md +0 -180
  162. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  163. package/docs/reference/interfaces/HttpResult.md +0 -126
  164. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  165. package/docs/reference/interfaces/PageResult.md +0 -356
  166. package/docs/reference/interfaces/RenderResult.md +0 -72
  167. package/docs/reference/interfaces/RenderedText.md +0 -110
  168. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  169. package/docs/reference/interfaces/ScreenResult.md +0 -128
  170. package/docs/reference/interfaces/TableAccessor.md +0 -26
  171. package/docs/reference/interfaces/TextAccessor.md +0 -96
  172. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  173. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  174. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  175. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  176. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  177. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  178. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  179. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  180. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  181. package/docs/reference/type-aliases/CliEnv.md +0 -11
  182. package/docs/reference/type-aliases/CliHandle.md +0 -80
  183. package/docs/reference/type-aliases/CliInput.md +0 -36
  184. package/docs/reference/type-aliases/CliOutput.md +0 -46
  185. package/docs/reference/type-aliases/CliPort.md +0 -58
  186. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  187. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  188. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  189. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  190. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  191. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  192. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  193. package/docs/reference/type-aliases/Contract.md +0 -68
  194. package/docs/reference/type-aliases/ContractInput.md +0 -13
  195. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  196. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  197. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  198. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  199. package/docs/reference/type-aliases/Contracts.md +0 -48
  200. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  201. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  202. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  203. package/docs/reference/type-aliases/DevicePort.md +0 -51
  204. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  205. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  206. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  207. package/docs/reference/type-aliases/DomMount.md +0 -21
  208. package/docs/reference/type-aliases/ElementKind.md +0 -27
  209. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  210. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  211. package/docs/reference/type-aliases/ElementRef.md +0 -128
  212. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  213. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  214. package/docs/reference/type-aliases/HonoApp.md +0 -31
  215. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  216. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  217. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  218. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  219. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  220. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  221. package/docs/reference/type-aliases/Intercept.md +0 -19
  222. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  223. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  224. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  225. package/docs/reference/type-aliases/JobHandle.md +0 -34
  226. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  227. package/docs/reference/type-aliases/JobsResult.md +0 -16
  228. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  229. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  230. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  231. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  232. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  233. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  234. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  235. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  236. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  237. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  238. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  239. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  240. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  241. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  242. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  243. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  244. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  245. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  246. package/docs/reference/type-aliases/MockPort.md +0 -10
  247. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  248. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  249. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  250. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  251. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  252. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  253. package/docs/reference/type-aliases/ServerPort.md +0 -36
  254. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  255. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  256. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  257. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  258. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  259. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  260. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  261. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  262. package/docs/reference/type-aliases/SpecKind.md +0 -8
  263. package/docs/reference/type-aliases/SpecRun.md +0 -108
  264. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  265. package/docs/reference/type-aliases/SpecStream.md +0 -32
  266. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  267. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  268. package/docs/reference/type-aliases/SseEvent.md +0 -58
  269. package/docs/reference/type-aliases/StreamBody.md +0 -61
  270. package/docs/reference/type-aliases/TextFilter.md +0 -22
  271. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  272. package/docs/reference/type-aliases/Visitor.md +0 -215
  273. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  274. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  275. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  276. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  277. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  278. package/docs/reference/variables/anthropic.md +0 -19
  279. package/docs/reference/variables/banner.md +0 -10
  280. package/docs/reference/variables/button.md +0 -10
  281. package/docs/reference/variables/clock.md +0 -16
  282. package/docs/reference/variables/complementary.md +0 -10
  283. package/docs/reference/variables/component.md +0 -10
  284. package/docs/reference/variables/content.md +0 -10
  285. package/docs/reference/variables/contentinfo.md +0 -10
  286. package/docs/reference/variables/dialog.md +0 -10
  287. package/docs/reference/variables/field.md +0 -10
  288. package/docs/reference/variables/form.md +0 -10
  289. package/docs/reference/variables/heading.md +0 -10
  290. package/docs/reference/variables/http.md +0 -40
  291. package/docs/reference/variables/intercept.md +0 -11
  292. package/docs/reference/variables/link.md +0 -10
  293. package/docs/reference/variables/listitem.md +0 -10
  294. package/docs/reference/variables/main.md +0 -10
  295. package/docs/reference/variables/match.md +0 -8
  296. package/docs/reference/variables/navigation.md +0 -10
  297. package/docs/reference/variables/openai.md +0 -21
  298. package/docs/reference/variables/option.md +0 -17
  299. package/docs/reference/variables/region.md +0 -10
  300. package/docs/reference/variables/row.md +0 -10
  301. package/docs/reference/variables/search.md +0 -10
  302. package/docs/reference/variables/specification.md +0 -8
  303. package/docs/reference/variables/status.md +0 -10
  304. package/docs/reference/variables/table.md +0 -10
@@ -10,8 +10,23 @@
10
10
  "type": "string"
11
11
  },
12
12
  {
13
+ "allOf": [
14
+ {
15
+ "anyOf": [
16
+ {
17
+ "required": [
18
+ "contains"
19
+ ]
20
+ },
21
+ {
22
+ "required": [
23
+ "equals"
24
+ ]
25
+ }
26
+ ]
27
+ }
28
+ ],
13
29
  "additionalProperties": false,
14
- "minProperties": 1,
15
30
  "properties": {
16
31
  "contains": {
17
32
  "oneOf": [
@@ -36,6 +51,20 @@
36
51
  ]
37
52
  },
38
53
  "run": {
54
+ "allOf": [
55
+ {
56
+ "not": {
57
+ "required": [
58
+ "stdin",
59
+ "waitFor"
60
+ ]
61
+ }
62
+ }
63
+ ],
64
+ "required": [
65
+ "command",
66
+ "exit"
67
+ ],
39
68
  "additionalProperties": false,
40
69
  "properties": {
41
70
  "command": {
@@ -74,10 +103,6 @@
74
103
  "type": "string"
75
104
  }
76
105
  },
77
- "required": [
78
- "command",
79
- "exit"
80
- ],
81
106
  "type": "object"
82
107
  },
83
108
  "serveEntry": {
@@ -102,10 +127,10 @@
102
127
  "$id": "https://jterrazz.com/schema/jterrazz-test/spec.schema.json",
103
128
  "$schema": "https://json-schema.org/draft/2020-12/schema",
104
129
  "additionalProperties": false,
105
- "description": "One scenario of the @jterrazz/test literate format: the ground a session stands on, then the session itself.",
130
+ "description": "One case of the @jterrazz/test literate format, one file per test: the ground it stands on, what it does, and what came back.",
106
131
  "properties": {
107
132
  "description": {
108
- "description": "The vitest test title — one lowercase line, no trailing period.",
133
+ "description": "The vitest test title: one lowercase line, no trailing period.",
109
134
  "type": "string"
110
135
  },
111
136
  "env": {
@@ -124,33 +149,34 @@
124
149
  "description": "A bare word names an env set registered in code; KEY=value is inline. $WORKDIR expands."
125
150
  },
126
151
  "fixture": {
152
+ "description": "Fixture paths copied into the working directory, layering in order, each relative to this document (`./...`, `../...`) and in the two forms the chain's `file()` and `directory()` take: a trailing `/` names a directory whose contents are spread, without it one file is copied under its own name. The first ground folder a path passes through is __fixtures__/: ./__fixtures__/ beside the document, or specs/__fixtures__/ reached by the path that climbs to it.",
127
153
  "oneOf": [
128
154
  {
155
+ "pattern": "^\\.\\.?/(?:(?!(?:__expected__|__fixtures__|__requests__|__seeds__)/)[^/]+/)*?__fixtures__/.*$",
129
156
  "type": "string"
130
157
  },
131
158
  {
132
159
  "items": {
160
+ "pattern": "^\\.\\.?/(?:(?!(?:__expected__|__fixtures__|__requests__|__seeds__)/)[^/]+/)*?__fixtures__/.*$",
133
161
  "type": "string"
134
162
  },
135
163
  "minItems": 1,
136
164
  "type": "array"
137
165
  }
138
- ],
139
- "description": "Fixture paths copied into the working directory, layering in order. `$FIXTURES/<name>` is the shared pool at `<specs-root>/_fixtures/`; a bare path is the ground beside this document, at `_fixtures/<name>`."
140
- },
141
- "kind": {
142
- "default": "cli",
143
- "description": "The facet this document describes.",
144
- "enum": [
145
- "cli"
146
- ],
147
- "type": "string"
166
+ ]
148
167
  },
149
168
  "runs": {
169
+ "contains": {
170
+ "required": [
171
+ "waitFor"
172
+ ]
173
+ },
150
174
  "description": "The session: one or more runs, sequential, sharing one working directory.",
151
175
  "items": {
152
176
  "$ref": "#/$defs/run"
153
177
  },
178
+ "maxContains": 1,
179
+ "minContains": 0,
154
180
  "minItems": 1,
155
181
  "type": "array"
156
182
  },
@@ -1,489 +0,0 @@
1
- import { a as formatElement, r as AmbiguousElementError, t as warnSubstringOnly } from "./substring-warning.js";
2
- import { mkdtempSync, writeFileSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { tmpdir } from "node:os";
5
- //#region src/facets/mobile/ambiguity.ts
6
- /**
7
- * The ambiguity refusal, mobile edition — CONVENTIONS W3.
8
- *
9
- * A mobile descriptor must designate exactly one element. Acting on "the
10
- * first match" is the failure mode this module exists to prevent: the spec
11
- * keeps passing while the visitor taps something else, and nothing ever
12
- * reports it. So when a descriptor matches several elements the framework
13
- * refuses, and the refusal carries everything needed to fix it without
14
- * opening the simulator — the descriptor in the caller's own vocabulary,
15
- * every candidate, and the concrete rewrites that would resolve it.
16
- *
17
- * The element vocabulary is shared with the website facet, so the source
18
- * rendering (`formatElement`) is too; only the evidence differs — XCUITest
19
- * types, labels and accessibility identifiers instead of tags and landmarks.
20
- * Pure string building: the appium integration stays a thin adapter.
21
- */
22
- /** `1. Button "Bookmark" [testId: event-42]` — one evidence line per candidate. */
23
- function formatMatch(match, index) {
24
- const label = match.label === void 0 ? "" : ` ${JSON.stringify(match.label)}`;
25
- const value = match.value === void 0 ? "" : ` value=${JSON.stringify(match.value)}`;
26
- const identifier = match.identifier === void 0 ? "" : ` [testId: ${match.identifier}]`;
27
- return ` ${index + 1}. ${match.type}${label}${value}${identifier}`;
28
- }
29
- /**
30
- * The rewrites worth offering, in the order they should be tried. Scoping is
31
- * always available; `exact` only to a descriptor that opted OUT of it (exact
32
- * is the default), and the count it would leave is stated so a
33
- * still-ambiguous suggestion never reads as a fix; distinct identifiers make
34
- * `testId()` a concrete rewrite rather than a guess.
35
- */
36
- function formatFixes(element, matches) {
37
- const fixes = [];
38
- if (!element.scope) fixes.push(`scope it within(testId('…'), ${formatElement(element)}) — a screen has no landmarks; any descriptor works as the scope`);
39
- if (element.exact === false && element.name !== void 0) {
40
- const remaining = matches.filter((match) => match.label === element.name).length;
41
- if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatElement({
42
- exact: true,
43
- kind: element.kind,
44
- name: element.name
45
- })} [leaves ${remaining} of ${matches.length}]`);
46
- }
47
- const identifiers = [...new Set(matches.map((match) => match.identifier).filter((identifier) => identifier !== void 0))];
48
- if (identifiers.length > 1 && element.kind !== "testId") fixes.push(`test id testId(${JSON.stringify(identifiers[0])}) [also here: ${identifiers.slice(1).join(", ")}]`);
49
- fixes.push("other element a button() or field() may name one thing where this does not");
50
- return fixes;
51
- }
52
- /**
53
- * Build the refusal thrown when a descriptor matches more than one element
54
- * on the screen.
55
- *
56
- * @param options.element The descriptor as the caller wrote it.
57
- * @param options.matches Every candidate, in screen order (already truncated).
58
- */
59
- function describeMobileAmbiguity(options) {
60
- const { element, matches } = options;
61
- const fixes = formatFixes(element, matches).map((fix) => ` • ${fix}`);
62
- return [
63
- `Ambiguous element: ${formatElement(element)} matched ${matches.length} elements on the screen.`,
64
- "",
65
- "A spec must designate exactly one element. Acting on the first match would let",
66
- "this test keep passing while the visitor taps something else.",
67
- "",
68
- "Matched:",
69
- ...matches.map((match, index) => formatMatch(match, index)),
70
- "",
71
- "Disambiguate with one of:",
72
- ...fixes,
73
- "",
74
- "Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
75
- ].join("\n");
76
- }
77
- //#endregion
78
- //#region src/facets/mobile/projection.ts
79
- const TAG_PATTERN = /<(?<openTag>[A-Za-z][\w.]*)(?<rawAttributes>(?:\s+[\w:.-]+="[^"]*")*)\s*(?<selfClosing>\/?)>|<\/(?<closeTag>[\w.]*)\s*>/gu;
80
- const ATTRIBUTE_PATTERN = /(?<key>[\w:.-]+)="(?<value>[^"]*)"/gu;
81
- const NAMED_ENTITIES = {
82
- "&amp;": "&",
83
- "&apos;": "'",
84
- "&gt;": ">",
85
- "&lt;": "<",
86
- "&quot;": "\""
87
- };
88
- /** Decode the XML entities an attribute value can carry. */
89
- function decodeEntities(value) {
90
- return value.replaceAll(/&(?:#x(?<hex>[\dA-Fa-f]+)|#(?<decimal>\d+)|(?<named>amp|apos|gt|lt|quot));/gu, (entity, hex, decimal) => {
91
- if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
92
- if (decimal) return String.fromCodePoint(Number(decimal));
93
- return NAMED_ENTITIES[entity] ?? entity;
94
- });
95
- }
96
- /** Parse the page source into a raw element tree (strict, attribute-only XML). */
97
- function parsePageSource(xml) {
98
- const stack = [];
99
- let root = null;
100
- for (const match of xml.matchAll(TAG_PATTERN)) {
101
- const { closeTag, openTag, rawAttributes, selfClosing } = match.groups ?? {};
102
- if (closeTag !== void 0 || openTag === void 0) {
103
- stack.pop();
104
- continue;
105
- }
106
- const attributes = {};
107
- for (const attribute of (rawAttributes ?? "").matchAll(ATTRIBUTE_PATTERN)) {
108
- const { key, value } = attribute.groups ?? {};
109
- if (key !== void 0 && value !== void 0) attributes[key] = decodeEntities(value);
110
- }
111
- const node = {
112
- attributes,
113
- children: [],
114
- tag: openTag
115
- };
116
- const parent = stack.at(-1);
117
- if (parent === void 0) root = node;
118
- else parent.children.push(node);
119
- if (!selfClosing) stack.push(node);
120
- }
121
- if (!root) throw new Error("projectScreen(): the page source contains no XML element");
122
- return root;
123
- }
124
- /** The substance of a node — what survives projection, or nothing. */
125
- function substance(node) {
126
- const label = node.attributes.label || void 0;
127
- const value = node.attributes.value || void 0;
128
- const identifier = node.attributes.name || void 0;
129
- return {
130
- type: node.tag.replace(/^XCUIElementType/u, ""),
131
- ...label === void 0 ? {} : { label },
132
- ...value === void 0 || value === label ? {} : { value },
133
- ...identifier === void 0 || identifier === label ? {} : { identifier }
134
- };
135
- }
136
- function hasSubstance(node) {
137
- return node.label !== void 0 || node.value !== void 0 || node.identifier !== void 0;
138
- }
139
- /** A childless child that repeats its parent is the same accessibility element twice. */
140
- function repeatsParent(child, parent) {
141
- return child.children === void 0 && child.type === parent.type && child.label === parent.label && child.value === parent.value && child.identifier === parent.identifier;
142
- }
143
- /** Project one raw node: a kept node, or (hoisting) the projections of its children. */
144
- function projectNode(node) {
145
- const own = substance(node);
146
- const children = node.children.flatMap((child) => projectNode(child)).filter((child) => !repeatsParent(child, own));
147
- if (!hasSubstance(own)) return children;
148
- return [{
149
- ...own,
150
- ...children.length > 0 ? { children } : {}
151
- }];
152
- }
153
- /** Collect the visible texts in document order, collapsing consecutive repeats. */
154
- function collectTexts(node, texts) {
155
- if (node.attributes.visible === "true") {
156
- const text = node.attributes.label || node.attributes.value || void 0;
157
- if (text !== void 0 && texts.at(-1) !== text) texts.push(text);
158
- }
159
- for (const child of node.children) collectTexts(child, texts);
160
- }
161
- /**
162
- * Project a raw XCUITest page source into the captured screen: the collapsed
163
- * tree (rooted at the application node, which is always kept) and the
164
- * visible texts in document order.
165
- */
166
- function projectScreen(xml) {
167
- let root = parsePageSource(xml);
168
- const [onlyChild] = root.children;
169
- if (root.tag === "AppiumAUT" && root.children.length === 1 && onlyChild !== void 0) root = onlyChild;
170
- const texts = [];
171
- for (const child of root.children) collectTexts(child, texts);
172
- const own = substance(root);
173
- const children = root.children.flatMap((child) => projectNode(child)).filter((child) => !repeatsParent(child, own));
174
- return {
175
- texts,
176
- tree: {
177
- ...own,
178
- ...children.length > 0 ? { children } : {}
179
- }
180
- };
181
- }
182
- //#endregion
183
- //#region src/seams/appium/appium.adapter.ts
184
- const DEFAULT_ACTION_TIMEOUT_MS = 3e4;
185
- const POLL_INTERVAL_MS = 500;
186
- /** How many candidates the ambiguity error enumerates before truncating. */
187
- const MAX_REPORTED_MATCHES = 10;
188
- /** WebDriverAgent builds once per simulator — the first session is the slow one. */
189
- const DEFAULT_WDA_LAUNCH_TIMEOUT_MS = 24e4;
190
- const delay = async (ms) => {
191
- await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
192
- };
193
- /** Escape a name for an NSPredicate single-quoted string literal. */
194
- function escapePredicate(value) {
195
- return value.replaceAll("\\", String.raw`\\`).replaceAll("'", String.raw`\'`);
196
- }
197
- /**
198
- * Translate a user-facing descriptor into an iOS predicate string. Landmarks
199
- * have no XCUITest analog — the shared vocabulary is wider than a screen,
200
- * and the boundary is named rather than silently approximated.
201
- */
202
- function compilePredicate(element, options) {
203
- const name = escapePredicate(element.name ?? "");
204
- const contains = (attribute) => element.exact === false ? `${attribute} CONTAINS '${name}'` : `${attribute} == '${name}'`;
205
- const visible = options?.anyVisibility ? "1 == 1" : "visible == 1";
206
- switch (element.kind) {
207
- case "button": return `type == 'XCUIElementTypeButton' AND ${contains("label")} AND ${visible}`;
208
- case "field": return `type IN {'XCUIElementTypeTextField','XCUIElementTypeSecureTextField'} AND (${contains("label")} OR ${contains("value")}) AND ${visible}`;
209
- case "testId": return `name == '${name}' AND ${visible}`;
210
- case "text": return `(${contains("label")} OR ${contains("value")}) AND ${visible}`;
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
- }
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
- }
224
- /** The outside-in scope chain of a descriptor, ending on the target itself. */
225
- function scopeChain(element) {
226
- const chain = [];
227
- for (let current = element; current; current = current.scope) chain.unshift(current);
228
- return chain;
229
- }
230
- /**
231
- * Load webdriverio's session factory, naming the install when the optional
232
- * peer dependency is absent. Lazy: the dependency is only required by a spec
233
- * that actually opens the app.
234
- */
235
- async function loadRemote() {
236
- try {
237
- const { remote: remoteFn } = await import("webdriverio");
238
- return remoteFn;
239
- } catch {
240
- throw new Error(".open() requires webdriverio (optional peer dependency): npm install -D appium webdriverio && npx appium driver install xcuitest");
241
- }
242
- }
243
- /** Capture one candidate's evidence for the ambiguity refusal. */
244
- async function captureMatch(element) {
245
- const [type, label, value, identifier] = await Promise.all([
246
- element.getAttribute("type"),
247
- element.getAttribute("label"),
248
- element.getAttribute("value"),
249
- element.getAttribute("name")
250
- ]);
251
- const shortLabel = label ? label.replaceAll(/\s+/gu, " ").trim().slice(0, 80) : void 0;
252
- return {
253
- type: (type ?? "Unknown").replace(/^XCUIElementType/u, ""),
254
- ...shortLabel === void 0 ? {} : { label: shortLabel },
255
- ...value && value !== label ? { value } : {},
256
- ...identifier && identifier !== label ? { identifier } : {}
257
- };
258
- }
259
- /**
260
- * Device adapter backed by appium's XCUITest driver over webdriverio.
261
- *
262
- * ONE driver session per adapter (= per runner = per vitest worker), created
263
- * lazily on the first `open()` and reused — WebDriverAgent startup is the
264
- * expensive part, not the app relaunch. Every `open()` terminates and
265
- * relaunches the app, so each spec starts from a deterministic fresh state.
266
- *
267
- * Webdriverio is an optional peer dependency: it is only imported here, and
268
- * this module is only loaded when a spec calls `.open()`.
269
- */
270
- var AppiumAdapter = class {
271
- /** The verb poll budget — the runner's `timeouts.action`, or the default. */
272
- actionTimeoutMs;
273
- driver = null;
274
- /** The WebDriverAgent launch budget — the runner's `timeouts.launch`, or the default. */
275
- launchTimeoutMs;
276
- options;
277
- /**
278
- * The session factory. Absent in production — webdriverio is then loaded
279
- * lazily on the first `open()`. Injected by this module's own unit test,
280
- * which drives the waits over a stub driver with no simulator in reach
281
- * (mocks are CODE here, CONVENTIONS I4).
282
- */
283
- remote;
284
- constructor(options) {
285
- this.options = {
286
- serverUrl: options.serverUrl,
287
- udid: options.udid
288
- };
289
- this.remote = options.remote;
290
- this.actionTimeoutMs = options.timeouts?.action ?? DEFAULT_ACTION_TIMEOUT_MS;
291
- this.launchTimeoutMs = options.timeouts?.launch ?? DEFAULT_WDA_LAUNCH_TIMEOUT_MS;
292
- }
293
- async close() {
294
- if (this.driver) {
295
- await this.driver.deleteSession();
296
- this.driver = null;
297
- }
298
- }
299
- async open(options) {
300
- const driver = await this.session(options.bundleId);
301
- try {
302
- await driver.executeScript("mobile: terminateApp", [{ bundleId: options.bundleId }]);
303
- } catch {}
304
- await (options.deepLink === void 0 ? driver.executeScript("mobile: activateApp", [{ bundleId: options.bundleId }]) : driver.executeScript("mobile: deepLink", [{
305
- bundleId: options.bundleId,
306
- url: options.deepLink
307
- }]));
308
- if (options.scenario) try {
309
- await options.scenario(this.createVisitor(driver));
310
- } catch (error) {
311
- const evidence = await this.captureEvidence(driver);
312
- const suffix = evidence ? `\nEvidence: ${evidence}` : "";
313
- if (error instanceof AmbiguousElementError) {
314
- error.message += suffix;
315
- throw error;
316
- }
317
- throw new Error(`open scenario failed: ${error instanceof Error ? error.message : String(error)}${suffix}`, { cause: error });
318
- }
319
- return projectScreen(await driver.getPageSource());
320
- }
321
- /**
322
- * The visitor implementation. Acting verbs (tap/fill) poll until exactly
323
- * one visible match exists (CONVENTIONS W3); `see()` acts on nothing, so
324
- * ANY visible match satisfies it — XCUITest trees legitimately expose the
325
- * same text more than once (container/child label duplication), and a
326
- * synchronization primitive refusing on that would punish honest screens.
327
- */
328
- createVisitor(driver) {
329
- return {
330
- fill: async (element, value) => {
331
- await (await this.resolveOne(driver, element, "fill", "one")).setValue(value);
332
- },
333
- see: async (element) => {
334
- await this.resolveOne(driver, element, "see", "any");
335
- },
336
- tap: async (element) => {
337
- await (await this.resolveOne(driver, element, "tap", "one")).click();
338
- }
339
- };
340
- }
341
- /**
342
- * Resolve a descriptor: poll until at least one visible match exists,
343
- * then enforce the verb's cardinality — `'one'` refuses ambiguity
344
- * (CONVENTIONS W3), `'any'` settles for the first match. Scopes stay
345
- * strict in BOTH modes: "within one of several containers" designates
346
- * nothing, whatever the verb. The chain resolves outside-in, so an
347
- * ambiguous scope names ITSELF as the fault rather than sending the
348
- * author to fix the target.
349
- */
350
- async resolveOne(driver, element, verb, cardinality) {
351
- const chain = scopeChain(element);
352
- const deadline = Date.now() + this.actionTimeoutMs;
353
- let scrollAttempted = false;
354
- let windowAsked = false;
355
- let looking = chain;
356
- for (;;) {
357
- const resolved = await this.resolveChain(driver, looking, cardinality, { tryScroll: !scrollAttempted });
358
- scrollAttempted = true;
359
- if (resolved) return resolved;
360
- if (!windowAsked) {
361
- windowAsked = true;
362
- looking = await this.widenedChain(driver, chain) ?? chain;
363
- }
364
- if (Date.now() > deadline) throw await this.timeoutError(driver, element, verb);
365
- await delay(POLL_INTERVAL_MS);
366
- }
367
- }
368
- /** One resolution pass over the chain — `null` means "nothing yet, keep polling". */
369
- async resolveChain(driver, chain, cardinality, options) {
370
- let scope = driver;
371
- for (const [index, level] of chain.entries()) {
372
- const matches = await matching(scope, level);
373
- const count = await matches.length;
374
- if (count === 0) {
375
- if (options?.tryScroll) {
376
- const first = (await matching(scope, level, { anyVisibility: true }))[0];
377
- if (first !== void 0) try {
378
- await driver.executeScript("mobile: scroll", [{
379
- elementId: first.elementId,
380
- toVisible: true
381
- }]);
382
- } catch {}
383
- }
384
- return null;
385
- }
386
- const isTarget = index === chain.length - 1;
387
- if (count > 1 && (cardinality === "one" || !isTarget)) throw new AmbiguousElementError(describeMobileAmbiguity({
388
- element: level,
389
- matches: await this.captureMatches(matches)
390
- }));
391
- const next = matches[0];
392
- if (next === void 0) return null;
393
- scope = next;
394
- }
395
- return scope;
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
- }
436
- /** Enumerate the candidates' evidence, truncated. */
437
- async captureMatches(matches) {
438
- const count = Math.min(await matches.length, MAX_REPORTED_MATCHES);
439
- const evidence = [];
440
- for (let index = 0; index < count; index++) {
441
- const match = matches[index];
442
- if (match !== void 0) evidence.push(await captureMatch(match));
443
- }
444
- return evidence;
445
- }
446
- /** Screenshot the failing state into a temp file; never masks the original error. */
447
- async captureEvidence(driver) {
448
- try {
449
- const dir = mkdtempSync(resolve(tmpdir(), "spec-mobile-"));
450
- const path = resolve(dir, "failure.png");
451
- writeFileSync(path, Buffer.from(await driver.takeScreenshot(), "base64"));
452
- return path;
453
- } catch {
454
- return null;
455
- }
456
- }
457
- /** The timeout refusal, with a compact excerpt of what IS on screen to aid diagnosis. */
458
- async timeoutError(driver, element, verb) {
459
- let excerpt = "";
460
- try {
461
- const source = await driver.getPageSource();
462
- const labels = [...new Set([...source.matchAll(/label="(?<label>[^"]{2,60})"/gu)].map((match) => match.groups?.label ?? ""))].slice(0, 15);
463
- if (labels.length > 0) excerpt = `\nCurrently on screen: ${labels.join(" | ")}`;
464
- } catch {}
465
- return /* @__PURE__ */ new Error(`${verb}(${formatElement(element)}) timed out after ${this.actionTimeoutMs}ms — no visible match.${excerpt}`);
466
- }
467
- /** Create the shared driver session (once), with an actionable error when webdriverio is absent. */
468
- async session(bundleId) {
469
- if (this.driver) return this.driver;
470
- const remoteFn = this.remote ?? await loadRemote();
471
- const url = new URL(this.options.serverUrl);
472
- this.driver = await remoteFn({
473
- capabilities: {
474
- "appium:automationName": "XCUITest",
475
- "appium:bundleId": bundleId,
476
- "appium:noReset": true,
477
- "appium:udid": this.options.udid,
478
- "appium:wdaLaunchTimeout": this.launchTimeoutMs,
479
- platformName: "iOS"
480
- },
481
- hostname: url.hostname,
482
- logLevel: "error",
483
- port: Number(url.port)
484
- });
485
- return this.driver;
486
- }
487
- };
488
- //#endregion
489
- export { AppiumAdapter };
package/dist/artifacts.js DELETED
@@ -1,32 +0,0 @@
1
- /** Everything a vitest run generates. */
2
- const VITEST_ARTIFACTS_DIR = `.artifacts/vitest`;
3
- /** Where the coverage provider writes its report. */
4
- const COVERAGE_DIR = `${VITEST_ARTIFACTS_DIR}/coverage`;
5
- /** Where `sqlite()` caches the schema template it copies per worker. */
6
- const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
7
- /**
8
- * Where `context.annotate()` puts what a test attaches, and where Browser Mode
9
- * drops a failure screenshot. Vitest 5 defaults both to `.vitest/` at the
10
- * repository ROOT — a second artefact folder beside the one the estate declares,
11
- * and one no `.gitignore` rule of ours covers.
12
- */
13
- const ATTACHMENTS_DIR = `${VITEST_ARTIFACTS_DIR}/attachments`;
14
- /** Where the browser provider drops a screenshot of a failing test. */
15
- const SCREENSHOTS_DIR = `${VITEST_ARTIFACTS_DIR}/screenshots`;
16
- /**
17
- * What each file-writing reporter writes, by reporter name.
18
- *
19
- * Vitest 5 turned the json and junit reporters into FILE writers (they printed
20
- * to stdout before) and gave the html and blob ones a `.vitest/` home. Each one
21
- * is a path the tool pins and a consumer never states, so the preset moves all
22
- * four at once rather than leaving a repository to discover them one CI run at
23
- * a time.
24
- */
25
- const REPORTER_OUTPUT_FILES = {
26
- blob: `${VITEST_ARTIFACTS_DIR}/blob`,
27
- html: `${VITEST_ARTIFACTS_DIR}/html/index.html`,
28
- json: `${VITEST_ARTIFACTS_DIR}/json/output.json`,
29
- junit: `${VITEST_ARTIFACTS_DIR}/junit/output.xml`
30
- };
31
- //#endregion
32
- export { SQLITE_TEMPLATE_DIR as a, SCREENSHOTS_DIR as i, COVERAGE_DIR as n, VITEST_ARTIFACTS_DIR as o, REPORTER_OUTPUT_FILES as r, ATTACHMENTS_DIR as t };