@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/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 { readFileSync } from "node:fs";
3
- import { isAbsolute, resolve } from "node:path";
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
- export { defineSpecConfig, literate };
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.1.1",
4
- "description": "Declarative testing framework for HTTP APIs, CLIs, and background jobs — one entry point, real infrastructure, golden-file assertions, plus an oxlint convention plugin.",
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.0.0",
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
- "vitest": "^4.1.10",
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
  }