@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
package/dist/vitest.js CHANGED
@@ -1,232 +1,55 @@
1
- import { i as GROUND_EXPECTED } from "./fixtures.js";
2
- import { i as SCREENSHOTS_DIR, n as COVERAGE_DIR, o as VITEST_ARTIFACTS_DIR, r as REPORTER_OUTPUT_FILES, t as ATTACHMENTS_DIR } from "./artifacts.js";
3
- import { n as specDescription } from "./literate.js";
1
+ import { a as callerFrames, r as writeInPlace } from "./shared-in-place.js";
2
+ import { t as isRecord } from "./shared-json-shape.js";
3
+ import { n as refusal$1, t as invariant } from "./shared-refusal.js";
4
+ import { i as renderText, n as literate, o as resolveSpecPath } from "./shared-literate-plugin.js";
5
+ import { u as shouldUpdateSnapshots } from "./shared-ledger.js";
6
+ import { i as COVERAGE_DIR, l as VITEST_ARTIFACTS_DIR, n as ATTACHMENTS_DIR, o as REPORTER_OUTPUT_FILES, r as BLOB_DIR, s as SCREENSHOTS_DIR, t as ARTIFACTS_DIR } from "./shared-artifacts.js";
7
+ import { t as derivedProjects } from "./shared-derived-projects.js";
8
+ import { t as TEST_SECTION } from "./shared-test-section.js";
9
+ import { r as servicesSetup } from "./shared-services.global-setup.js";
10
+ import { r as tempRootSetup } from "./shared-temp-root.global-setup.js";
11
+ import { inPlaceSetup } from "./in-place.global-setup.js";
12
+ import { t as routeCoverageSetup } from "./shared-route-coverage.global-setup.js";
13
+ import { mobileSessionSetup } from "./mobile-session.global-setup.js";
4
14
  import { createRequire } from "node:module";
15
+ import { GROUND_GLOB, readSettings } from "@jterrazz/typescript/rules";
5
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
17
  import { basename, dirname, isAbsolute, resolve } from "node:path";
7
18
  import { fileURLToPath } from "node:url";
8
19
  import { configDefaults, mergeConfig } from "vitest/config";
9
- //#region src/runner/literate-plugin.ts
10
- /**
11
- * The `literate()` vite plugin — door one of the spec-document format.
12
- *
13
- * It makes a `<case>.spec.yaml` file a TEST FILE: the glob joins the project's
14
- * test include, and each document is transformed into a one-test module that
15
- * runs the file through the registered `cli` runner. The runner shows the
16
- * document's path as the file and its `description:` as the title, so a failing
17
- * scenario is opened where it is written — not in a `.test.ts` that merely
18
- * points at it.
19
- *
20
- * ```typescript
21
- * // vitest.config.ts
22
- * import { defineConfig } from 'vitest/config';
23
- * import { literate } from '@jterrazz/test/vitest';
24
- *
25
- * export default defineConfig({
26
- * plugins: [literate({ specification: './specs/cli/cli.specification.ts' })],
27
- * });
28
- * ```
29
- */
30
- /** The `.spec.yaml` extension, matched on the id vite hands the plugin. */
31
- const SPEC_FILE = /\.spec\.yaml(?:\?.*)?$/u;
32
- /**
33
- * The module a `<case>.spec.yaml` becomes: one `test()` whose title is the
34
- * document's `description:` and whose body runs the whole file — its ground,
35
- * its servers, every run asserted.
36
- *
37
- * @internal Exported for unit tests.
38
- */
39
- function literateModule(content, filePath, specificationPath) {
40
- const title = specDescription(content, filePath);
41
- return [
42
- "import { test } from 'vitest';",
43
- `import { cli } from ${JSON.stringify(specificationPath)};`,
44
- "",
45
- `test(${JSON.stringify(title)}, async () => {`,
46
- ` await cli.run(${JSON.stringify(filePath)});`,
47
- "});",
48
- ""
49
- ].join("\n");
50
- }
51
- /** Strip vite's query suffix (`?v=…`) from a module id. */
52
- function cleanId(id) {
53
- return id.split("?")[0] ?? id;
54
- }
55
- function literate(options) {
56
- const include = options.include ?? ["**/*.spec.yaml"];
57
- let root = process.cwd();
58
- return {
59
- config: () => ({ test: { include } }),
60
- configResolved: (resolved) => {
61
- root = resolved.root;
62
- },
63
- enforce: "pre",
64
- load: (id) => {
65
- if (!SPEC_FILE.test(id)) return null;
66
- const filePath = cleanId(id);
67
- const specificationPath = isAbsolute(options.specification) ? options.specification : resolve(root, options.specification);
68
- return literateModule(readFileSync(filePath, "utf8"), filePath, specificationPath);
69
- },
70
- name: "jterrazz-test:literate"
71
- };
72
- }
73
- //#endregion
74
- //#region src/runner/preset.ts
75
- /**
76
- * `defineSpecConfig()` — the shared vitest config every repository of the
77
- * ecosystem starts from.
78
- *
79
- * Fourteen hand-rolled `vitest.config.ts` files had converged on the same
80
- * handful of settings and disagreed on the rest by accident; the preset is
81
- * their common ground, stated once. A consumer writes ONE call and keeps
82
- * everything vitest offers: what it passes is a plain vite/vitest config,
83
- * merged OVER the defaults, so any of them is overridable in place.
84
- *
85
- * ```typescript
86
- * // vitest.config.ts
87
- * import { defineSpecConfig } from '@jterrazz/test/vitest';
88
- *
89
- * export default defineSpecConfig({
90
- * literate: { specification: './specs/cli/cli.specification.ts' },
91
- * test: {
92
- * projects: [
93
- * { test: { include: ['src/**\/*.test.ts'], name: 'unit' } },
94
- * { test: { include: ['specs/api/**\/*.test.ts'], name: 'api' } },
95
- * ],
96
- * },
97
- * });
98
- * ```
99
- */
100
- /**
101
- * A run's budget. Vitest's own 5s test / 10s hook defaults were raised by every
102
- * repository that starts real infrastructure, and none of them meant 5s — a
103
- * container boot, a `prisma db push` or a `next build` all outlive it. 30s is
104
- * the value the ecosystem's configs already converged on; a suite that needs
105
- * more (a simulator cold boot, a real image build) states its own per project.
106
- */
20
+ //#region src/vitest/project.ts
107
21
  const TEST_TIMEOUT_MS = 3e4;
108
22
  const HOOK_TIMEOUT_MS = 3e4;
