@jterrazz/test 15.1.1 → 15.3.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/README.md +38 -29
- package/dist/ambiguity.js +23 -4
- package/dist/artifacts.js +108 -163
- package/dist/browser/index.js +2796 -0
- package/dist/checker.js +470 -29
- package/dist/index.d.ts +1434 -609
- package/dist/index.js +3696 -2745
- package/dist/intercept.js +117 -59
- package/dist/oxlint.cjs +481 -35
- package/dist/oxlint.d.cts +13 -1
- package/dist/oxlint.d.ts +13 -1
- package/dist/oxlint.js +481 -35
- package/dist/playwright.adapter.js +96 -22
- package/dist/{queue.js → stream.js} +61 -2
- package/dist/vitest.d.ts +130 -2
- package/dist/vitest.js +433 -3
- package/package.json +37 -5
package/dist/vitest.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { c as GROUND_EXPECTED } from "./match.js";
|
|
1
2
|
import { a as specDescription, r as VITEST_ARTIFACTS_DIR, t as COVERAGE_DIR } from "./artifacts.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { configDefaults, mergeConfig } from "vitest/config";
|
|
5
8
|
//#region src/vitest/literate-plugin.ts
|
|
6
9
|
/**
|
|
@@ -116,6 +119,12 @@ const EXCLUDE = [...configDefaults.exclude, "**/_fixtures/**"];
|
|
|
116
119
|
* Defaults a vitest PROJECT accepts. `coverage` and `reporters` are root-only
|
|
117
120
|
* options (vitest omits them from a project's config), so they are absent here
|
|
118
121
|
* on purpose — setting them would be silently dropped.
|
|
122
|
+
*
|
|
123
|
+
* Exported for the project helpers, which build their canonical project ON
|
|
124
|
+
* these rather than beside them: a helper's project must carry the same
|
|
125
|
+
* budgets and the same artefact directory as a hand-written one.
|
|
126
|
+
*
|
|
127
|
+
* @internal
|
|
119
128
|
*/
|
|
120
129
|
function projectDefaults() {
|
|
121
130
|
return {
|
|
@@ -164,4 +173,425 @@ function defineSpecConfig(config = {}) {
|
|
|
164
173
|
};
|
|
165
174
|
}
|
|
166
175
|
//#endregion
|
|
167
|
-
|
|
176
|
+
//#region src/integrations/vitest-browser/commands.ts
|
|
177
|
+
/** Where a golden sits: `_expected/<name>` beside the test that asked for it. */
|
|
178
|
+
function goldenPath(testPath, name) {
|
|
179
|
+
if (testPath === void 0) throw new Error("golden: the command carries no test path — a golden is read from a test file.");
|
|
180
|
+
return resolve(dirname(testPath), GROUND_EXPECTED, name);
|
|
181
|
+
}
|
|
182
|
+
/** The ARIA snapshot of the rendered `<body>`, as YAML. */
|
|
183
|
+
const ariaTree = async (context) => {
|
|
184
|
+
return await (await context.frame()).locator("body").ariaSnapshot();
|
|
185
|
+
};
|
|
186
|
+
/** The golden's current content, or `null` when it does not exist yet. */
|
|
187
|
+
const goldenRead = (context, name) => {
|
|
188
|
+
const path = goldenPath(context.testPath, name);
|
|
189
|
+
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
190
|
+
};
|
|
191
|
+
/** Write the golden — update mode's half, and the path it wrote, for the message. */
|
|
192
|
+
const goldenWrite = (context, name, content) => {
|
|
193
|
+
const path = goldenPath(context.testPath, name);
|
|
194
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
195
|
+
writeFileSync(path, content, "utf8");
|
|
196
|
+
return path;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* The commands, as `component()` registers them — one list, so the names the
|
|
200
|
+
* page calls and the names the project registers cannot drift.
|
|
201
|
+
*/
|
|
202
|
+
const COMPONENT_COMMANDS = {
|
|
203
|
+
ariaTree,
|
|
204
|
+
goldenRead,
|
|
205
|
+
goldenWrite
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/vitest/projects.ts
|
|
209
|
+
/** The pipeline keys a consumer's Vite config contributes, and nothing else. */
|
|
210
|
+
const PIPELINE_KEYS = [
|
|
211
|
+
"assetsInclude",
|
|
212
|
+
"css",
|
|
213
|
+
"define",
|
|
214
|
+
"envPrefix",
|
|
215
|
+
"esbuild",
|
|
216
|
+
"json",
|
|
217
|
+
"oxc",
|
|
218
|
+
"plugins",
|
|
219
|
+
"resolve"
|
|
220
|
+
];
|
|
221
|
+
/**
|
|
222
|
+
* The seam's own runtime, pre-bundled so a cold cache never reloads mid-run.
|
|
223
|
+
*
|
|
224
|
+
* `msw` and `msw/browser` are NOT here: `@vitest/browser` already puts both in
|
|
225
|
+
* `optimizeDeps.exclude`, and Vite's esbuild optimizer (6 and 7) treats an
|
|
226
|
+
* entry that is both included and excluded as fatal — dependency optimisation
|
|
227
|
+
* throws before a single test runs.
|
|
228
|
+
*/
|
|
229
|
+
const SEAM_DEPENDENCIES = [
|
|
230
|
+
"react",
|
|
231
|
+
"react-dom/client",
|
|
232
|
+
"react/jsx-dev-runtime",
|
|
233
|
+
"react/jsx-runtime",
|
|
234
|
+
"vitest-browser-react"
|
|
235
|
+
];
|
|
236
|
+
const requireFrom = createRequire(import.meta.url);
|
|
237
|
+
/** This module's own file — the frames the caller search walks past. */
|
|
238
|
+
const HELPERS_FILE = import.meta.filename;
|
|
239
|
+
/** A stack frame's file, in either of the two shapes V8 prints it in. */
|
|
240
|
+
const STACK_FRAME = /(?:\((?<wrapped>[^()]+):\d+:\d+\)|at (?<bare>[^()\s]+):\d+:\d+)$/u;
|
|
241
|
+
/** A config vite bundled before importing it — `<its name>.timestamp-<hash>.mjs`. */
|
|
242
|
+
const BUNDLED_CONFIG = /^(?<name>.+)\.timestamp-[^.]+\.mjs$/u;
|
|
243
|
+
/** The file of the first frame below this module — who called the helper. */
|
|
244
|
+
function callerFile() {
|
|
245
|
+
const { stack } = /* @__PURE__ */ new Error("@jterrazz/test: reading the call site of a project helper");
|
|
246
|
+
for (const frame of stack?.split("\n").slice(1) ?? []) {
|
|
247
|
+
const groups = STACK_FRAME.exec(frame.trim())?.groups;
|
|
248
|
+
const location = groups?.wrapped ?? groups?.bare;
|
|
249
|
+
if (location === void 0 || location.startsWith("node:")) continue;
|
|
250
|
+
const file = location.startsWith("file:") ? fileURLToPath(location) : location;
|
|
251
|
+
if (file !== HELPERS_FILE) return file;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** The nearest directory at or above the cwd holding a file of this name. */
|
|
255
|
+
function nearestHolding(name) {
|
|
256
|
+
let directory = process.cwd();
|
|
257
|
+
for (;;) {
|
|
258
|
+
if (existsSync(resolve(directory, name))) return directory;
|
|
259
|
+
const parent = dirname(directory);
|
|
260
|
+
if (parent === directory) return;
|
|
261
|
+
directory = parent;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The directory of the CONFIG that called a helper — what the relative paths
|
|
266
|
+
* it was handed mean.
|
|
267
|
+
*
|
|
268
|
+
* Not `process.cwd()`: a `vitest.config.ts` is read by more tools than the
|
|
269
|
+
* runner, and knip and `typescript check` load every config of a repository
|
|
270
|
+
* from the repository ROOT — `component({ vite: './web/vite.config.ts' })`
|
|
271
|
+
* written beside an app resolved against the wrong tree and threw before a
|
|
272
|
+
* single test ran. `import.meta` cannot answer either: it describes THIS
|
|
273
|
+
* module. And vitest hands a project neither its root nor its config file — a
|
|
274
|
+
* project function is called with `{ command, mode, isPreview, isSsrBuild }`
|
|
275
|
+
* and nothing else.
|
|
276
|
+
*
|
|
277
|
+
* So the call site is read off the stack, where a loader that keeps a config's
|
|
278
|
+
* identity (node, tsx, jiti — what those tools use) names the file itself.
|
|
279
|
+
* Vite's own loader does not: it BUNDLES the config into
|
|
280
|
+
* `node_modules/.vite-temp/` and only the config's NAME survives the move. The
|
|
281
|
+
* name is what is then looked for upward from the cwd, because that loader is
|
|
282
|
+
* the runner's and a runner is started in the tree it tests. A config loaded
|
|
283
|
+
* by the runner from another tree entirely states `root` itself.
|
|
284
|
+
*/
|
|
285
|
+
function callerDirectory() {
|
|
286
|
+
const file = callerFile();
|
|
287
|
+
if (file === void 0) return process.cwd();
|
|
288
|
+
const bundled = BUNDLED_CONFIG.exec(basename(file))?.groups?.name;
|
|
289
|
+
if (bundled === void 0 || basename(dirname(file)) !== ".vite-temp") return dirname(file);
|
|
290
|
+
return nearestHolding(bundled) ?? process.cwd();
|
|
291
|
+
}
|
|
292
|
+
/** Is this specifier installed here? An absent optional peer is simply not pre-bundled. */
|
|
293
|
+
function resolves(specifier) {
|
|
294
|
+
try {
|
|
295
|
+
requireFrom.resolve(specifier);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Serve msw's worker script from the package's OWN install, at the path the
|
|
303
|
+
* worker asks for, with the scope header a worker registered at `/` needs.
|
|
304
|
+
*
|
|
305
|
+
* `@vitest/browser` resolves `/mockServiceWorker.js` too, but from ITS own
|
|
306
|
+
* directory — and it does not depend on `msw`, so that resolution only lands
|
|
307
|
+
* where the installer hoists. The package that OWNS the dependency is the one
|
|
308
|
+
* that can always find it, and it is this one. Without either, a consumer runs
|
|
309
|
+
* `msw init` and declares `msw`, which F8 forbids.
|
|
310
|
+
*/
|
|
311
|
+
function mswWorkerPlugin() {
|
|
312
|
+
return {
|
|
313
|
+
configureServer(server) {
|
|
314
|
+
const script = requireFrom.resolve("msw/mockServiceWorker.js");
|
|
315
|
+
server.middlewares.use("/mockServiceWorker.js", (_request, response) => {
|
|
316
|
+
response.setHeader("content-type", "text/javascript");
|
|
317
|
+
response.setHeader("service-worker-allowed", "/");
|
|
318
|
+
response.end(readFileSync(script, "utf8"));
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
name: "@jterrazz/test:msw-worker"
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Keep an app's Vite PIPELINE and drop everything that says where the app
|
|
326
|
+
* lives.
|
|
327
|
+
*
|
|
328
|
+
* An allow-list, not a drop-list: a consumer's config carries keys the
|
|
329
|
+
* framework has never heard of — `environments`, `ssr`, `appType`, `publicDir`
|
|
330
|
+
* — and one of them re-roots the run and the project collects nothing. Naming
|
|
331
|
+
* what the pipeline IS keeps an unknown key from leaking in. The consumer's
|
|
332
|
+
* `plugins` are CONCATENATED after the seam's, never assigned over them.
|
|
333
|
+
*/
|
|
334
|
+
async function reroot(source, root) {
|
|
335
|
+
const environment = {
|
|
336
|
+
command: "serve",
|
|
337
|
+
isSsrBuild: false,
|
|
338
|
+
mode: "test"
|
|
339
|
+
};
|
|
340
|
+
let loaded;
|
|
341
|
+
let from = "the value it was given";
|
|
342
|
+
if (typeof source === "string") {
|
|
343
|
+
const { loadConfigFromFile } = await import("vite");
|
|
344
|
+
from = isAbsolute(source) ? source : resolve(root, source);
|
|
345
|
+
loaded = (await loadConfigFromFile(environment, from))?.config;
|
|
346
|
+
} else if (typeof source === "function") loaded = await source(environment);
|
|
347
|
+
else loaded = await source;
|
|
348
|
+
if (loaded === void 0) throw new Error(`component({ vite }): no config at ${from}`);
|
|
349
|
+
const inactive = transformKey() === "oxc" ? "esbuild" : "oxc";
|
|
350
|
+
const kept = {};
|
|
351
|
+
const config = { ...loaded };
|
|
352
|
+
for (const key of PIPELINE_KEYS) if (config[key] !== void 0 && key !== inactive) kept[key] = config[key];
|
|
353
|
+
return kept;
|
|
354
|
+
}
|
|
355
|
+
/** What a missing optional peer costs, said once rather than as a resolver stack. */
|
|
356
|
+
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).";
|
|
357
|
+
/** The installed version of a package, read from its own manifest. */
|
|
358
|
+
function versionOf(specifier) {
|
|
359
|
+
const manifest = JSON.parse(readFileSync(requireFrom.resolve(`${specifier}/package.json`), "utf8"));
|
|
360
|
+
if (typeof manifest === "object" && manifest !== null && "version" in manifest) return String(manifest.version);
|
|
361
|
+
throw new Error(`component(): ${specifier} is installed without a version.`);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Which of the two transformer keys the INSTALLED Vite actually reads.
|
|
365
|
+
*
|
|
366
|
+
* Vite 8 transforms with oxc and prints a warning for every `esbuild` option it
|
|
367
|
+
* is handed; Vite 6 and 7 transform with esbuild and have no `oxc` key at all.
|
|
368
|
+
* The inactive one is dropped rather than carried as a second, ignored
|
|
369
|
+
* statement — a config that says the same thing twice is a config that will one
|
|
370
|
+
* day say two different things.
|
|
371
|
+
*/
|
|
372
|
+
function transformKey() {
|
|
373
|
+
return Number.parseInt(versionOf("vite").split(".")[0] ?? "", 10) >= 8 ? "oxc" : "esbuild";
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The JSX default, on the key the running Vite reads, yielding to the
|
|
377
|
+
* consumer's own: a `jsxImportSource` or a classic runtime is the app's
|
|
378
|
+
* statement, not the seam's to overwrite.
|
|
379
|
+
*/
|
|
380
|
+
function jsxPipeline(pipeline) {
|
|
381
|
+
const key = transformKey();
|
|
382
|
+
const stated = pipeline[key];
|
|
383
|
+
if (key === "oxc") return { oxc: stated ?? { jsx: { runtime: "automatic" } } };
|
|
384
|
+
return { esbuild: stated ?? { jsx: "automatic" } };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* `@vitest/browser-playwright` peers vitest on an EXACT version: 4.1.10 wants
|
|
388
|
+
* 4.1.10, not `^4.1`. A mismatch fails deep inside the tester with a message
|
|
389
|
+
* about a missing runner, so the pair is checked where the project is built
|
|
390
|
+
* and both versions are named.
|
|
391
|
+
*/
|
|
392
|
+
function assertProviderPin() {
|
|
393
|
+
const runner = versionOf("vitest");
|
|
394
|
+
let provider;
|
|
395
|
+
try {
|
|
396
|
+
provider = versionOf("@vitest/browser-playwright");
|
|
397
|
+
} catch {
|
|
398
|
+
throw new Error(PROVIDER_MISSING);
|
|
399
|
+
}
|
|
400
|
+
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}).`);
|
|
401
|
+
}
|
|
402
|
+
/** The framework's own update flag, read where `process` exists. */
|
|
403
|
+
function updating() {
|
|
404
|
+
return process.env.TEST_UPDATE === "1" || process.argv.includes("-u") || process.argv.includes("--update");
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Hand the project's `wrap` to the page as a STATIC import.
|
|
408
|
+
*
|
|
409
|
+
* The page could fetch the module by URL when the first render needs it, and
|
|
410
|
+
* that is what breaks: a dependency Vite never scanned is discovered mid-run,
|
|
411
|
+
* the optimizer rewrites its cache, and every module the page is holding —
|
|
412
|
+
* the test's own included — 404s behind a stale `?v=` hash. A setup file is in
|
|
413
|
+
* the graph before the first test runs, so the wrap and everything it imports
|
|
414
|
+
* are scanned, pre-bundled, and never re-optimised under the run.
|
|
415
|
+
*
|
|
416
|
+
* It is written into `.artifacts/` rather than served from memory because
|
|
417
|
+
* vitest resolves `setupFiles` as paths on disk; it is generated, so it belongs
|
|
418
|
+
* with everything else a tool generates, and never on the row.
|
|
419
|
+
*
|
|
420
|
+
* The wrapper lands on `globalThis` under a shared symbol rather than an
|
|
421
|
+
* export: the setup module and the package's browser build are separate graphs
|
|
422
|
+
* in the page and share no module instance.
|
|
423
|
+
*/
|
|
424
|
+
function writeSetupFile(wrap, root) {
|
|
425
|
+
const absolute = resolve(root, wrap);
|
|
426
|
+
const path = resolve(root, VITEST_ARTIFACTS_DIR, "component-setup.mjs");
|
|
427
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
428
|
+
writeFileSync(path, [
|
|
429
|
+
"// Generated by `component({ wrap })` — @jterrazz/test. Do not edit.",
|
|
430
|
+
`import wrap from ${JSON.stringify(absolute)};`,
|
|
431
|
+
"globalThis[Symbol.for('@jterrazz/test:component-wrap')] = wrap;",
|
|
432
|
+
""
|
|
433
|
+
].join("\n"), "utf8");
|
|
434
|
+
return path;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* What the page cannot read for itself: update mode is a `process.env`/`argv`
|
|
438
|
+
* read, and the project's clock is stated where `process` exists.
|
|
439
|
+
*/
|
|
440
|
+
function providedToPage(options) {
|
|
441
|
+
return {
|
|
442
|
+
...options.clock === void 0 ? {} : { componentClock: options.clock },
|
|
443
|
+
componentViewport: viewportOf(options),
|
|
444
|
+
update: updating()
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/** The page size every render of the project starts at. */
|
|
448
|
+
function viewportOf(options) {
|
|
449
|
+
return options.viewport ?? {
|
|
450
|
+
height: 720,
|
|
451
|
+
width: 1280
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** The globs a component project collects, and the ones it stays out of. */
|
|
455
|
+
function componentGlobs(options) {
|
|
456
|
+
return {
|
|
457
|
+
exclude: options.exclude ?? (options.include === void 0 ? ["specs/**"] : []),
|
|
458
|
+
include: options.include ?? ["**/*.test.tsx"]
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
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
|
+
* A component test renders in a real Chromium — the browser the website facet
|
|
496
|
+
* already drives through the same `playwright` peer.
|
|
497
|
+
*
|
|
498
|
+
* The default `include` reaches the whole project, so the default `exclude`
|
|
499
|
+
* keeps it out of `specs/`, where the assembled product is specified. State
|
|
500
|
+
* your own `include` and the exclusion is yours to state too.
|
|
501
|
+
*/
|
|
502
|
+
async function component(options = {}) {
|
|
503
|
+
const root = options.root ?? callerDirectory();
|
|
504
|
+
assertProviderPin();
|
|
505
|
+
const { playwright } = await import("@vitest/browser-playwright");
|
|
506
|
+
const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
|
|
507
|
+
const consumerPlugins = Array.isArray(pipeline.plugins) ? pipeline.plugins : [];
|
|
508
|
+
const project = {
|
|
509
|
+
...pipeline,
|
|
510
|
+
...jsxPipeline(pipeline),
|
|
511
|
+
optimizeDeps: { include: SEAM_DEPENDENCIES.filter(resolves) },
|
|
512
|
+
plugins: [mswWorkerPlugin(), ...consumerPlugins],
|
|
513
|
+
test: {
|
|
514
|
+
attachmentsDir: `${VITEST_ARTIFACTS_DIR}/attachments`,
|
|
515
|
+
browser: {
|
|
516
|
+
commands: COMPONENT_COMMANDS,
|
|
517
|
+
enabled: true,
|
|
518
|
+
headless: true,
|
|
519
|
+
instances: [{ browser: "chromium" }],
|
|
520
|
+
provider: playwright({ contextOptions: {
|
|
521
|
+
locale: options.locale ?? "en-US",
|
|
522
|
+
timezoneId: options.timezone ?? "UTC"
|
|
523
|
+
} }),
|
|
524
|
+
screenshotDirectory: `${VITEST_ARTIFACTS_DIR}/screenshots`,
|
|
525
|
+
viewport: viewportOf(options)
|
|
526
|
+
},
|
|
527
|
+
...componentGlobs(options),
|
|
528
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
529
|
+
name: "component",
|
|
530
|
+
provide: providedToPage(options),
|
|
531
|
+
...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] },
|
|
532
|
+
sequence: { groupOrder: 2 },
|
|
533
|
+
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
return mergeConfig(projectDefaults(), project);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The node facets' canonical projects.
|
|
540
|
+
*
|
|
541
|
+
* Each one collects the facet's own tree, carries the preset's budgets and
|
|
542
|
+
* artefact directory, and runs in group 0 — the browser facets follow, so two
|
|
543
|
+
* Chromiums never share a slot on a two-vCPU runner. `{ include, exclude,
|
|
544
|
+
* timeout, serial }` is what a repository states when its tree, its budget or
|
|
545
|
+
* its parallelism differs;
|
|
546
|
+
* everything else is the framework's.
|
|
547
|
+
*/
|
|
548
|
+
/** The globs a node facet collects, and the ones a caller stated instead. */
|
|
549
|
+
function facetProject(facet, options) {
|
|
550
|
+
return mergeConfig(projectDefaults(), { test: {
|
|
551
|
+
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
552
|
+
include: options.include ?? [`specs/${facet}/**/*.test.ts`],
|
|
553
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
554
|
+
name: facet,
|
|
555
|
+
sequence: { groupOrder: 0 },
|
|
556
|
+
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
557
|
+
} });
|
|
558
|
+
}
|
|
559
|
+
/** `api()` — the app met through HTTP. */
|
|
560
|
+
function api(options = {}) {
|
|
561
|
+
return facetProject("api", options);
|
|
562
|
+
}
|
|
563
|
+
/** `jobs()` — what a name triggers in-process. */
|
|
564
|
+
function jobs(options = {}) {
|
|
565
|
+
return facetProject("jobs", options);
|
|
566
|
+
}
|
|
567
|
+
/** `integration()` — a module against real services, or against a golden. */
|
|
568
|
+
function integration(options = {}) {
|
|
569
|
+
return facetProject("integration", options);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* `mobile()` — the app on a simulator. A simulator is the one thing that
|
|
573
|
+
* cannot share a machine with itself, so it runs last and alone (group 3).
|
|
574
|
+
*/
|
|
575
|
+
function mobile(options = {}) {
|
|
576
|
+
return mergeConfig(facetProject("mobile", options), { test: { sequence: { groupOrder: 3 } } });
|
|
577
|
+
}
|
|
578
|
+
/** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
|
|
579
|
+
const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
|
|
580
|
+
const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
|
|
581
|
+
/**
|
|
582
|
+
* A cli project collects the facet's `.test.ts` files AND its documents: a
|
|
583
|
+
* `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
|
|
584
|
+
* so the plugin's glob has to join the include of the project that collects
|
|
585
|
+
* them — which is this one, never the root config.
|
|
586
|
+
*/
|
|
587
|
+
function cli(options = {}) {
|
|
588
|
+
const { literate: literateOptions, ...common } = options;
|
|
589
|
+
const project = facetProject("cli", common);
|
|
590
|
+
if (literateOptions === false) return project;
|
|
591
|
+
return mergeConfig(project, { plugins: [literate({
|
|
592
|
+
include: literateOptions?.include ?? CLI_DOCUMENTS,
|
|
593
|
+
specification: literateOptions?.specification ?? CLI_SPECIFICATION
|
|
594
|
+
})] });
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
export { api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/test",
|
|
3
|
-
"version": "15.
|
|
4
|
-
"description": "Declarative testing framework for HTTP APIs, CLIs,
|
|
3
|
+
"version": "15.3.0",
|
|
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",
|
|
7
|
+
"browser-mode",
|
|
7
8
|
"cli-testing",
|
|
9
|
+
"component-testing",
|
|
8
10
|
"e2e",
|
|
9
11
|
"golden-files",
|
|
10
12
|
"hexagonal",
|
|
@@ -14,7 +16,8 @@
|
|
|
14
16
|
"test-framework",
|
|
15
17
|
"testcontainers",
|
|
16
18
|
"testing",
|
|
17
|
-
"vitest"
|
|
19
|
+
"vitest",
|
|
20
|
+
"website-testing"
|
|
18
21
|
],
|
|
19
22
|
"license": "MIT",
|
|
20
23
|
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
@@ -34,6 +37,7 @@
|
|
|
34
37
|
"exports": {
|
|
35
38
|
".": {
|
|
36
39
|
"types": "./dist/index.d.ts",
|
|
40
|
+
"browser": "./dist/browser/index.js",
|
|
37
41
|
"import": "./dist/index.js"
|
|
38
42
|
},
|
|
39
43
|
"./vitest": {
|
|
@@ -74,29 +78,57 @@
|
|
|
74
78
|
},
|
|
75
79
|
"devDependencies": {
|
|
76
80
|
"@hono/node-server": "^2.0.8",
|
|
77
|
-
"@jterrazz/typescript": "^10.
|
|
81
|
+
"@jterrazz/typescript": "^10.2.1",
|
|
78
82
|
"@types/better-sqlite3": "^7.6.13",
|
|
79
83
|
"@types/node": "^26.1.1",
|
|
80
84
|
"@types/pg": "^8.20.0",
|
|
85
|
+
"@types/react": "^19.3.0",
|
|
86
|
+
"@types/react-dom": "^19.3.0",
|
|
87
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
81
88
|
"hono": "^4.12.30",
|
|
82
89
|
"playwright": "^1.49.0",
|
|
90
|
+
"react": "^19.3.0",
|
|
91
|
+
"react-dom": "^19.3.0",
|
|
92
|
+
"react-router": "^8.4.0",
|
|
83
93
|
"tsdown": "^0.22.7",
|
|
94
|
+
"vite": "^8.1.4",
|
|
84
95
|
"vitest": "^4.1.10",
|
|
96
|
+
"vitest-browser-react": "^2.3.0",
|
|
85
97
|
"webdriverio": "^9.30.0"
|
|
86
98
|
},
|
|
87
99
|
"peerDependencies": {
|
|
100
|
+
"@vitest/browser-playwright": "*",
|
|
88
101
|
"appium": "^3.0.0",
|
|
89
102
|
"playwright": "^1.49.0",
|
|
90
|
-
"
|
|
103
|
+
"react": "^18 || ^19",
|
|
104
|
+
"react-dom": "^18 || ^19",
|
|
105
|
+
"vite": "^6.4.0 || ^7 || ^8",
|
|
106
|
+
"vitest": "^4.1.10 || ^5",
|
|
107
|
+
"vitest-browser-react": "^2.3.0",
|
|
91
108
|
"webdriverio": "^9.30.0"
|
|
92
109
|
},
|
|
93
110
|
"peerDependenciesMeta": {
|
|
111
|
+
"@vitest/browser-playwright": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
94
114
|
"appium": {
|
|
95
115
|
"optional": true
|
|
96
116
|
},
|
|
97
117
|
"playwright": {
|
|
98
118
|
"optional": true
|
|
99
119
|
},
|
|
120
|
+
"react": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"react-dom": {
|
|
124
|
+
"optional": true
|
|
125
|
+
},
|
|
126
|
+
"vite": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
129
|
+
"vitest-browser-react": {
|
|
130
|
+
"optional": true
|
|
131
|
+
},
|
|
100
132
|
"webdriverio": {
|
|
101
133
|
"optional": true
|
|
102
134
|
}
|