@jterrazz/test 15.2.0 → 16.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.
package/dist/vitest.js CHANGED
@@ -1,11 +1,12 @@
1
- import { c as GROUND_EXPECTED } from "./match.js";
2
- import { a as specDescription, r as VITEST_ARTIFACTS_DIR, t as COVERAGE_DIR } from "./artifacts.js";
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";
3
4
  import { createRequire } from "node:module";
4
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { basename, dirname, isAbsolute, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { configDefaults, mergeConfig } from "vitest/config";
8
- //#region src/vitest/literate-plugin.ts
9
+ //#region src/runner/literate-plugin.ts
9
10
  /**
10
11
  * The `literate()` vite plugin — door one of the spec-document format.
11
12
  *
@@ -70,7 +71,7 @@ function literate(options) {
70
71
  };
71
72
  }
72
73
  //#endregion
73
- //#region src/vitest/preset.ts
74
+ //#region src/runner/preset.ts
74
75
  /**
75
76
  * `defineSpecConfig()` — the shared vitest config every repository of the
76
77
  * ecosystem starts from.
@@ -116,6 +117,27 @@ const HOOK_TIMEOUT_MS = 3e4;
116
117
  */
117
118
  const EXCLUDE = [...configDefaults.exclude, "**/_fixtures/**"];
118
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
+ */
134
+ const HYGIENE = {
135
+ restoreMocks: true,
136
+ retry: 0,
137
+ unstubEnvs: true,
138
+ unstubGlobals: true
139
+ };
140
+ /**
119
141
  * Defaults a vitest PROJECT accepts. `coverage` and `reporters` are root-only
120
142
  * options (vitest omits them from a project's config), so they are absent here
121
143
  * on purpose — setting them would be silently dropped.
@@ -130,20 +152,64 @@ function projectDefaults() {
130
152
  return {
131
153
  cacheDir: VITEST_ARTIFACTS_DIR,
132
154
  test: {
155
+ attachmentsDir: ATTACHMENTS_DIR,
133
156
  exclude: EXCLUDE,
134
157
  hookTimeout: HOOK_TIMEOUT_MS,
158
+ ...HYGIENE,
135
159
  testTimeout: TEST_TIMEOUT_MS
136
160
  }
137
161
  };
138
162
  }
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
+ };
139
202
  /** Defaults for the root config — the project ones, plus what only a root carries. */
140
203
  function rootDefaults() {
141
204
  return {
142
205
  cacheDir: VITEST_ARTIFACTS_DIR,
143
206
  test: {
144
- coverage: { reportsDirectory: COVERAGE_DIR },
207
+ attachmentsDir: ATTACHMENTS_DIR,
208
+ coverage: COVERAGE,
145
209
  exclude: EXCLUDE,
146
210
  hookTimeout: HOOK_TIMEOUT_MS,
211
+ ...HYGIENE,
212
+ outputFile: REPORTER_OUTPUT_FILES,
147
213
  testTimeout: TEST_TIMEOUT_MS
148
214
  }
149
215
  };
@@ -173,7 +239,47 @@ function defineSpecConfig(config = {}) {
173
239
  };
174
240
  }
175
241
  //#endregion
176
- //#region src/integrations/vitest-browser/commands.ts
242
+ //#region src/runner/facet-project.ts
243
+ /** The globs a node facet collects, and the ones a caller stated instead. */
244
+ function facetProject(facet, options) {
245
+ return mergeConfig(projectDefaults(), { test: {
246
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude },
247
+ include: options.include ?? [`specs/${facet}/**/*.spec.ts`],
248
+ ...options.serial === true ? { fileParallelism: false } : {},
249
+ name: facet,
250
+ sequence: { groupOrder: 0 },
251
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
252
+ } });
253
+ }
254
+ //#endregion
255
+ //#region src/facets/api/api.project.ts
256
+ /** `api()` — the app met through HTTP. */
257
+ function api(options = {}) {
258
+ return facetProject("api", options);
259
+ }
260
+ //#endregion
261
+ //#region src/facets/cli/cli.project.ts
262
+ /** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
263
+ const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
264
+ const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
265
+ /**
266
+ * A cli project collects the facet's `.test.ts` files AND its documents: a
267
+ * `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
268
+ * so the plugin's glob has to join the include of the project that collects
269
+ * them — which is this one, never the root config.
270
+ */
271
+ function cli(options = {}) {
272
+ const { literate: literateOptions, ...common } = options;
273
+ const project = facetProject("cli", common);
274
+ if (literateOptions === false) return project;
275
+ const documents = literateOptions?.include ?? CLI_DOCUMENTS;
276
+ return mergeConfig(project, { plugins: [literate({
277
+ include: documents,
278
+ specification: literateOptions?.specification ?? CLI_SPECIFICATION
279
+ })] });
280
+ }
281
+ //#endregion
282
+ //#region src/seams/vitest-browser/commands.ts
177
283
  /** Where a golden sits: `_expected/<name>` beside the test that asked for it. */