109
- /**
110
- * What a spec STANDS ON is never itself a test: `_fixtures/` holds inputs —
111
- * deliberately-wrong twins, fixture projects a spec lints — and collecting them
112
- * runs a repository's own counter-examples as if they were its suite. Every
113
- * config that ever hit it added the same line by hand.
114
- *
115
- * Merged, not replaced: vite concatenates arrays, so a consumer's `exclude`
116
- * ADDS to this list and never has to spread `configDefaults.exclude` again.
117
- */
118
- const EXCLUDE = [...configDefaults.exclude, "**/_fixtures/**"];
119
- /**
120
- * What a test may never carry into the next one, and the one policy the
121
- * framework states rather than lints.
122
- *
123
- * `retry: 0` is the policy: a flaky test is fixed or deleted, never re-rolled
124
- * until it passes. A retry turns a real defect into a slow one and hides the
125
- * day it becomes permanent.
126
- *
127
- * The three restores are the other half of the same idea. `vi.spyOn`,
128
- * `vi.stubGlobal` and `vi.stubEnv` each own a teardown, and a test that forgets
129
- * one leaks into whatever runs next — a failure that appears in a FILE that did
130
- * nothing wrong and disappears when that file is run alone. Turning them on
131
- * here is what lets `clock.at()` and `vi.stubEnv()` be written without an
132
- * `afterEach`, which is the shape rule J6w asks for.
133
- */
23
+ /** Ground folders hold inputs and deliberately-wrong twins, never tests, and `.artifacts/` copies of the project (a mutation sandbox); vite concatenates, so a consumer's `exclude` adds to this. */
24
+ const EXCLUDE = [
25
+ ...configDefaults.exclude,
26
+ `${ARTIFACTS_DIR}/**`,
27
+ GROUND_GLOB
28
+ ];
134
29
  const HYGIENE = {
135
30
  restoreMocks: true,
136
31
  retry: 0,
137
32
  unstubEnvs: true,
138
33
  unstubGlobals: true
139
34
  };
140
- /**
141
- * Defaults a vitest PROJECT accepts. `coverage` and `reporters` are root-only
142
- * options (vitest omits them from a project's config), so they are absent here
143
- * on purpose — setting them would be silently dropped.
144
- *
145
- * Exported for the project helpers, which build their canonical project ON
146
- * these rather than beside them: a helper's project must carry the same
147
- * budgets and the same artefact directory as a hand-written one.
148
- *
149
- * @internal
150
- */
35
+ /** `coverage` and `reporters` are root-only: vitest drops them from a project. */
151
36
  function projectDefaults() {
152
37
  return {
153
38
  cacheDir: VITEST_ARTIFACTS_DIR,
154
39
  test: {
155
40
  attachmentsDir: ATTACHMENTS_DIR,
156
41
  exclude: EXCLUDE,
42
+ globalSetup: [inPlaceSetup(), tempRootSetup()],
157
43
  hookTimeout: HOOK_TIMEOUT_MS,
158
44
  ...HYGIENE,
159
45
  testTimeout: TEST_TIMEOUT_MS
160
46
  }
161
47
  };
162
48
  }
163
- /**
164
- * Coverage, when `--coverage` asks for it.
165
- *
166
- * `v8` because it is the runtime's own counter: no instrumentation pass, no
167
- * second transform of every file, and the numbers do not move when the bundler
168
- * does. The provider is an OPTIONAL peer (`@vitest/coverage-v8`), so a project
169
- * that never asks for coverage never installs it.
170
- *
171
- * `json-summary` is not a taste: it is the machine-readable report the ratchet
172
- * reads. `text` is for the human running the command, and `html` for the one
173
- * chasing a line. All three land under `.artifacts/vitest/coverage/`, the one
174
- * directory the toolchain's page owns.
175
- *
176
- * What is EXCLUDED is the shape of the answer, not a way to raise the number:
177
- * a type-only module has nothing to execute (v8 reports it as 0/0 or, worse,
178
- * as uncovered lines that cannot be covered), and the CLI entries are proven
179
- * by running the binary, not by importing it.
180
- */
181
- const COVERAGE = {
182
- exclude: [
183
- "**/node_modules/**",
184
- "specs/**",
185
- "**/_fixtures/**",
186
- "**/*.d.ts",
187
- "**/*.test-d.ts",
188
- "**/*.fixtures.ts",
189
- "dist/**",
190
- ".artifacts/**",
191
- "**/*.config.ts",
192
- "**/*.config.mts"
193
- ],
194
- provider: "v8",
195
- reporter: [
196
- "text",
197
- "html",
198
- "json-summary"
199
- ],
200
- reportsDirectory: COVERAGE_DIR
201
- };
202
- /** Defaults for the root config — the project ones, plus what only a root carries. */
203
- function rootDefaults() {
204
- return {
205
- cacheDir: VITEST_ARTIFACTS_DIR,
206
- test: {
207
- attachmentsDir: ATTACHMENTS_DIR,
208
- coverage: COVERAGE,
209
- exclude: EXCLUDE,
210
- hookTimeout: HOOK_TIMEOUT_MS,
211
- ...HYGIENE,
212
- outputFile: REPORTER_OUTPUT_FILES,
213
- testTimeout: TEST_TIMEOUT_MS
214
- }
215
- };
216
- }
217
- /** A glob list with its repeats taken out, or nothing where there is no list. */
218
49
  function eachOnce(globs) {
219
50
  return globs === void 0 ? void 0 : [...new Set(globs)];
220
51
  }
221
- /**
222
- * The glob lists vite CONCATENATES rather than overrides, each glob kept once.
223
- *
224
- * A project built ON the defaults and then merged INTO them by
225
- * `defineSpecConfig` carried every default twice. Glob matching is idempotent,
226
- * so nothing collected differently — but the no-tests banner is the one place a
227
- * consumer ever reads that list, and it read as three merges of the same three
228
- * globs.
229
- */
52
+ /** Vite concatenates these lists: each glob is kept once. */
230
53
  function statedOnce(test) {
231
54
  const exclude = eachOnce(test.exclude);
232
55
  const include = eachOnce(test.include);
@@ -236,11 +59,6 @@ function statedOnce(test) {
236
59
  ...include === void 0 ? {} : { include }
237
60
  };
238
61
  }
