@jterrazz/test 16.1.1 → 18.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 (309) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +1 -2980
  3. package/dist/browser/services.js +1 -0
  4. package/dist/browser/stubs.js +1 -0
  5. package/dist/browser-shared-index.browser.js +3233 -0
  6. package/dist/checker.d.ts +6 -1
  7. package/dist/checker.js +3721 -2624
  8. package/dist/config.d.ts +40 -0
  9. package/dist/config.js +309 -0
  10. package/dist/in-place.global-setup.d.ts +12 -0
  11. package/dist/in-place.global-setup.js +21 -0
  12. package/dist/index.d.ts +1641 -3004
  13. package/dist/index.js +5095 -3835
  14. package/dist/mobile-session.global-setup.d.ts +15 -0
  15. package/dist/mobile-session.global-setup.js +31 -0
  16. package/dist/oxlint.cjs +6069 -5634
  17. package/dist/oxlint.d.cts +43 -7212
  18. package/dist/oxlint.d.ts +43 -7212
  19. package/dist/oxlint.js +3979 -5799
  20. package/dist/route-coverage.global-setup.d.ts +43 -0
  21. package/dist/route-coverage.global-setup.js +1 -0
  22. package/dist/services.d.ts +3 -0
  23. package/dist/services.global-setup.d.ts +43 -0
  24. package/dist/services.global-setup.js +1 -0
  25. package/dist/services.js +597 -0
  26. package/dist/shared-appium.adapter.js +428 -0
  27. package/dist/shared-artifacts.js +34 -0
  28. package/dist/shared-chapter.js +7 -0
  29. package/dist/shared-container.port.d.ts +31 -0
  30. package/dist/shared-derived-projects.js +73 -0
  31. package/dist/shared-device.port.d.ts +189 -0
  32. package/dist/shared-exchange.js +447 -0
  33. package/dist/shared-in-place.js +172 -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-listened-port.js +19 -0
  40. package/dist/shared-literate-plugin.js +184 -0
  41. package/dist/shared-manifest.js +1321 -0
  42. package/dist/shared-pause.js +22 -0
  43. package/dist/shared-peer.js +88 -0
  44. package/dist/shared-playwright.adapter.js +328 -0
  45. package/dist/shared-process-alive.js +13 -0
  46. package/dist/shared-process.adapter.js +74 -0
  47. package/dist/shared-record-key.js +13 -0
  48. package/dist/shared-refusal.js +35 -0
  49. package/dist/shared-resolve.js +24 -0
  50. package/dist/shared-route-coverage.global-setup.js +187 -0
  51. package/dist/shared-scenario-failure.js +198 -0
  52. package/dist/shared-serve.js +407 -0
  53. package/dist/shared-service-images.js +10 -0
  54. package/dist/shared-services.global-setup.js +174 -0
  55. package/dist/shared-session-ledger.js +305 -0
  56. package/dist/shared-setup-entry.js +12 -0
  57. package/dist/shared-source-scan.js +133 -0
  58. package/dist/shared-spec-document.js +303 -0
  59. package/dist/shared-spec-grammar.js +66 -0
  60. package/dist/shared-specifications.js +26 -0
  61. package/dist/shared-specs-modules.js +1033 -0
  62. package/dist/shared-stub-reply.js +167 -0
  63. package/dist/shared-stub-server.js +142 -0
  64. package/dist/shared-temp-root.global-setup.js +73 -0
  65. package/dist/shared-test-baseline.js +26 -0
  66. package/dist/shared-test-section.js +58 -0
  67. package/dist/shared-testcontainers.adapter.js +306 -0
  68. package/dist/shared-tokens.js +55 -0
  69. package/dist/shared-yaml-document.js +119 -0
  70. package/dist/stubs.d.ts +2 -0
  71. package/dist/stubs.js +657 -0
  72. package/dist/temp-root.global-setup.d.ts +7 -0
  73. package/dist/temp-root.global-setup.js +1 -0
  74. package/dist/vitest.d.ts +93 -164
  75. package/dist/vitest.js +382 -550
  76. package/docs/README.md +82 -36
  77. package/docs/concepts/architecture.md +118 -0
  78. package/docs/concepts/conventions.md +333 -0
  79. package/docs/guides/api-specs.md +374 -0
  80. package/docs/guides/cli-specs.md +566 -0
  81. package/docs/guides/component-specs.md +252 -0
  82. package/docs/guides/develop.md +97 -0
  83. package/docs/guides/integration-specs.md +181 -0
  84. package/docs/guides/job-specs.md +180 -0
  85. package/docs/guides/lint-specs.md +174 -0
  86. package/docs/guides/mobile-specs.md +319 -0
  87. package/docs/guides/module-tests.md +149 -0
  88. package/docs/guides/release.md +44 -0
  89. package/docs/guides/spec-documents.md +189 -0
  90. package/docs/guides/website-specs.md +413 -0
  91. package/docs/reference/assertions.md +345 -0
  92. package/docs/reference/glossary.md +20 -0
  93. package/docs/reference/linting.md +483 -0
  94. package/docs/reference/services.md +236 -0
  95. package/docs/reference/stubs.md +500 -0
  96. package/docs/reference/time-and-doubles.md +137 -0
  97. package/docs/reference/tokens.md +258 -0
  98. package/docs/reference/visitor.md +221 -0
  99. package/docs/start/getting-started.md +187 -0
  100. package/docs/start/set-up-a-project.md +228 -0
  101. package/package.json +56 -36
  102. package/schema/spec.schema.json +43 -17
  103. package/dist/appium.adapter.js +0 -489
  104. package/dist/artifacts.js +0 -32
  105. package/dist/fixtures.js +0 -110
  106. package/dist/literate.js +0 -1747
  107. package/dist/match.js +0 -804
  108. package/dist/playwright.adapter.js +0 -438
  109. package/dist/server.js +0 -160
  110. package/dist/stream.js +0 -321
  111. package/dist/substring-warning.js +0 -282
  112. package/docs/01-architecture.md +0 -95
  113. package/docs/02-developing.md +0 -351
  114. package/docs/03-testing.md +0 -495
  115. package/docs/04-operating.md +0 -101
  116. package/docs/05-module-tests.md +0 -152
  117. package/docs/06-integration.md +0 -180
  118. package/docs/07-component.md +0 -246
  119. package/docs/08-website.md +0 -404
  120. package/docs/09-mobile.md +0 -270
  121. package/docs/10-api.md +0 -264
  122. package/docs/11-jobs.md +0 -157
  123. package/docs/12-cli.md +0 -677
  124. package/docs/13-elements.md +0 -248
  125. package/docs/14-assertions.md +0 -314
  126. package/docs/15-tokens.md +0 -253
  127. package/docs/16-contracts.md +0 -438
  128. package/docs/17-services.md +0 -188
  129. package/docs/18-conventions.md +0 -332
  130. package/docs/19-linting.md +0 -512
  131. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  132. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  133. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  134. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  135. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  136. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  137. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  138. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  139. package/docs/decisions/_template.md +0 -22
  140. package/docs/reference/classes/Matcher.md +0 -120
  141. package/docs/reference/classes/ProcessHandle.md +0 -368
  142. package/docs/reference/functions/defineContract.md +0 -39
  143. package/docs/reference/functions/defineContracts.md +0 -36
  144. package/docs/reference/functions/disabled.md +0 -28
  145. package/docs/reference/functions/enabled.md +0 -20
  146. package/docs/reference/functions/focused.md +0 -26
  147. package/docs/reference/functions/mockOf.md +0 -79
  148. package/docs/reference/functions/postgres.md +0 -31
  149. package/docs/reference/functions/process.md +0 -33
  150. package/docs/reference/functions/redis.md +0 -28
  151. package/docs/reference/functions/required.md +0 -41
  152. package/docs/reference/functions/selected.md +0 -26
  153. package/docs/reference/functions/sqlite.md +0 -33
  154. package/docs/reference/functions/testId.md +0 -20
  155. package/docs/reference/functions/text.md +0 -40
  156. package/docs/reference/functions/valued.md +0 -28
  157. package/docs/reference/functions/waitUntil.md +0 -46
  158. package/docs/reference/functions/within.md +0 -29
  159. package/docs/reference/index.md +0 -196
  160. package/docs/reference/interfaces/BaseResult.md +0 -87
  161. package/docs/reference/interfaces/CallResult.md +0 -160
  162. package/docs/reference/interfaces/CaptureScope.md +0 -82
  163. package/docs/reference/interfaces/CliResult.md +0 -253
  164. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  165. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  166. package/docs/reference/interfaces/FetchResult.md +0 -180
  167. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  168. package/docs/reference/interfaces/HttpResult.md +0 -126
  169. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  170. package/docs/reference/interfaces/PageResult.md +0 -356
  171. package/docs/reference/interfaces/RenderResult.md +0 -72
  172. package/docs/reference/interfaces/RenderedText.md +0 -110
  173. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  174. package/docs/reference/interfaces/ScreenResult.md +0 -128
  175. package/docs/reference/interfaces/TableAccessor.md +0 -26
  176. package/docs/reference/interfaces/TextAccessor.md +0 -96
  177. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  178. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  179. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  180. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  181. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  182. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  183. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  184. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  185. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  186. package/docs/reference/type-aliases/CliEnv.md +0 -11
  187. package/docs/reference/type-aliases/CliHandle.md +0 -80
  188. package/docs/reference/type-aliases/CliInput.md +0 -36
  189. package/docs/reference/type-aliases/CliOutput.md +0 -46
  190. package/docs/reference/type-aliases/CliPort.md +0 -58
  191. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  192. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  193. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  194. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  195. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  196. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  197. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  198. package/docs/reference/type-aliases/Contract.md +0 -68
  199. package/docs/reference/type-aliases/ContractInput.md +0 -13
  200. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  201. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  202. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  203. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  204. package/docs/reference/type-aliases/Contracts.md +0 -48
  205. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  206. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  207. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  208. package/docs/reference/type-aliases/DevicePort.md +0 -51
  209. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  210. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  211. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  212. package/docs/reference/type-aliases/DomMount.md +0 -21
  213. package/docs/reference/type-aliases/ElementKind.md +0 -27
  214. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  215. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  216. package/docs/reference/type-aliases/ElementRef.md +0 -128
  217. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  218. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  219. package/docs/reference/type-aliases/HonoApp.md +0 -31
  220. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  221. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  222. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  223. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  224. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  225. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  226. package/docs/reference/type-aliases/Intercept.md +0 -19
  227. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  228. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  229. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  230. package/docs/reference/type-aliases/JobHandle.md +0 -34
  231. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  232. package/docs/reference/type-aliases/JobsResult.md +0 -16
  233. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  234. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  235. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  236. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  237. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  238. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  239. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  240. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  241. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  242. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  243. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  244. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  245. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  246. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  247. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  248. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  249. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  250. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  251. package/docs/reference/type-aliases/MockPort.md +0 -10
  252. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  253. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  254. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  255. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  256. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  257. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  258. package/docs/reference/type-aliases/ServerPort.md +0 -36
  259. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  260. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  261. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  262. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  263. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  264. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  265. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  266. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  267. package/docs/reference/type-aliases/SpecKind.md +0 -8
  268. package/docs/reference/type-aliases/SpecRun.md +0 -108
  269. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  270. package/docs/reference/type-aliases/SpecStream.md +0 -32
  271. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  272. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  273. package/docs/reference/type-aliases/SseEvent.md +0 -58
  274. package/docs/reference/type-aliases/StreamBody.md +0 -61
  275. package/docs/reference/type-aliases/TextFilter.md +0 -22
  276. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  277. package/docs/reference/type-aliases/Visitor.md +0 -215
  278. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  279. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  280. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  281. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  282. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  283. package/docs/reference/variables/anthropic.md +0 -19
  284. package/docs/reference/variables/banner.md +0 -10
  285. package/docs/reference/variables/button.md +0 -10
  286. package/docs/reference/variables/clock.md +0 -16
  287. package/docs/reference/variables/complementary.md +0 -10
  288. package/docs/reference/variables/component.md +0 -10
  289. package/docs/reference/variables/content.md +0 -10
  290. package/docs/reference/variables/contentinfo.md +0 -10
  291. package/docs/reference/variables/dialog.md +0 -10
  292. package/docs/reference/variables/field.md +0 -10
  293. package/docs/reference/variables/form.md +0 -10
  294. package/docs/reference/variables/heading.md +0 -10
  295. package/docs/reference/variables/http.md +0 -40
  296. package/docs/reference/variables/intercept.md +0 -11
  297. package/docs/reference/variables/link.md +0 -10
  298. package/docs/reference/variables/listitem.md +0 -10
  299. package/docs/reference/variables/main.md +0 -10
  300. package/docs/reference/variables/match.md +0 -8
  301. package/docs/reference/variables/navigation.md +0 -10
  302. package/docs/reference/variables/openai.md +0 -21
  303. package/docs/reference/variables/option.md +0 -17
  304. package/docs/reference/variables/region.md +0 -10
  305. package/docs/reference/variables/row.md +0 -10
  306. package/docs/reference/variables/search.md +0 -10
  307. package/docs/reference/variables/specification.md +0 -8
  308. package/docs/reference/variables/status.md +0 -10
  309. package/docs/reference/variables/table.md +0 -10