178
284
  function goldenPath(testPath, name) {
179
285
  if (testPath === void 0) throw new Error("golden: the command carries no test path — a golden is read from a test file.");
@@ -205,7 +311,7 @@ const COMPONENT_COMMANDS = {
205
311
  goldenWrite
206
312
  };
207
313
  //#endregion
208
- //#region src/vitest/projects.ts
314
+ //#region src/facets/component/component.project.ts
209
315
  /** The pipeline keys a consumer's Vite config contributes, and nothing else. */
210
316
  const PIPELINE_KEYS = [
211
317
  "assetsInclude",
@@ -459,39 +565,6 @@ function componentGlobs(options) {
459
565
  };
460
566
  }
461
567
  /**
462
- * A module test runs beside the module it covers: no DOM, no browser, no
463
- * pipeline. `.test.tsx` is excluded because a rendered thing is a component
464
- * test — a project's dependency scan reads every file its globs match, and a
465
- * `.tsx` collected here would be optimised for a page that never opens.
466
- */
467
- function unit(options = {}) {
468
- const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
469
- return mergeConfig(projectDefaults(), { test: {
470
- exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
471
- include: options.include ?? fromRoots ?? ["**/*.test.ts"],
472
- ...options.serial === true ? { fileParallelism: false } : {},
473
- name: "unit",
474
- sequence: { groupOrder: 0 },
475
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
476
- } });
477
- }
478
- /**
479
- * A website spec meets the assembled product through a served page, in the
480
- * Chromium playwright drives. It opens a browser, so it is scheduled before the
481
- * component project and never beside it: two Chromiums must not share a slot on
482
- * a 2-vCPU runner.
483
- */
484
- function website(options = {}) {
485
- return mergeConfig(projectDefaults(), { test: {
486
- ...options.exclude === void 0 ? {} : { exclude: options.exclude },
487
- ...options.serial === true ? { fileParallelism: false } : {},
488
- include: options.include ?? ["specs/website/**/*.test.ts"],
489
- name: "website",
490
- sequence: { groupOrder: 1 },
491
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
492
- } });
493
- }
494
- /**
495
568
  * A component test renders in a real Chromium — the browser the website facet
496
569
  * already drives through the same `playwright` peer.
497
570
  *
@@ -511,17 +584,17 @@ async function component(options = {}) {
511
584
  optimizeDeps: { include: SEAM_DEPENDENCIES.filter(resolves) },
512
585
  plugins: [mswWorkerPlugin(), ...consumerPlugins],
513
586
  test: {
514
- attachmentsDir: `${VITEST_ARTIFACTS_DIR}/attachments`,
515
587
  browser: {
516
588
  commands: COMPONENT_COMMANDS,
517
589
  enabled: true,
590
+ expect: { toMatchScreenshot: { screenshotDirectory: SCREENSHOTS_DIR } },
518
591
  headless: true,
519
592
  instances: [{ browser: "chromium" }],
520
593
  provider: playwright({ contextOptions: {
521
594
  locale: options.locale ?? "en-US",
522
595
  timezoneId: options.timezone ?? "UTC"
523
596
  } }),
524
- screenshotDirectory: `${VITEST_ARTIFACTS_DIR}/screenshots`,
597
+ screenshotDirectory: SCREENSHOTS_DIR,
525
598
  viewport: viewportOf(options)
526
599
  },
527
600
  ...componentGlobs(options),
@@ -536,4 +609,65 @@ async function component(options = {}) {
536
609
  return mergeConfig(projectDefaults(), project);
537
610
  }
538
611
  //#endregion
539
- export { component, defineSpecConfig, literate, unit, website };
612
+ //#region src/facets/integration/integration.project.ts
613
+ /** `integration()` — a module against real services, or against a golden. */
614
+ function integration(options = {}) {
615
+ return facetProject("integration", options);
616
+ }
617
+ //#endregion
618
+ //#region src/facets/jobs/jobs.project.ts
619
+ /** `jobs()` — what a name triggers in-process. */
620
+ function jobs(options = {}) {
621
+ return facetProject("jobs", options);
622
+ }
623
+ //#endregion
624
+ //#region src/facets/mobile/mobile.project.ts
625
+ /**
626
+ * `mobile()` — the app on a simulator. A simulator is the one thing that
627
+ * cannot share a machine with itself, so it runs last and alone (group 3).
628
+ */
629
+ function mobile(options = {}) {
630
+ return mergeConfig(facetProject("mobile", options), { test: { sequence: { groupOrder: 3 } } });
631
+ }
632
+ //#endregion
633
+ //#region src/facets/website/website.project.ts
634
+ /**
635
+ * A website spec meets the assembled product through a served page, in the
636
+ * Chromium playwright drives. It opens a browser, so it is scheduled before the
637
+ * component project and never beside it: two Chromiums must not share a slot on
638
+ * a 2-vCPU runner.
639
+ */
640
+ function website(options = {}) {
641
+ return mergeConfig(projectDefaults(), { test: {
642
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude },
643
+ ...options.serial === true ? { fileParallelism: false } : {},
644
+ include: options.include ?? ["specs/website/**/*.spec.ts"],
645
+ name: "website",
646
+ sequence: { groupOrder: 1 },
647
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
648
+ } });
649
+ }
650
+ //#endregion
651
+ //#region src/runner/projects.ts
652
+ /**
653
+ * A module test runs beside the module it covers: no DOM, no browser, no
654
+ * pipeline. Its suffix is `.test.ts` and its place is OUTSIDE `specs/`, where
655
+ * the assembled product is specified in `.spec.ts` — the two exclusions say
656
+ * exactly that. `.test.tsx` is the third exclusion because a rendered thing is
657
+ * a component test: a project's dependency scan reads every file its globs
658
+ * match, and a `.tsx` collected here would be optimised for a page that never
659
+ * opens.
660
+ */
661
+ function unit(options = {}) {
662
+ const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
663
+ return mergeConfig(projectDefaults(), { test: {
664
+ exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
665
+ include: options.include ?? fromRoots ?? ["**/*.test.ts"],
666
+ ...options.serial === true ? { fileParallelism: false } : {},
667
+ name: "unit",
668
+ sequence: { groupOrder: 0 },
669
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
670
+ } });
671
+ }
672
+ //#endregion
673
+ export { api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "15.2.0",
3
+ "version": "16.0.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, background jobs, websites, mobile apps and rendered components — one entry point, real infrastructure and a real browser, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",
@@ -31,6 +31,7 @@
31
31
  "files": [
32
32
  "dist",
33
33
  "!dist/catalog.*",
34
+ "!dist/coverage.*",
34
35
  "schema"
35
36
  ],
36
37
  "type": "module",
@@ -64,15 +65,12 @@
64
65
  "docs": "npm run build && node dist/catalog.js && typescript docs",
65
66
  "lint": "typescript check && node dist/checker.js specs",
66
67
  "lint:fix": "typescript fix && node dist/checker.js specs --fix",
67
- "test": "vitest --run"
68
+ "test": "vitest --run",
69
+ "coverage": "npm run build && vitest --run --coverage && node dist/coverage.js --write",
70
+ "coverage:check": "node dist/coverage.js"
68
71
  },