239
- /**
240
- * Build a project ON the defaults — the ONE way a project is built here.
241
- *
242
- * @internal
243
- */
244
62
  function onProjectDefaults(stated) {
245
63
  const merged = mergeConfig(projectDefaults(), stated);
246
64
  return merged.test === void 0 ? merged : {
@@ -248,132 +66,139 @@ function onProjectDefaults(stated) {
248
66
  test: statedOnce(merged.test)
249
67
  };
250
68
  }
251
- /**
252
- * Is every project an inline object — one this preset merged its defaults into?
253
- * A project named by a glob or handed as a promise carries a config this call
254
- * never saw, and the root's exclusions are the only ones it gets from here.
255
- */
256
- function allInline(projects) {
257
- return projects.every((project) => typeof project === "object" && project !== null && !("then" in project));
258
- }
259
- /**
260
- * The root's exclusions, beside projects that already carry them.
261
- *
262
- * With `projects`, the root collects nothing itself: its `exclude` only travels
263
- * INTO each project, where vitest CONCATENATES it with the project's own — so
264
- * every preset glob reached the banner twice. The preset's own are dropped
265
- * here, since each inline project states them in full; what the CONSUMER stated
266
- * at the root stays, because no project holds a copy of that.
267
- */
268
- function rootExcludeBeside(projects, exclude) {
269
- return allInline(projects) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
270
- }
271
- /**
272
- * A project inherits NOTHING from the root `test` block — vitest resolves each
273
- * project as its own config — so the defaults are merged into every inline one.
274
- * A project declared as a glob string, a promise or a function is handed back
275
- * untouched: there is no object to merge into.
276
- */
277
- function withProjectDefaults(project) {
278
- if (typeof project !== "object" || project === null || "then" in project) return project;
279
- return onProjectDefaults(project);
280
- }
281
- function defineSpecConfig(config = {}) {
282
- const { literate: literateOptions, ...userConfig } = config;
283
- const stated = literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig;
284
- const merged = mergeConfig(rootDefaults(), stated);
285
- const root = merged.test === void 0 ? void 0 : statedOnce(merged.test);
286
- if (root === void 0) return merged;
287
- const { projects } = root;
288
- if (projects === void 0) return {
289
- ...merged,
290
- test: root
291
- };
292
- const inline = projects.map(withProjectDefaults);
69
+ /** A stated `include` or `exclude` replaces the canonical one; the group is 0 until a specification states its own. */
70
+ function projectTest(name, options, canonical) {
71
+ const exclude = options.exclude ?? canonical.exclude;
293
72
  return {
294
- ...merged,
295
- test: {
296
- ...root,
297
- ...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
298
- projects: inline
73
+ ...exclude === void 0 ? {} : { exclude },
74
+ include: options.include ?? canonical.include,
75
+ ...options.serial === true ? { fileParallelism: false } : {},
76
+ name,
77
+ sequence: { groupOrder: 0 },
78
+ ...options.timeout === void 0 ? {} : {
79
+ hookTimeout: options.timeout,
80
+ testTimeout: options.timeout
299
81
  }
300
82
  };
301
83
  }
302
- //#endregion
303
- //#region src/runner/facet-project.ts
304
- /** The globs a node facet collects, and the ones a caller stated instead. */
305
- function facetProject(facet, options) {
306
- return onProjectDefaults({ test: {
307
- ...options.exclude === void 0 ? {} : { exclude: options.exclude },
308
- include: options.include ?? [`specs/${facet}/**/*.spec.ts`],
309
- ...options.serial === true ? { fileParallelism: false } : {},
310
- name: facet,
311
- sequence: { groupOrder: 0 },
312
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
84
+ function specificationProject(specification, options) {
85
+ return onProjectDefaults({ test: projectTest(specification, options, { include: [`specs/${specification}/**/*.spec.ts`] }) });
86
+ }
87
+ function inProcessProject(specification, options) {
88
+ const project = specificationProject(specification, options);
89
+ return options.runner === void 0 ? project : mergeConfig(project, { test: {
90
+ isolate: false,
91
+ setupFiles: [options.runner]
313
92
  } });
314
93
  }
315
94
  //#endregion
316
- //#region src/facets/api/api.project.ts
317
- /** `api()` — the app met through HTTP. */
95
+ //#region src/specifications/api/api.project.ts
318
96
  function api(options = {}) {
319
- return facetProject("api", options);
97
+ return mergeConfig(inProcessProject("api", options), { test: { globalSetup: [servicesSetup(), routeCoverageSetup()] } });
320
98
  }
321
99
  //#endregion
322
- //#region src/facets/cli/cli.project.ts
323
- /** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
324
- const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
325
- const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
326
- /**
327
- * A cli project collects the facet's `.test.ts` files AND its documents: a
328
- * `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
329
- * so the plugin's glob has to join the include of the project that collects
330
- * them — which is this one, never the root config.
331
- */
100
+ //#region src/specifications/cli/cli.project.ts
101
+ /** A cli project: the specification's `.spec.ts` files and its documents (docs/guides/spec-documents.md); the plugin's glob joins this project's include, never the root config. */
332
102
  function cli(options = {}) {
333
- const { literate: literateOptions, ...common } = options;
334
- const project = facetProject("cli", common);
335
- if (literateOptions === false) return project;
336
- const documents = literateOptions?.include ?? CLI_DOCUMENTS;
103
+ const { literate: door, ...common } = options;
104
+ const project = specificationProject("cli", common);
105
+ if (door === false) return project;
337
106
  return mergeConfig(project, { plugins: [literate({
338
- include: documents,
339
- specification: literateOptions?.specification ?? CLI_SPECIFICATION
107
+ include: door?.include ?? ["specs/cli/**/*.spec.yaml"],
108
+ runner: door?.runner ?? "./specs/cli/cli.specification.ts"
340
109
  })] });
341
110
  }
342
111
  //#endregion
343
- //#region src/seams/vitest-browser/commands.ts
344
- /** Where a golden sits: `_expected/<name>` beside the test that asked for it. */
112
+ //#region src/stubs/delivery/browser-page/worker-script.ts
113
+ /** From this package's own install: `@vitest/browser` resolves it only where the installer hoists, and a consumer declaring `msw` is what `no-redundant-test-dependency` forbids. */
114
+ function mswWorkerScript() {
115
+ return createRequire(import.meta.url).resolve("msw/mockServiceWorker.js");
116
+ }
117
+ //#endregion
118
+ //#region src/specifications/component/golden/commands.ts
119
+ function testFile(testPath) {
120
+ if (testPath === void 0) throw invariant("a golden command reached the server with no test path");
121
+ return testPath;
122
+ }
345
123
  function goldenPath(testPath, name) {
346
- if (testPath === void 0) throw new Error("golden: the command carries no test path — a golden is read from a test file.");
347
- return resolve(dirname(testPath), GROUND_EXPECTED, name);
124
+ return resolveSpecPath("toMatch", name, dirname(testFile(testPath)));
348
125
  }
349
- /** The ARIA snapshot of the rendered `<body>`, as YAML. */
350
126
  const ariaTree = async (context) => {
351
127
  return await (await context.frame()).locator("body").ariaSnapshot();
352
128
  };
353
- /** The golden's current content, or `null` when it does not exist yet. */
354
129
  const goldenRead = (context, name) => {
355
130
  const path = goldenPath(context.testPath, name);
356
131
  return existsSync(path) ? readFileSync(path, "utf8") : null;
357
132
  };
358
- /** Write the golden — update mode's half, and the path it wrote, for the message. */
359
133
  const goldenWrite = (context, name, content) => {
360
134
  const path = goldenPath(context.testPath, name);
361
135
  mkdirSync(dirname(path), { recursive: true });
362
- writeFileSync(path, content, "utf8");
136
+ writeFileSync(path, content);
363
137
  return path;
364
138
  };
365
- /**
366
- * The commands, as `component()` registers them — one list, so the names the
367
- * page calls and the names the project registers cannot drift.
368
- */
139
+ const inPlaceWrite = (context, stack, text) => {
140
+ const file = testFile(context.testPath);
141
+ const frame = context.project.browser?.parseStacktrace(stack).find((candidate) => candidate.file === file);
142
+ if (frame === void 0) throw new Error(refusal$1({
143
+ fix: "call toMatch() from the test file itself, or compare to a golden file: toMatch(file(...))",
144
+ found: "update mode: no frame of this toMatch call is the test file, so the value in place has nowhere to be written",
145
+ read: "docs/reference/tokens.md#a-value-written-in-place-is-rewritten-in-the-test",
146
+ why: "A value written in place is rewritten in the test that states it."
147
+ }));
148
+ writeInPlace({
149
+ column: frame.column,
150
+ file,
151
+ line: frame.line
152
+ }, (indent) => renderText(text, indent), context.project.getProvidedContext().inPlaceRewrites);
153
+ return `${file}:${frame.line}`;
154
+ };
155
+ /** One list, so the names the page calls and the names the project registers cannot drift. */
369
156
  const COMPONENT_COMMANDS = {
370
157
  ariaTree,
371
158
  goldenRead,
372
- goldenWrite
159
+ goldenWrite,
160
+ inPlaceWrite
373
161
  };
374
162
  //#endregion
375
- //#region src/facets/component/component.project.ts
376
- /** The pipeline keys a consumer's Vite config contributes, and nothing else. */
163
+ //#region src/specifications/component/peers/peer-versions.ts
164
+ const requireFrom$1 = createRequire(import.meta.url);
165
+ function versionOf(specifier) {
166
+ const path = requireFrom$1.resolve(`${specifier}/package.json`);
167
+ const manifest = JSON.parse(String(readFileSync(path)));
168
+ if (typeof manifest === "object" && manifest !== null && "version" in manifest) return String(manifest.version);
169
+ throw new Error(refusal$1({
170
+ fix: `reinstall ${specifier}: \`npm install ${specifier}\``,
171
+ found: `the installed ${specifier} manifest carries no version`,
172
+ read: "docs/start/set-up-a-project.md#what-to-install",
173
+ why: "The component project reads the version of the peers it configures from their installed manifests."
174
+ }));
175
+ }
176
+ /** Vite 8 transforms with oxc and warns on every `esbuild` option; Vite 6 and 7 have no `oxc` key. */
177
+ function transformKey() {
178
+ return Number.parseInt(versionOf("vite").split(".")[0] ?? "", 10) >= 8 ? "oxc" : "esbuild";
179
+ }
180
+ /** `@vitest/browser-playwright` peers vitest on an exact version; a mismatch fails deep in the tester naming neither. */
181
+ function assertProviderPin(runner, provider) {
182
+ if (provider === null) throw new Error(refusal$1("seam-is-an-optional-peer", {
183
+ fix: `npm i -D @vitest/browser-playwright@${runner} playwright`,
184
+ found: "component() requires `@vitest/browser-playwright`, and it is not installed"
185
+ }));
186
+ if (runner !== provider) throw new Error(refusal$1({
187
+ fix: `bump both to the same version in one change: npm i -D vitest@${provider} @vitest/browser-playwright@${provider}`,
188
+ found: `component(): @vitest/browser-playwright is ${provider}, and vitest is ${runner}`,
189
+ read: "docs/start/set-up-a-project.md#what-to-install",
190
+ why: "`@vitest/browser-playwright` peers vitest on an exact version."
191
+ }));
192
+ }
193
+ function installedVersion(specifier) {
194
+ try {
195
+ return versionOf(specifier);
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+ //#endregion
201
+ //#region src/specifications/component/component.project.ts
377
202
  const PIPELINE_KEYS = [
378
203
  "assetsInclude",
379
204
  "css",
@@ -381,44 +206,25 @@ const PIPELINE_KEYS = [
381
206
  "envPrefix",
382
207
  "esbuild",
383
208
  "json",
209
+ "optimizeDeps",
384
210
  "oxc",
385
211
  "plugins",
386
212
  "resolve"
387
213
  ];
388
- /**
389
- * The seam's own runtime, pre-bundled so a cold cache never reloads mid-run.
390
- *
391
- * `msw` and `msw/browser` are NOT here: `@vitest/browser` already puts both in
392
- * `optimizeDeps.exclude`, and Vite's esbuild optimizer (6 and 7) treats an
393
- * entry that is both included and excluded as fatal — dependency optimisation
394
- * throws before a single test runs.
395
- */
396
- const SEAM_DEPENDENCIES = [
214
+ const PAGE_DEPENDENCIES = [
397
215
  "react",
398
216
  "react-dom/client",
399
217
  "react/jsx-dev-runtime",
400
218
  "react/jsx-runtime",
401
- "vitest-browser-react"
219
+ "vitest-browser-react",
220
+ "vitest-mock-extended"
402
221
  ];
403
222
  const requireFrom = createRequire(import.meta.url);
404
- /** This module's own file — the frames the caller search walks past. */
405
223
  const HELPERS_FILE = import.meta.filename;
406
- /** A stack frame's file, in either of the two shapes V8 prints it in. */
407
- const STACK_FRAME = /(?:\((?<wrapped>[^()]+):\d+:\d+\)|at (?<bare>[^()\s]+):\d+:\d+)$/u;
408
- /** A config vite bundled before importing it — `<its name>.timestamp-<hash>.mjs`. */
409
224
  const BUNDLED_CONFIG = /^(?<name>.+)\.timestamp-[^.]+\.mjs$/u;
410
- /** The file of the first frame below this module — who called the helper. */
411
225
  function callerFile() {
412
- const { stack } = /* @__PURE__ */ new Error("@jterrazz/test: reading the call site of a project helper");
413
- for (const frame of stack?.split("\n").slice(1) ?? []) {
414
- const groups = STACK_FRAME.exec(frame.trim())?.groups;
415
- const location = groups?.wrapped ?? groups?.bare;
416
- if (location === void 0 || location.startsWith("node:")) continue;
417
- const file = location.startsWith("file:") ? fileURLToPath(location) : location;
418
- if (file !== HELPERS_FILE) return file;
419
- }
226
+ return callerFrames().find(({ file }) => file !== HELPERS_FILE)?.file;
420
227
  }
421
- /** The nearest directory at or above the cwd holding a file of this name. */
422
228
  function nearestHolding(name) {
423
229
  let directory = process.cwd();
424
230
  for (;;) {
@@ -428,27 +234,6 @@ function nearestHolding(name) {
428
234
  directory = parent;
429
235
  }
430
236
  }
431
- /**
432
- * The directory of the CONFIG that called a helper — what the relative paths
433
- * it was handed mean.
434
- *
435
- * Not `process.cwd()`: a `vitest.config.ts` is read by more tools than the
436
- * runner, and knip and `typescript check` load every config of a repository
437
- * from the repository ROOT — `component({ vite: './web/vite.config.ts' })`
438
- * written beside an app resolved against the wrong tree and threw before a
439
- * single test ran. `import.meta` cannot answer either: it describes THIS
440
- * module. And vitest hands a project neither its root nor its config file — a
441
- * project function is called with `{ command, mode, isPreview, isSsrBuild }`
442
- * and nothing else.
443
- *
444
- * So the call site is read off the stack, where a loader that keeps a config's
445
- * identity (node, tsx, jiti — what those tools use) names the file itself.
446
- * Vite's own loader does not: it BUNDLES the config into
447
- * `node_modules/.vite-temp/` and only the config's NAME survives the move. The
448
- * name is what is then looked for upward from the cwd, because that loader is
449
- * the runner's and a runner is started in the tree it tests. A config loaded
450
- * by the runner from another tree entirely states `root` itself.
451
- */
452
237
  function callerDirectory() {
453
238
  const file = callerFile();
454
239
  if (file === void 0) return process.cwd();
@@ -456,7 +241,6 @@ function callerDirectory() {
456
241
  if (bundled === void 0 || basename(dirname(file)) !== ".vite-temp") return dirname(file);
457
242
  return nearestHolding(bundled) ?? process.cwd();
458
243
  }
459
- /** Is this specifier installed here? An absent optional peer is simply not pre-bundled. */
460
244
  function resolves(specifier) {
461
245
  try {
462
246
  requireFrom.resolve(specifier);
@@ -465,20 +249,10 @@ function resolves(specifier) {
465
249
  return false;
466
250
  }
467
251
  }
468
- /**
469
- * Serve msw's worker script from the package's OWN install, at the path the
470
- * worker asks for, with the scope header a worker registered at `/` needs.
471
- *
472
- * `@vitest/browser` resolves `/mockServiceWorker.js` too, but from ITS own
473
- * directory — and it does not depend on `msw`, so that resolution only lands
474
- * where the installer hoists. The package that OWNS the dependency is the one
475
- * that can always find it, and it is this one. Without either, a consumer runs
476
- * `msw init` and declares `msw`, which F8 forbids.
477
- */
478
252
  function mswWorkerPlugin() {
479
253
  return {
480
254
  configureServer(server) {
481
- const script = requireFrom.resolve("msw/mockServiceWorker.js");
255
+ const script = mswWorkerScript();
482
256
  server.middlewares.use("/mockServiceWorker.js", (_request, response) => {
483
257
  response.setHeader("content-type", "text/javascript");
484
258
  response.setHeader("service-worker-allowed", "/");
@@ -488,16 +262,7 @@ function mswWorkerPlugin() {
488
262
  name: "@jterrazz/test:msw-worker"
489
263
  };
490
264
  }
491
- /**
492
- * Keep an app's Vite PIPELINE and drop everything that says where the app
493
- * lives.
494
- *
495
- * An allow-list, not a drop-list: a consumer's config carries keys the
496
- * framework has never heard of — `environments`, `ssr`, `appType`, `publicDir`
497
- * — and one of them re-roots the run and the project collects nothing. Naming
498
- * what the pipeline IS keeps an unknown key from leaking in. The consumer's
499
- * `plugins` are CONCATENATED after the seam's, never assigned over them.
500
- */
265
+ const isPluginList = (plugins) => Array.isArray(plugins);
501
266
  async function reroot(source, root) {
502
267
  const environment = {
503
268
  command: "serve",
@@ -505,144 +270,90 @@ async function reroot(source, root) {
505
270
  mode: "test"
506
271
  };
507
272
  let loaded;
508
- let from = "the value it was given";
273
+ let found;
274
+ let fix;
509
275
  if (typeof source === "string") {
510
276
  const { loadConfigFromFile } = await import("vite");
511
- from = isAbsolute(source) ? source : resolve(root, source);
277
+ const from = isAbsolute(source) ? source : resolve(root, source);
512
278
  loaded = (await loadConfigFromFile(environment, from))?.config;
513
- } else if (typeof source === "function") loaded = await source(environment);
514
- else loaded = await source;
515
- if (loaded === void 0) throw new Error(`component({ vite }): no config at ${from}`);
279
+ found = `${from} exports no config`;
280
+ fix = "point `vite` at the app's own vite.config.ts";
281
+ } else if (typeof source === "function") {
282
+ loaded = await source(environment);
283
+ if (typeof loaded === "function") {
284
+ found = "the config function returned a function, not a config";
285
+ fix = "call the framework's factory and pass what it returns (for Astro, `getViteConfig({}, { root: import.meta.dirname })`, never `getViteConfig`)";
286
+ } else {
287
+ found = "the config function returned no config";
288
+ fix = "return the framework's config from the function (for Astro, `getViteConfig({}, { root: import.meta.dirname })`)";
289
+ }
290
+ } else {
291
+ loaded = await source;
292
+ found = "the config promise resolved to no config";
293
+ fix = "resolve the promise to the framework's config object";
294
+ }
295
+ if (typeof loaded !== "object" || loaded === null) throw new Error(refusal$1({
296
+ fix,
297
+ found: `component({ vite }): ${found}`,
298
+ read: "docs/guides/component-specs.md#the-constructor-there-is-none",
299
+ why: "A component renders through the Vite pipeline the app builds with, read from its config."
300
+ }));
516
301
  const inactive = transformKey() === "oxc" ? "esbuild" : "oxc";
517
302
  const kept = {};
518
303
  const config = { ...loaded };
519
- for (const key of PIPELINE_KEYS) if (config[key] !== void 0 && key !== inactive) kept[key] = config[key];
304
+ for (const key of PIPELINE_KEYS) if (key !== inactive) kept[key] = config[key];
520
305
  return kept;
521
306
  }
522
- /** What a missing optional peer costs, said once rather than as a resolver stack. */
523
- const PROVIDER_MISSING = "component(): @vitest/browser-playwright is an optional peer — install it beside the runner it pins (npm i -D @vitest/browser-playwright@<the installed vitest version> playwright).";
524
- /** The installed version of a package, read from its own manifest. */
525
- function versionOf(specifier) {
526
- const manifest = JSON.parse(readFileSync(requireFrom.resolve(`${specifier}/package.json`), "utf8"));
527
- if (typeof manifest === "object" && manifest !== null && "version" in manifest) return String(manifest.version);
528
- throw new Error(`component(): ${specifier} is installed without a version.`);
529
- }
530
- /**
531
- * Which of the two transformer keys the INSTALLED Vite actually reads.
532
- *
533
- * Vite 8 transforms with oxc and prints a warning for every `esbuild` option it
534
- * is handed; Vite 6 and 7 transform with esbuild and have no `oxc` key at all.
535
- * The inactive one is dropped rather than carried as a second, ignored
536
- * statement — a config that says the same thing twice is a config that will one
537
- * day say two different things.
538
- */
539
- function transformKey() {
540
- return Number.parseInt(versionOf("vite").split(".")[0] ?? "", 10) >= 8 ? "oxc" : "esbuild";
307
+ function optimizeDepsOf(declared) {
308
+ const own = isRecord(declared) ? { ...declared } : {};
309
+ const listed = "include" in own && Array.isArray(own.include) ? own.include : [];
310
+ return {
311
+ ...own,
312
+ include: [.../* @__PURE__ */ new Set([...PAGE_DEPENDENCIES.filter(resolves), ...listed.filter((entry) => typeof entry === "string")])]
313
+ };
541
314
  }
542
- /**
543
- * The JSX default, on the key the running Vite reads, yielding to the
544
- * consumer's own: a `jsxImportSource` or a classic runtime is the app's
545
- * statement, not the seam's to overwrite.
546
- */
547
315
  function jsxPipeline(pipeline) {
548
316
  const key = transformKey();
549
317
  const stated = pipeline[key];
550
318
  if (key === "oxc") return { oxc: stated ?? { jsx: { runtime: "automatic" } } };
551
319
  return { esbuild: stated ?? { jsx: "automatic" } };
552
320
  }
553
- /**
554
- * `@vitest/browser-playwright` peers vitest on an EXACT version: 4.1.10 wants
555
- * 4.1.10, not `^4.1`. A mismatch fails deep inside the tester with a message
556
- * about a missing runner, so the pair is checked where the project is built
557
- * and both versions are named.
558
- */
559
- function assertProviderPin() {
560
- const runner = versionOf("vitest");
561
- let provider;
562
- try {
563
- provider = versionOf("@vitest/browser-playwright");
564
- } catch {
565
- throw new Error(PROVIDER_MISSING);
566
- }
567
- if (runner !== provider) throw new Error(`component(): @vitest/browser-playwright@${provider} peers vitest on an exact version, and vitest is ${runner}. Bump both to the same version in one change (npm i -D vitest@${provider} @vitest/browser-playwright@${provider}).`);
568
- }
569
- /** The framework's own update flag, read where `process` exists. */
570
- function updating() {
571
- return process.env.TEST_UPDATE === "1" || process.argv.includes("-u") || process.argv.includes("--update");
572
- }
573
- /**
574
- * Hand the project's `wrap` to the page as a STATIC import.
575
- *
576
- * The page could fetch the module by URL when the first render needs it, and
577
- * that is what breaks: a dependency Vite never scanned is discovered mid-run,
578
- * the optimizer rewrites its cache, and every module the page is holding —
579
- * the test's own included — 404s behind a stale `?v=` hash. A setup file is in
580
- * the graph before the first test runs, so the wrap and everything it imports
581
- * are scanned, pre-bundled, and never re-optimised under the run.
582
- *
583
- * It is written into `.artifacts/` rather than served from memory because
584
- * vitest resolves `setupFiles` as paths on disk; it is generated, so it belongs
585
- * with everything else a tool generates, and never on the row.
586
- *
587
- * The wrapper lands on `globalThis` under a shared symbol rather than an
588
- * export: the setup module and the package's browser build are separate graphs
589
- * in the page and share no module instance.
590
- */
591
321
  function writeSetupFile(wrap, root) {
592
322
  const absolute = resolve(root, wrap);
593
323
  const path = resolve(root, VITEST_ARTIFACTS_DIR, "component-setup.mjs");
594
324
  mkdirSync(dirname(path), { recursive: true });
595
325
  writeFileSync(path, [
596
- "// Generated by `component({ wrap })` — @jterrazz/test. Do not edit.",
326
+ "// Generated by `component({ wrap })` of @jterrazz/test. Do not edit.",
597
327
  `import wrap from ${JSON.stringify(absolute)};`,
598
328
  "globalThis[Symbol.for('@jterrazz/test:component-wrap')] = wrap;",
599
329
  ""
600
330
  ].join("\n"), "utf8");
601
331
  return path;
602
332
  }
603
- /**
604
- * What the page cannot read for itself: update mode is a `process.env`/`argv`
605
- * read, and the project's clock is stated where `process` exists.
606
- */
607
333
  function providedToPage(options) {
608
334
  return {
609
335
  ...options.clock === void 0 ? {} : { componentClock: options.clock },
610
336
  componentViewport: viewportOf(options),
611
- update: updating()
337
+ update: shouldUpdateSnapshots()
612
338
  };
613
339
  }
614
- /** The page size every render of the project starts at. */
615
340
  function viewportOf(options) {
616
341
  return options.viewport ?? {
617
342
  height: 720,
618
343
  width: 1280
619
344
  };
620
345
  }
621
- /** The globs a component project collects, and the ones it stays out of. */
622
- function componentGlobs(options) {
623
- return {
624
- exclude: options.exclude ?? (options.include === void 0 ? ["specs/**"] : []),
625
- include: options.include ?? ["**/*.test.tsx"]
626
- };
627
- }
628
- /**
629
- * A component test renders in a real Chromium — the browser the website facet
630
- * already drives through the same `playwright` peer.
631
- *
632
- * The default `include` reaches the whole project, so the default `exclude`
633
- * keeps it out of `specs/`, where the assembled product is specified. State
634
- * your own `include` and the exclusion is yours to state too.
635
- */
346
+ /** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
636
347
  async function component(options = {}) {
637
348
  const root = options.root ?? callerDirectory();
638
- assertProviderPin();
349
+ assertProviderPin(versionOf("vitest"), installedVersion("@vitest/browser-playwright"));
639
350
  const { playwright } = await import("@vitest/browser-playwright");
640
351
  const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
641
- const consumerPlugins = Array.isArray(pipeline.plugins) ? pipeline.plugins : [];
352
+ const consumerPlugins = isPluginList(pipeline.plugins) ? pipeline.plugins : [];
642
353
  return onProjectDefaults({
643
354
  ...pipeline,
644
355
  ...jsxPipeline(pipeline),
645
- optimizeDeps: { include: SEAM_DEPENDENCIES.filter(resolves) },
356
+ optimizeDeps: optimizeDepsOf(pipeline.optimizeDeps),
646
357
  plugins: [mswWorkerPlugin(), ...consumerPlugins],
647
358
  test: {
648
359
  browser: {
@@ -658,76 +369,232 @@ async function component(options = {}) {
658
369
  screenshotDirectory: SCREENSHOTS_DIR,
659
370
  viewport: viewportOf(options)
660
371
  },
661
- ...componentGlobs(options),
662
- ...options.serial === true ? { fileParallelism: false } : {},
663
- name: "component",
372
+ ...projectTest("component", options, {
373
+ exclude: options.include === void 0 ? ["specs/**"] : [],
374
+ include: ["**/*.test.tsx"]
375
+ }),
664
376
  provide: providedToPage(options),
665
377
  ...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] },
666
- sequence: { groupOrder: 2 },
667
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
378
+ sequence: { groupOrder: 2 }
668
379
  }
669
380
  });
670
381
  }
671
382
  //#endregion
672
- //#region src/facets/integration/integration.project.ts
673
- /** `integration()` — a module against real services, or against a golden. */
383
+ //#region src/specifications/integration/integration.project.ts
674
384
  function integration(options = {}) {
675
- return facetProject("integration", options);
385
+ return mergeConfig(inProcessProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
676
386
  }
677
387
  //#endregion
678
- //#region src/facets/jobs/jobs.project.ts
679
- /** `jobs()` — what a name triggers in-process. */
680
- function jobs(options = {}) {
681
- return facetProject("jobs", options);
388
+ //#region src/specifications/job/job.project.ts
389
+ function job(options = {}) {
390
+ return mergeConfig(inProcessProject("job", options), { test: { globalSetup: [servicesSetup()] } });
682
391
  }
683
392
  //#endregion
684
- //#region src/facets/mobile/mobile.project.ts
685
- /**
686
- * `mobile()` — the app on a simulator. A simulator is the one thing that
687
- * cannot share a machine with itself, so it runs last and alone (group 3).
688
- */
393
+ //#region src/specifications/lint/lint.project.ts
394
+ function lint(options = {}) {
395
+ return specificationProject("lint", options);
396
+ }
397
+ //#endregion
398
+ //#region src/specifications/mobile/mobile.project.ts
399
+ /** A simulator cannot share a machine with itself: serial. A global setup holds one driver session for the run. */
689
400
  function mobile(options = {}) {
690
- return mergeConfig(facetProject("mobile", options), { test: { sequence: { groupOrder: 3 } } });
401
+ const { device, root, ...common } = options;
402
+ return mergeConfig(specificationProject("mobile", common), { test: {
403
+ fileParallelism: false,
404
+ globalSetup: [mobileSessionSetup()],
405
+ ...device === void 0 ? {} : { provide: { mobileAppium: {
406
+ device,
407
+ root: root ?? "."
408
+ } } },
409
+ sequence: { groupOrder: 3 }
410
+ } });
691
411
  }
692
412
  //#endregion
693
- //#region src/facets/website/website.project.ts
694
- /**
695
- * A website spec meets the assembled product through a served page, in the
696
- * Chromium playwright drives. It opens a browser, so it is scheduled before the
697
- * component project and never beside it: two Chromiums must not share a slot on
698
- * a 2-vCPU runner.
699
- */
413
+ //#region src/specifications/website/website.project.ts
700
414
  function website(options = {}) {
701
- return onProjectDefaults({ test: {
702
- ...options.exclude === void 0 ? {} : { exclude: options.exclude },
703
- ...options.serial === true ? { fileParallelism: false } : {},
704
- include: options.include ?? ["specs/website/**/*.spec.ts"],
705
- name: "website",
706
- sequence: { groupOrder: 1 },
707
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
415
+ return mergeConfig(specificationProject("website", options), { test: { sequence: { groupOrder: 1 } } });
416
+ }
417
+ //#endregion
418
+ //#region src/vitest/preset.ts
419
+ const COVERAGE = {
420
+ exclude: [
421
+ "**/node_modules/**",
422
+ "specs/**",
423
+ GROUND_GLOB,
424
+ "**/*.d.ts",
425
+ "**/*.test-d.ts",
426
+ "**/*.fixtures.ts",
427
+ ".artifacts/**",
428
+ "**/*.config.ts",
429
+ "**/*.config.mts"
430
+ ],
431
+ autoAttachSubprocess: true,
432
+ excludeAfterRemap: true,
433
+ provider: "v8",
434
+ reporter: [
435
+ "text",
436
+ "html",
437
+ "json-summary"
438
+ ],
439
+ reportsDirectory: COVERAGE_DIR
440
+ };
441
+ function shardOf(argv) {
442
+ for (const [index, argument] of argv.entries()) {
443
+ if (argument.startsWith("--shard=")) return argument.slice(8);
444
+ if (argument === "--shard") return argv[index + 1];
445
+ }
446
+ }
447
+ function reporterOutputFiles(argv) {
448
+ const shard = shardOf(argv);
449
+ return shard === void 0 ? REPORTER_OUTPUT_FILES : {
450
+ ...REPORTER_OUTPUT_FILES,
451
+ blob: `${BLOB_DIR}/blob-${shard.replace("/", "-")}.json`
452
+ };
453
+ }
454
+ function rootDefaults() {
455
+ return mergeConfig(projectDefaults(), { test: {
456
+ coverage: COVERAGE,
457
+ outputFile: reporterOutputFiles(process.argv)
708
458
  } });
709
459
  }
460
+ function isInline(project) {
461
+ return isRecord(project) && !("then" in project);
462
+ }
463
+ function rootExcludeBeside(projects, exclude) {
464
+ return projects.every(isInline) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
465
+ }
466
+ function withProjectDefaults(project) {
467
+ return isInline(project) ? onProjectDefaults(project) : project;
468
+ }
469
+ function presetConfig(stated = {}) {
470
+ const merged = mergeConfig(rootDefaults(), stated);
471
+ const root = statedOnce(merged.test ?? {});
472
+ const { projects } = root;
473
+ if (projects === void 0) return {
474
+ ...merged,
475
+ test: root
476
+ };
477
+ const inline = projects.map(withProjectDefaults);
478
+ return {
479
+ ...merged,
480
+ test: {
481
+ ...root,
482
+ ...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
483
+ projects: inline
484
+ }
485
+ };
486
+ }
710
487
  //#endregion
711
- //#region src/runner/projects.ts
712
- /**
713
- * A module test runs beside the module it covers: no DOM, no browser, no
714
- * pipeline. Its suffix is `.test.ts` and its place is OUTSIDE `specs/`, where
715
- * the assembled product is specified in `.spec.ts` — the two exclusions say
716
- * exactly that. `.test.tsx` is the third exclusion because a rendered thing is
717
- * a component test: a project's dependency scan reads every file its globs
718
- * match, and a `.tsx` collected here would be optimised for a page that never
719
- * opens.
720
- */
488
+ //#region src/vitest/unit.project.ts
489
+ /** `.test.tsx` is excluded: a rendered thing is a component test, and the dependency scan would optimise it for a page that never opens. */
721
490
  function unit(options = {}) {
722
491
  const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
723
492
  return onProjectDefaults({ test: {
724
- exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
725
- include: options.include ?? fromRoots ?? ["**/*.test.ts"],
726
- ...options.serial === true ? { fileParallelism: false } : {},
727
- name: "unit",
728
- sequence: { groupOrder: 0 },
729
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
493
+ ...projectTest("unit", options, {
494
+ exclude: fromRoots === void 0 ? ["specs/**", "**/*.test.tsx"] : ["**/*.test.tsx"],
495
+ include: fromRoots ?? ["**/*.test.ts"]
496
+ }),
497
+ ...options.isolate === void 0 ? {} : { isolate: options.isolate }
730
498
  } });
731
499
  }
732
500
  //#endregion
733
- export { api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
501
+ //#region src/index.vitest.ts
502
+ /** The shared vitest config: what you pass is merged over the defaults. */
503
+ function defineSpecConfig(config = {}) {
504
+ const { literate: literateOptions, ...userConfig } = config;
505
+ return presetConfig(literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig);
506
+ }
507
+ const BUILDERS = /* @__PURE__ */ new Map([
508
+ ["unit", unit],
509
+ ["lint", lint],
510
+ ["cli", cli],
511
+ ["api", api],
512
+ ["job", job],
513
+ ["integration", integration],
514
+ ["website", website],
515
+ ["mobile", mobile],
516
+ ["component", component]
517
+ ]);
518
+ const anchored = (root, path) => isAbsolute(path) ? path : resolve(root, path);
519
+ const anchoredTarget = (root, target) => typeof target === "string" && target.startsWith(".") ? anchored(root, target) : target;
520
+ function aliasesFrom(root, vite) {
521
+ if (!isRecord(vite) || !isRecord(vite.resolve)) return vite;
522
+ const stated = vite.resolve.alias;
523
+ if (Array.isArray(stated)) {
524
+ const alias = stated.map((entry) => isRecord(entry) ? {
525
+ ...entry,
526
+ replacement: anchoredTarget(root, entry.replacement)
527
+ } : entry);
528
+ return {
529
+ ...vite,
530
+ resolve: {
531
+ ...vite.resolve,
532
+ alias
533
+ }
534
+ };
535
+ }
536
+ if (!isRecord(stated)) return vite;
537
+ const alias = Object.fromEntries(Object.entries(stated).map(([name, target]) => [name, anchoredTarget(root, target)]));
538
+ return {
539
+ ...vite,
540
+ resolve: {
541
+ ...vite.resolve,
542
+ alias
543
+ }
544
+ };
545
+ }
546
+ function refused(found, fix) {
547
+ return new Error(refusal$1({
548
+ fix,
549
+ found,
550
+ read: "docs/start/set-up-a-project.md#the-vitest-config",
551
+ why: "a project is derived from the tree, and a declared one only states how it departs from that, with its reason"
552
+ }));
553
+ }
554
+ function declaredProjects(declared) {
555
+ if (declared === void 0) return /* @__PURE__ */ new Map();
556
+ if (!isRecord(declared)) throw refused("`test.projects` is not a record", "key each project by its specification: `projects: { cli: { timeout: 120_000, reason: ... } }`");
557
+ const projects = /* @__PURE__ */ new Map();
558
+ for (const [specification, project] of Object.entries(declared)) {
559
+ if (!BUILDERS.has(specification)) throw refused(`\`test.projects\` names the specification \`${specification}\``, `key it by one of ${[...BUILDERS.keys()].join(", ")}`);
560
+ const { reason, ...options } = isRecord(project) ? project : {};
561
+ if (typeof reason !== "string" || reason.trim() === "") throw refused(`\`test.projects.${specification}\` departs from its derived project with no reason`, `state why under \`test.projects.${specification}.reason\`, or delete what the tree already derives`);
562
+ projects.set(specification, options);
563
+ }
564
+ return projects;
565
+ }
566
+ async function projectOf(root, specification, { setup, ...options }) {
567
+ const builder = BUILDERS.get(specification);
568
+ if (builder === void 0) return {};
569
+ const componentRoot = anchored(root, typeof options.root === "string" ? options.root : ".");
570
+ const project = await builder({
571
+ ...options,
572
+ ...specification === "component" ? { root: componentRoot } : {},
573
+ vite: aliasesFrom(root, options.vite)
574
+ });
575
+ const setups = Array.isArray(setup) ? setup.filter((path) => typeof path === "string") : [];
576
+ return mergeConfig({ test: { globalSetup: setups.map((path) => anchored(root, path)) } }, project);
577
+ }
578
+ async function derivedFrom(root) {
579
+ const { problem, resolved } = await readSettings(root);
580
+ if (problem !== null) throw new Error(refusal$1({
581
+ fix: "correct jterrazz.config.ts as the finding says, then run vitest again",
582
+ found: `jterrazz.config.ts ${problem}`,
583
+ read: "docs/start/set-up-a-project.md#the-vitest-config",
584
+ why: "the test projects are declared there, and a config read without them would run none of them"
585
+ }));
586
+ const section = resolved.sections[TEST_SECTION];
587
+ const declared = declaredProjects(isRecord(section) ? section.projects : void 0);
588
+ const own = resolved.own[TEST_SECTION]?.projects;
589
+ const derived = derivedProjects(root);
590
+ const specifications = [...BUILDERS.keys()].filter((specification) => derived.has(specification) || isRecord(own) && own[specification] !== void 0);
591
+ return presetConfig(specifications.length === 0 ? {} : { test: { projects: await Promise.all(specifications.map(async (specification) => await projectOf(root, specification, {
592
+ ...derived.get(specification),
593
+ ...declared.get(specification)
594
+ }))) } });
595
+ }
596
+ const vitestConfig = async (configUrl) => await derivedFrom(dirname(fileURLToPath(configUrl)));
597
+ //#endregion
598
+ export { api, cli, component, defineSpecConfig, integration, job, lint, literate, mobile, unit, vitestConfig, website };
599
+
600
+ //# sourceMappingURL=vitest.js.map