@@ -1,489 +0,0 @@
1
- import { i as AmbiguousElementError, n as warnSubstringOnly, o as formatElement } 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
- await 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 };
package/dist/fixtures.js DELETED
@@ -1,110 +0,0 @@
1
- import { basename, dirname, resolve } from "node:path";
2
- //#region src/model/chain/ground.ts
3
- /**
4
- * The GROUND a row of specs stands on — the naming law of a spec tree.
5
- *
6
- * In a row of specs, a MEMBER is a spec: `<aspect>.test.ts`, `<case>.spec.yaml`,
7
- * `<name>.specification.ts`, and the facet/domain folders that hold them.
8
- * Everything else in that row is what the members STAND ON — inert material the
9
- * framework resolves BY PATH, never imported, never executed — and it carries a
10
- * LEADING UNDERSCORE. A spec's own folder never does.
11
- *
12
- * Four names, and only four: `_fixtures/` (file state), `_expected/` (goldens),
13
- * `_requests/` (inputs), `_seeds/` (database state). `contracts/` is NOT ground —
14
- * a contract is TypeScript a spec imports, behaviour composed with the spec's own,
15
- * so it stays a member. `docker/` is not ground either: it sits at the PROJECT
16
- * root beside `package.json`, outside any row of specs.
17
- *
18
- * This module is the single home of those names. Every resolver, matcher, lint
19
- * rule and checker pass reads them from here, so the runtime and the static
20
- * channels cannot disagree about where a spec's ground lives.
21
- */
22
- /** Goldens: every expected fixture, flat, extension included. */
23
- const GROUND_EXPECTED = "_expected";
24
- /** File state copied into a working directory by `.fixture()`. */
25
- const GROUND_FIXTURES = "_fixtures";
26
- /** Inputs: complete `.http` request documents. */
27
- const GROUND_REQUESTS = "_requests";
28
- /** Database state: `.sql` fragments loaded by `.seed()`. */
29
- const GROUND_SEEDS = "_seeds";
30
- /** The four ground directory names, in the order they read. */
31
- const GROUND_DIRS = [
32
- GROUND_EXPECTED,
33
- GROUND_FIXTURES,
34
- GROUND_REQUESTS,
35
- GROUND_SEEDS
36
- ];
37
- //#endregion
38
- //#region src/model/chain/fixtures.ts
39
- /**
40
- * Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
41
- *
42
- * A fixture path is one of two shapes:
43
- *
44
- * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/_fixtures/<rest>`,
45
- * where `<specs-root>` is the nearest ancestor directory named `specs`.
46
- * - `<path>` (no marker) — feature-local, at `<test-dir>/_fixtures/<path>`.
47
- *
48
- * The leading underscore is the ground rule of a spec tree ({@link GROUND_FIXTURES}).
49
- * Any other `$`-prefixed marker is a usage error (listed against the known
50
- * markers). Copy semantics mirror rsync's trailing-slash rule (see
51
- * {@link copyPlan}).
52
- */
53
- /**
54
- * Markers understood in a `.fixture()` path. Extend here + in
55
- * {@link resolveFixtureSource}. Exported so the lint layer (rule
56
- * `jterrazz/b2-known-fixture-marker`) validates literals against the same list.
57
- */
58
- const KNOWN_FIXTURE_MARKERS = ["$FIXTURES"];
59
- /**
60
- * Walk up from `startDir` to the nearest ancestor directory named `specs`.
61
- * Throws with guidance if none is found up to the filesystem root — the
62
- * `$FIXTURES` marker is meaningless without a specs root.
63
- */
64
- function discoverSpecsRoot(startDir) {
65
- let dir = startDir;
66
- for (;;) {
67
- if (basename(dir) === "specs") return dir;
68
- const parent = dirname(dir);
69
- if (parent === dir) break;
70
- dir = parent;
71
- }
72
- throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root>/${GROUND_FIXTURES}, but no directory named "specs" was found walking up from ${startDir}. Move the specification under a specs/ directory, or use a feature-local ${GROUND_FIXTURES}/ path (no $FIXTURES marker).`);
73
- }
74
- /**
75
- * Resolve the absolute source path of a fixture, applying marker resolution.
76
- * The trailing slash (spread semantics) is preserved by callers via
77
- * {@link copyPlan} — it is irrelevant to source resolution.
78
- */
79
- function resolveFixtureSource(path, testDir) {
80
- const clean = path.replace(/\/+$/u, "");
81
- if (clean === "$FIXTURES" || path.startsWith("$FIXTURES/")) {
82
- const rest = clean.slice(9).replace(/^\/+/u, "");
83
- return resolve(discoverSpecsRoot(testDir), GROUND_FIXTURES, rest);
84
- }
85
- if (path.startsWith("$")) {
86
- const marker = clean.split("/")[0];
87
- throw new Error(`.fixture("${path}"): unknown marker "${marker}". Known markers: ${KNOWN_FIXTURE_MARKERS.join(", ")}. A path without a marker is feature-local (resolved under <test-dir>/${GROUND_FIXTURES}/).`);
88
- }
89
- return resolve(testDir, GROUND_FIXTURES, clean);
90
- }
91
- /**
92
- * Compute the source + destination for copying a fixture into the working
93
- * directory. Copy semantics follow rsync's trailing-slash rule:
94
- *
95
- * - `dir/` (trailing slash) — the CONTENTS are spread into the cwd.
96
- * - `dir` (no slash, a directory) — the directory is copied as `<cwd>/<basename>`.
97
- * - `file` — copied as `<cwd>/<basename>`.
98
- *
99
- * Chained `.fixture()` calls layer in order: a later fixture overwrites files
100
- * written by an earlier one.
101
- */
102
- function copyPlan(path, testDir, workDir) {
103
- const src = resolveFixtureSource(path, testDir);
104
- return {
105
- dest: /\/+$/u.test(path) ? workDir : resolve(workDir, basename(src)),
106
- src
107
- };
108
- }
109
- //#endregion
110
- export { GROUND_FIXTURES as a, GROUND_EXPECTED as i, resolveFixtureSource as n, GROUND_REQUESTS as o, GROUND_DIRS as r, GROUND_SEEDS as s, copyPlan as t };