69
72
  "dependencies": {
70
- "better-sqlite3": "^12.11.1",
71
- "mockdate": "^3.0.5",
72
73
  "msw": "^2.15.0",
73
- "pg": "^8.22.0",
74
- "redis": "^6.1.0",
75
- "testcontainers": "^12.0.4",
76
74
  "vitest-mock-extended": "^5.0.0",
77
75
  "yaml": "^2.9.0"
78
76
  },
@@ -84,26 +82,36 @@
84
82
  "@types/pg": "^8.20.0",
85
83
  "@types/react": "^19.3.0",
86
84
  "@types/react-dom": "^19.3.0",
87
- "@vitest/browser-playwright": "^4.1.10",
85
+ "@vitest/browser-playwright": "5.0.1",
86
+ "@vitest/coverage-v8": "^5.0.1",
87
+ "better-sqlite3": "^12.11.1",
88
88
  "hono": "^4.12.30",
89
+ "oxlint": "^1.83.0",
90
+ "pg": "^8.22.0",
89
91
  "playwright": "^1.49.0",
90
92
  "react": "^19.3.0",
91
93
  "react-dom": "^19.3.0",
92
94
  "react-router": "^8.4.0",
95
+ "redis": "^6.1.0",
96
+ "testcontainers": "^12.0.4",
93
97
  "tsdown": "^0.22.7",
94
98
  "vite": "^8.1.4",
95
- "vitest": "^4.1.10",
99
+ "vitest": "5.0.1",
96
100
  "vitest-browser-react": "^2.3.0",
97
101
  "webdriverio": "^9.30.0"
98
102
  },
99
103
  "peerDependencies": {
100
104
  "@vitest/browser-playwright": "*",
101
105
  "appium": "^3.0.0",
106
+ "better-sqlite3": "^12.11.1",
107
+ "pg": "^8.22.0",
102
108
  "playwright": "^1.49.0",
103
109
  "react": "^18 || ^19",
104
110
  "react-dom": "^18 || ^19",
111
+ "redis": "^6.1.0",
112
+ "testcontainers": "^12.0.4",
105
113
  "vite": "^6.4.0 || ^7 || ^8",
106
- "vitest": "^4.1.10 || ^5",
114
+ "vitest": "^5",
107
115
  "vitest-browser-react": "^2.3.0",
108
116
  "webdriverio": "^9.30.0"
109
117
  },
@@ -114,6 +122,12 @@
114
122
  "appium": {
115
123
  "optional": true
116
124
  },
125
+ "better-sqlite3": {
126
+ "optional": true
127
+ },
128
+ "pg": {
129
+ "optional": true
130
+ },
117
131
  "playwright": {
118
132
  "optional": true
119
133
  },
@@ -123,6 +137,12 @@
123
137
  "react-dom": {
124
138
  "optional": true
125
139
  },
140
+ "redis": {
141
+ "optional": true
142
+ },
143
+ "testcontainers": {
144
+ "optional": true
145
+ },
126
146
  "vite": {
127
147
  "optional": true
128
148
  },
@@ -134,6 +154,6 @@
134
154
  }
135
155
  },
136
156
  "engines": {
137
- "node": ">=20"
157
+ "node": ">=24"
138
158
  }
139
159
  }