@testspectra/react 1.1.8-rc.9 → 1.1.10

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/index.js CHANGED
@@ -1,16 +1,19 @@
1
1
  // src/dom_driver_bridge.ts
2
- var cachedUser = null;
3
- async function getUser() {
4
- if (!cachedUser) {
5
- const { userEvent } = await import("@testing-library/user-event");
6
- cachedUser = userEvent.setup();
7
- }
8
- return cachedUser;
2
+ import { userEvent } from "@testing-library/user-event";
3
+ function getUser() {
4
+ return userEvent.setup({ delay: null });
9
5
  }
10
6
  function resolveSelector(selector) {
11
7
  const trimmed = selector.trim();
12
8
  if (trimmed.startsWith("~")) {
13
- return `[aria-label=${JSON.stringify(trimmed.slice(1))}]`;
9
+ const raw = trimmed.slice(1);
10
+ const bracketIndex = raw.indexOf("[");
11
+ if (bracketIndex !== -1) {
12
+ const label = raw.slice(0, bracketIndex);
13
+ const rest = raw.slice(bracketIndex);
14
+ return `[aria-label=${JSON.stringify(label)}]${rest}`;
15
+ }
16
+ return `[aria-label=${JSON.stringify(raw)}]`;
14
17
  }
15
18
  return trimmed;
16
19
  }
@@ -34,6 +37,8 @@ var DomDriverBridge = class {
34
37
  /** Polls until `selector` resolves to an element, then returns it — shared by every
35
38
  * interaction method below (user-event needs a real element handle, not a selector string). */
36
39
  async waitForEl(selector, index) {
40
+ const immediate = resolveEl(selector, index);
41
+ if (immediate) return immediate;
37
42
  let found = null;
38
43
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
39
44
  found = resolveEl(selector, index);
@@ -51,6 +56,14 @@ var DomDriverBridge = class {
51
56
  "Spectra.navigate() is not supported in component testing mode. Use Spectra.mount(<Component />) to render a component tree instead of navigating a URL."
52
57
  );
53
58
  }
59
+ async setCookies(setCookieHeaders, _url) {
60
+ const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
61
+ for (const header of headers) {
62
+ if (typeof document !== "undefined") {
63
+ document.cookie = header;
64
+ }
65
+ }
66
+ }
54
67
  async back() {
55
68
  throw new Error("Spectra.back() is not supported in component testing mode.");
56
69
  }
@@ -72,20 +85,20 @@ var DomDriverBridge = class {
72
85
  // ─── Element Actions ─────────────────────────────────────────────────────
73
86
  async click(selector, index = null) {
74
87
  const el = await this.waitForEl(selector, index);
75
- await (await getUser()).click(el);
88
+ await getUser().click(el);
76
89
  }
77
90
  async doubleClick(selector, index = null) {
78
91
  const el = await this.waitForEl(selector, index);
79
- await (await getUser()).dblClick(el);
92
+ await getUser().dblClick(el);
80
93
  }
81
94
  async rightClick(selector, index = null) {
82
95
  const el = await this.waitForEl(selector, index);
83
- await (await getUser()).pointer({ keys: "[MouseRight]", target: el });
96
+ await getUser().pointer({ keys: "[MouseRight]", target: el });
84
97
  }
85
98
  async setValue(selector, value, index = null) {
86
99
  const el = await this.waitForEl(selector, index);
87
100
  if (!isFormEl(el)) throw new Error("Element is not a form field: " + JSON.stringify(selector));
88
- const u = await getUser();
101
+ const u = getUser();
89
102
  await u.clear(el);
90
103
  if (String(value).length > 0) {
91
104
  await u.type(el, String(value));
@@ -93,7 +106,7 @@ var DomDriverBridge = class {
93
106
  }
94
107
  async clearValue(selector, index = null) {
95
108
  const el = await this.waitForEl(selector, index);
96
- await (await getUser()).clear(el);
109
+ await getUser().clear(el);
97
110
  }
98
111
  async select(selector, option, index = null) {
99
112
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
@@ -113,7 +126,7 @@ var DomDriverBridge = class {
113
126
  }
114
127
  async hover(selector, index = null) {
115
128
  const el = await this.waitForEl(selector, index);
116
- await (await getUser()).hover(el);
129
+ await getUser().hover(el);
117
130
  }
118
131
  async focus(selector, index = null) {
119
132
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
@@ -284,7 +297,9 @@ var DomDriverBridge = class {
284
297
  }
285
298
  async shouldHaveNoConsoleErrors() {
286
299
  if (this.consoleErrors.length > 0) {
287
- throw new Error(`Expected no console errors, but found ${this.consoleErrors.length}: ${this.consoleErrors.join("; ")}`);
300
+ throw new Error(
301
+ `Expected no console errors, but found ${this.consoleErrors.length}: ${this.consoleErrors.join("; ")}`
302
+ );
288
303
  }
289
304
  }
290
305
  async clearCookies() {
@@ -307,9 +322,27 @@ var DomDriverBridge = class {
307
322
  };
308
323
 
309
324
  // src/mount.ts
310
- import { createRoot } from "react-dom/client";
311
325
  var ROOT_ID = "spectra-root";
312
326
  var cachedRoot = null;
327
+ var dynamicCreateRoot = null;
328
+ async function getCreateRoot() {
329
+ if (typeof window !== "undefined" && window.__TESTSPECTRA_CREATE_ROOT__) {
330
+ return window.__TESTSPECTRA_CREATE_ROOT__;
331
+ }
332
+ if (dynamicCreateRoot) {
333
+ return dynamicCreateRoot;
334
+ }
335
+ try {
336
+ const mod = await import(
337
+ /* @vite-ignore */
338
+ "react-dom/client"
339
+ );
340
+ dynamicCreateRoot = mod.createRoot || mod.default?.createRoot || mod.default;
341
+ if (dynamicCreateRoot) return dynamicCreateRoot;
342
+ } catch {
343
+ }
344
+ throw new Error("Could not find react-dom/client createRoot. Ensure react-dom is installed in your project.");
345
+ }
313
346
  function ensureContainer() {
314
347
  let container = document.getElementById(ROOT_ID);
315
348
  if (!container) {
@@ -322,7 +355,8 @@ function ensureContainer() {
322
355
  async function mount(element) {
323
356
  const container = ensureContainer();
324
357
  if (!cachedRoot) {
325
- cachedRoot = createRoot(container);
358
+ const createRootFn = await getCreateRoot();
359
+ cachedRoot = createRootFn(container);
326
360
  }
327
361
  const componentName = (typeof element.type === "string" ? element.type : element.type?.displayName || element.type?.name) || "component";
328
362
  return globalThis.__TS_RUNTIME__.trackStep(
@@ -330,42 +364,189 @@ async function mount(element) {
330
364
  { type: "action", key: "mount", target: componentName, args: [] },
331
365
  async () => {
332
366
  cachedRoot.render(element);
333
- await new Promise((r) => setTimeout(r, 0));
367
+ await new Promise((r) => setTimeout(r, 20));
334
368
  return globalThis.__TS_RUNTIME__.createElementProxy(`#${ROOT_ID}`);
335
369
  }
336
370
  );
337
371
  }
338
372
 
339
373
  // src/cli.ts
340
- import { spawn } from "node:child_process";
374
+ import { existsSync as existsSync2 } from "node:fs";
375
+ import path2 from "node:path";
341
376
  import { fileURLToPath } from "node:url";
377
+ import spawn from "cross-spawn";
378
+
379
+ // src/config_generator.ts
380
+ import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
381
+ import { createRequire } from "node:module";
382
+ import os from "node:os";
383
+ import path from "node:path";
384
+ import { pathToFileURL } from "node:url";
385
+ function resolveEsmUrl(specifier, packageDir) {
386
+ const cjsPath = createRequire(path.join(packageDir, "package.json")).resolve(specifier);
387
+ const esmPath = cjsPath.replace(/\.cjs$/, ".js");
388
+ return pathToFileURL(existsSync(esmPath) ? esmPath : cjsPath).href;
389
+ }
390
+ function buildAutoImportManifest(root) {
391
+ function scanEntityDir(subdir, filePattern) {
392
+ const dir = path.join(root, subdir);
393
+ if (!existsSync(dir)) return [];
394
+ const entries = [];
395
+ for (const name of readdirSync(dir, { withFileTypes: true })) {
396
+ if (!name.isDirectory()) continue;
397
+ if (name.name.startsWith(".") || name.name === "node_modules" || name.name === "dist" || name.name === "__screenshots__" || name.name === "coverage") {
398
+ continue;
399
+ }
400
+ for (const platform of ["web", "common"]) {
401
+ for (const ext of ["tsx", "ts"]) {
402
+ const candidate = path.join(dir, name.name, filePattern(platform).replace(/\.tsx?$/, `.${ext}`));
403
+ if (existsSync(candidate)) {
404
+ entries.push({ name: name.name, path: candidate.replace(/\\/g, "/") });
405
+ break;
406
+ }
407
+ }
408
+ if (entries.at(-1)?.name === name.name) break;
409
+ }
410
+ }
411
+ return entries;
412
+ }
413
+ function scanFixtures() {
414
+ const dir = path.join(root, "fixtures");
415
+ if (!existsSync(dir)) return [];
416
+ return readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => ({ name: f.replace(/\.json$/, ""), path: path.join(dir, f).replace(/\\/g, "/") }));
417
+ }
418
+ return {
419
+ pageObjects: scanEntityDir("page-objects", (p) => `${p}.tsx`),
420
+ steps: scanEntityDir("support/steps", (p) => `${p}.step.tsx`),
421
+ fixtures: scanFixtures()
422
+ };
423
+ }
424
+ function generateVitestConfig(opts) {
425
+ const reactPluginUrl = resolveEsmUrl("@vitejs/plugin-react", opts.packageDir);
426
+ const vitestConfigUrl = resolveEsmUrl("vitest/config", opts.packageDir);
427
+ const manifest = buildAutoImportManifest(opts.testRoot);
428
+ const setupFile = path.join(opts.packageDir, "dist/setup.js").replace(/\\/g, "/");
429
+ const reporterFile = path.join(opts.packageDir, "dist/vitest_reporter.js").replace(/\\/g, "/");
430
+ const reporterUrl = pathToFileURL(reporterFile).href;
431
+ const normalizedPackageDir = opts.packageDir.replace(/\\/g, "/");
432
+ const normalizedTestRoot = opts.testRoot.replace(/\\/g, "/");
433
+ const configContent = `import react from '${reactPluginUrl}';
434
+ import { defineConfig } from '${vitestConfigUrl}';
435
+ ${opts.semanticReporter ? `import { SpectraVitestReporter } from '${reporterUrl}';` : ""}
436
+
437
+ export default defineConfig({
438
+ root: ${JSON.stringify(normalizedTestRoot)},
439
+ plugins: [react()],
440
+ define: {
441
+ __TESTSPECTRA_AUTO_IMPORT_MANIFEST__: ${JSON.stringify(JSON.stringify(manifest))},
442
+ __TESTSPECTRA_STEP_DELAY_MS__: ${JSON.stringify(opts.stepDelayMs)},
443
+ __TESTSPECTRA_SEMANTIC_REPORTER__: ${JSON.stringify(opts.semanticReporter)},
444
+ },
445
+ optimizeDeps: {
446
+ include: [
447
+ 'react',
448
+ 'react-dom',
449
+ 'react-dom/client',
450
+ '@testing-library/react',
451
+ '@testing-library/user-event',
452
+ ],
453
+ },
454
+ server: {
455
+ fs: {
456
+ allow: [
457
+ ${JSON.stringify(normalizedPackageDir)},
458
+ ${JSON.stringify(normalizedTestRoot)},
459
+ ],
460
+ },
461
+ },
462
+ test: {
463
+ globals: true,
464
+ setupFiles: [${JSON.stringify(setupFile)}],
465
+ reporters: ${opts.semanticReporter ? `[
466
+ new SpectraVitestReporter({ testRoot: ${JSON.stringify(normalizedTestRoot)} }),
467
+ ${opts.outputFile ? `['json', { outputFile: ${JSON.stringify(opts.outputFile)} }],` : ""}
468
+ ]` : opts.outputFile ? `['default', ['json', { outputFile: ${JSON.stringify(opts.outputFile)} }]]` : "'default'"},
469
+ dir: ${JSON.stringify(normalizedTestRoot)},
470
+ include: [
471
+ 'specs/**/web.test.tsx',
472
+ 'specs/**/*.test.tsx',
473
+ 'specs/**/web.test.ts',
474
+ 'specs/**/*.test.ts',
475
+ ],
476
+ exclude: [
477
+ '**/node_modules/**',
478
+ '**/dist/**',
479
+ '**/__screenshots__/**',
480
+ '**/.testspectra/**',
481
+ '**/.git/**',
482
+ '**/coverage/**',
483
+ ],
484
+ browser: {
485
+ enabled: true,
486
+ provider: 'playwright',
487
+ name: 'chromium',
488
+ headless: ${opts.headless},
489
+ fileParallelism: ${opts.fileParallelism},
490
+ ${opts.browserExecutable ? `providerOptions: { launch: { executablePath: ${JSON.stringify(opts.browserExecutable)} } },` : ""}
491
+ },
492
+ },
493
+ });
494
+ `;
495
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "testspectra-react-"));
496
+ const configPath = path.join(tmpDir, "vitest.config.mjs");
497
+ writeFileSync(configPath, configContent, "utf-8");
498
+ return {
499
+ configPath,
500
+ cleanup: () => {
501
+ try {
502
+ rmSync(tmpDir, { recursive: true, force: true });
503
+ } catch {
504
+ }
505
+ }
506
+ };
507
+ }
508
+
509
+ // src/cli.ts
342
510
  async function runComponentTests(options = {}) {
343
- const cwd = options.cwd ?? process.cwd();
344
- const configPath = fileURLToPath(new URL("../vitest.config.ts", import.meta.url));
511
+ const runner = options.runner ?? "vitest";
512
+ if (runner === "spectra") {
513
+ console.log("TestSpectra native component testing is orchestrated by the Rust Core Engine.");
514
+ console.log("Use `spectra test` to run tests with full parallel CDP orchestration.");
515
+ return true;
516
+ }
517
+ const rawCwd = options.cwd ?? process.cwd();
518
+ const cwd = path2.resolve(rawCwd);
519
+ const packageDir = fileURLToPath(new URL("..", import.meta.url));
345
520
  const ext = process.platform === "win32" ? ".cmd" : "";
346
- const vitestBin = fileURLToPath(new URL(`../node_modules/.bin/vitest${ext}`, import.meta.url));
521
+ const localVitestBin = fileURLToPath(new URL(`../node_modules/.bin/vitest${ext}`, import.meta.url));
522
+ const consumerVitestBin = path2.join(cwd, `node_modules/.bin/vitest${ext}`);
523
+ const vitestBin = existsSync2(localVitestBin) ? localVitestBin : consumerVitestBin;
524
+ const semanticReporter = Boolean(options.onLine);
525
+ const { configPath, cleanup } = generateVitestConfig({
526
+ packageDir,
527
+ testRoot: cwd,
528
+ headless: options.headless !== false,
529
+ stepDelayMs: options.stepDelayMs ?? 0,
530
+ semanticReporter,
531
+ fileParallelism: options.concurrency !== 1,
532
+ browserExecutable: options.browserExecutablePath,
533
+ outputFile: options.outputFile
534
+ });
347
535
  const args = ["run", "--config", configPath];
348
- if (options.outputFile) {
349
- args.push("--reporter=default", "--reporter=json", `--outputFile.json=${options.outputFile}`);
350
- }
351
536
  if (options.spec) {
352
- args.push(options.spec);
537
+ let specPattern = options.spec.replace(/\\/g, "/");
538
+ if (path2.isAbsolute(options.spec)) {
539
+ specPattern = path2.relative(cwd, options.spec).replace(/\\/g, "/");
540
+ }
541
+ args.push(specPattern);
353
542
  }
354
- const semanticReporter = Boolean(options.onLine);
355
543
  return new Promise((resolve) => {
356
544
  const child = spawn(vitestBin, args, {
357
545
  stdio: semanticReporter ? ["inherit", "pipe", "pipe"] : "inherit",
358
546
  cwd,
359
547
  env: {
360
- ...process.env,
361
- TESTSPECTRA_TEST_ROOT: cwd,
362
- TESTSPECTRA_HEADLESS: options.headless === false ? "false" : "true",
363
- TESTSPECTRA_STEP_DELAY: String(options.stepDelayMs ?? 0),
364
- TESTSPECTRA_FILE_PARALLELISM: options.concurrency === 1 ? "false" : "true",
365
- ...options.browserExecutablePath ? { TESTSPECTRA_BROWSER_EXECUTABLE: options.browserExecutablePath } : {},
366
- TESTSPECTRA_SEMANTIC_REPORTER: semanticReporter ? "1" : "0"
367
- },
368
- shell: process.platform === "win32"
548
+ ...process.env
549
+ }
369
550
  });
370
551
  if (semanticReporter) {
371
552
  const handleData = (buf) => {
@@ -376,16 +557,30 @@ async function runComponentTests(options = {}) {
376
557
  child.stdout?.on("data", handleData);
377
558
  child.stderr?.on("data", handleData);
378
559
  }
379
- child.on("exit", (code) => resolve(code === 0));
560
+ child.on("exit", (code) => {
561
+ cleanup();
562
+ resolve(code === 0);
563
+ });
380
564
  child.on("error", (err) => {
565
+ cleanup();
381
566
  console.error(err);
382
567
  resolve(false);
383
568
  });
384
569
  });
385
570
  }
386
571
  function getFlagValue(argv, ...names) {
387
- const index = argv.findIndex((a) => names.includes(a));
388
- return index !== -1 ? argv[index + 1] : void 0;
572
+ for (let i = 0; i < argv.length; i++) {
573
+ const arg = argv[i];
574
+ for (const name of names) {
575
+ if (arg === name) {
576
+ return argv[i + 1];
577
+ }
578
+ if (arg.startsWith(`${name}=`)) {
579
+ return arg.slice(name.length + 1);
580
+ }
581
+ }
582
+ }
583
+ return void 0;
389
584
  }
390
585
  async function runCli(argv = process.argv.slice(2)) {
391
586
  const [command] = argv;
@@ -396,13 +591,40 @@ async function runCli(argv = process.argv.slice(2)) {
396
591
  let headless = true;
397
592
  if (argv.includes("--headed") || argv.includes("--no-headless")) headless = false;
398
593
  else if (argv.includes("--headless")) headless = true;
399
- const rawDelay = getFlagValue(argv, "--step-delay", "--delay");
594
+ const rawRunner = getFlagValue(argv, "--runner", "-r");
595
+ const runner = rawRunner === "vitest" ? "vitest" : "spectra";
596
+ const rawDelay = getFlagValue(argv, "--step-delay", "--delay", "-d");
400
597
  let stepDelayMs;
401
598
  if (rawDelay !== void 0) {
402
599
  const parsed = parseInt(String(rawDelay), 10);
403
600
  if (!isNaN(parsed) && parsed >= 0) stepDelayMs = parsed;
404
601
  }
405
- const spec = getFlagValue(argv, "--spec", "-s");
602
+ let spec = getFlagValue(argv, "--spec", "-s");
603
+ if (!spec) {
604
+ const flagsWithValues = /* @__PURE__ */ new Set([
605
+ "--runner",
606
+ "-r",
607
+ "--step-delay",
608
+ "--delay",
609
+ "-d",
610
+ "--spec",
611
+ "-s",
612
+ "--output",
613
+ "-o",
614
+ "--concurrency",
615
+ "-c",
616
+ "--browser-executable"
617
+ ]);
618
+ for (let i = 1; i < argv.length; i++) {
619
+ const arg = argv[i];
620
+ if (arg.startsWith("-")) {
621
+ if (flagsWithValues.has(arg)) i++;
622
+ } else {
623
+ spec = arg;
624
+ break;
625
+ }
626
+ }
627
+ }
406
628
  const outputFile = getFlagValue(argv, "--output", "-o");
407
629
  const rawConcurrency = getFlagValue(argv, "--concurrency", "-c");
408
630
  let concurrency;
@@ -411,12 +633,513 @@ async function runCli(argv = process.argv.slice(2)) {
411
633
  if (!isNaN(parsed) && parsed >= 1) concurrency = parsed;
412
634
  }
413
635
  const browserExecutablePath = getFlagValue(argv, "--browser-executable");
414
- const ok = await runComponentTests({ headless, stepDelayMs, spec, outputFile, concurrency, browserExecutablePath });
636
+ const ok = await runComponentTests({
637
+ runner,
638
+ headless,
639
+ stepDelayMs,
640
+ spec,
641
+ outputFile,
642
+ concurrency,
643
+ browserExecutablePath
644
+ });
415
645
  process.exit(ok ? 0 : 1);
416
646
  }
647
+
648
+ // src/cdp_runner/bundle_server.ts
649
+ import fs from "node:fs";
650
+ import http from "node:http";
651
+ import path3 from "node:path";
652
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
653
+ import * as esbuild from "esbuild";
654
+ import { createRequire as createRequire2 } from "node:module";
655
+ var bundleCache = /* @__PURE__ */ new Map();
656
+ var inFlightBuilds = /* @__PURE__ */ new Map();
657
+ var cssInjectorPlugin = {
658
+ name: "spectra-css-injector",
659
+ setup(build2) {
660
+ build2.onLoad({ filter: /\.(css|scss|sass|less)$/ }, async (args) => {
661
+ try {
662
+ const raw = await fs.promises.readFile(args.path, "utf8");
663
+ const escaped = JSON.stringify(raw);
664
+ const sourcePath = JSON.stringify(args.path);
665
+ return {
666
+ contents: `
667
+ if (typeof document !== 'undefined') {
668
+ const style = document.createElement('style');
669
+ style.setAttribute('data-spectra-css', ${sourcePath});
670
+ style.textContent = ${escaped};
671
+ document.head.appendChild(style);
672
+ }
673
+ `,
674
+ loader: "js"
675
+ };
676
+ } catch (e) {
677
+ return {
678
+ contents: `/* failed to load css: ${String(e)} */`,
679
+ loader: "js"
680
+ };
681
+ }
682
+ });
683
+ }
684
+ };
685
+ var SHARED_REACT_EXTERNALS = [
686
+ "react",
687
+ "react/jsx-runtime",
688
+ "react/jsx-dev-runtime",
689
+ "react/compiler-runtime",
690
+ "react-dom",
691
+ "react-dom/client"
692
+ ];
693
+ async function buildBundle(entrypoint, opts = {}) {
694
+ const mergedExternals = opts.skipSharedExternals ? opts.external ?? [] : Array.from(/* @__PURE__ */ new Set([...opts.external ?? [], ...SHARED_REACT_EXTERNALS]));
695
+ const buildOptions = {
696
+ bundle: true,
697
+ format: "esm",
698
+ platform: "browser",
699
+ target: "esnext",
700
+ external: mergedExternals,
701
+ minify: false,
702
+ write: false,
703
+ plugins: [cssInjectorPlugin],
704
+ loader: {
705
+ ".png": "dataurl",
706
+ ".jpg": "dataurl",
707
+ ".jpeg": "dataurl",
708
+ ".gif": "dataurl",
709
+ ".webp": "dataurl",
710
+ ".svg": "text",
711
+ ".json": "json",
712
+ ".woff": "dataurl",
713
+ ".woff2": "dataurl",
714
+ ".ttf": "dataurl",
715
+ ".eot": "dataurl"
716
+ },
717
+ logLevel: "error",
718
+ logOverride: {
719
+ "ignored-bare-import": "silent"
720
+ },
721
+ sourcemap: "inline",
722
+ jsx: "automatic",
723
+ jsxImportSource: "react"
724
+ };
725
+ if (opts.virtualContents) {
726
+ buildOptions.stdin = {
727
+ contents: opts.virtualContents,
728
+ resolveDir: opts.resolveDir ?? process.cwd(),
729
+ sourcefile: entrypoint,
730
+ loader: "ts"
731
+ };
732
+ } else {
733
+ buildOptions.entryPoints = [entrypoint];
734
+ }
735
+ const result = await esbuild.build(buildOptions);
736
+ if (result.errors.length > 0) {
737
+ const msgs = result.errors.map((e) => e.text).join("\n");
738
+ throw new Error(`esbuild failed for ${entrypoint}:
739
+ ${msgs}`);
740
+ }
741
+ if (!result.outputFiles || result.outputFiles.length === 0) {
742
+ throw new Error(`esbuild produced no output files for ${entrypoint}`);
743
+ }
744
+ return result.outputFiles[0].text;
745
+ }
746
+ async function getCachedBundle(cacheKey, entrypoint, opts) {
747
+ const cached = bundleCache.get(cacheKey);
748
+ if (cached) return cached;
749
+ const existingInFlight = inFlightBuilds.get(cacheKey);
750
+ if (existingInFlight) return existingInFlight;
751
+ const buildPromise = buildBundle(entrypoint, opts).then(
752
+ (js) => {
753
+ bundleCache.set(cacheKey, js);
754
+ inFlightBuilds.delete(cacheKey);
755
+ return js;
756
+ },
757
+ (err) => {
758
+ inFlightBuilds.delete(cacheKey);
759
+ throw err;
760
+ }
761
+ );
762
+ inFlightBuilds.set(cacheKey, buildPromise);
763
+ return buildPromise;
764
+ }
765
+ function buildCombinedReactBundleCode(testRoot) {
766
+ let req;
767
+ try {
768
+ req = createRequire2(path3.join(testRoot, "package.json"));
769
+ } catch {
770
+ req = createRequire2(import.meta.url);
771
+ }
772
+ const resolveSafe = (specifier) => {
773
+ try {
774
+ return req.resolve(specifier);
775
+ } catch {
776
+ try {
777
+ return createRequire2(import.meta.url).resolve(specifier);
778
+ } catch {
779
+ return null;
780
+ }
781
+ }
782
+ };
783
+ const loadSafe = (specifier) => {
784
+ try {
785
+ return req(specifier);
786
+ } catch {
787
+ try {
788
+ return createRequire2(import.meta.url)(specifier);
789
+ } catch {
790
+ return null;
791
+ }
792
+ }
793
+ };
794
+ const reactPath = resolveSafe("react");
795
+ const jsxPath = resolveSafe("react/jsx-runtime");
796
+ const compilerPath = resolveSafe("react/compiler-runtime");
797
+ const domPath = resolveSafe("react-dom");
798
+ const domClientPath = resolveSafe("react-dom/client");
799
+ const reactPkg = loadSafe("react") ?? {};
800
+ const jsxPkg = loadSafe("react/jsx-runtime") ?? {};
801
+ const compilerPkg = loadSafe("react/compiler-runtime") ?? {};
802
+ const domPkg = loadSafe("react-dom") ?? {};
803
+ const domClientPkg = loadSafe("react-dom/client") ?? {};
804
+ const allKeys = /* @__PURE__ */ new Set();
805
+ const exportLines = [];
806
+ const addExports = (varName, pkg) => {
807
+ const keys = Object.keys(pkg).filter((k) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k));
808
+ for (const k of keys) {
809
+ if (!allKeys.has(k)) {
810
+ allKeys.add(k);
811
+ exportLines.push(`export const ${k} = (${varName} as any).${k};`);
812
+ }
813
+ }
814
+ };
815
+ addExports("_react", reactPkg);
816
+ addExports("_jsx", jsxPkg);
817
+ addExports("_compiler", compilerPkg);
818
+ addExports("_dom", domPkg);
819
+ addExports("_domClient", domClientPkg);
820
+ const importParts = [];
821
+ const initParts = [];
822
+ if (reactPath) {
823
+ importParts.push(`import * as _rawReact from ${JSON.stringify(reactPath)};`);
824
+ initParts.push(`const _react: any = (_rawReact as any).default || _rawReact;`);
825
+ } else {
826
+ initParts.push(`const _react: any = {};`);
827
+ }
828
+ if (jsxPath) {
829
+ importParts.push(`import * as _rawJsx from ${JSON.stringify(jsxPath)};`);
830
+ initParts.push(`const _jsx: any = (_rawJsx as any).default || _rawJsx;`);
831
+ } else {
832
+ initParts.push(`const _jsx: any = {};`);
833
+ }
834
+ if (compilerPath) {
835
+ importParts.push(`import * as _rawCompiler from ${JSON.stringify(compilerPath)};`);
836
+ initParts.push(`const _compiler: any = (_rawCompiler as any).default || _rawCompiler;`);
837
+ } else {
838
+ initParts.push(`const _compiler: any = {};`);
839
+ }
840
+ if (domPath) {
841
+ importParts.push(`import * as _rawDom from ${JSON.stringify(domPath)};`);
842
+ initParts.push(`const _dom: any = (_rawDom as any).default || _rawDom;`);
843
+ } else {
844
+ initParts.push(`const _dom: any = {};`);
845
+ }
846
+ if (domClientPath) {
847
+ importParts.push(`import * as _rawDomClient from ${JSON.stringify(domClientPath)};`);
848
+ initParts.push(`const _domClient: any = (_rawDomClient as any).default || _rawDomClient;`);
849
+ } else {
850
+ initParts.push(`const _domClient: any = {};`);
851
+ }
852
+ return `
853
+ ${importParts.join("\n")}
854
+
855
+ ${initParts.join("\n")}
856
+
857
+ ${exportLines.join("\n")}
858
+
859
+ export default _react;
860
+ `;
861
+ }
862
+ function buildHarnessHtml(manifest, stepDelayMs) {
863
+ const manifestJson = JSON.stringify(manifest);
864
+ return `<!DOCTYPE html>
865
+ <html>
866
+ <head>
867
+ <meta charset="utf-8">
868
+ <title>Spectra Component Test</title>
869
+ <script type="importmap">
870
+ {
871
+ "imports": {
872
+ "react": "/@modules/react.js",
873
+ "react/jsx-runtime": "/@modules/react.js",
874
+ "react/jsx-dev-runtime": "/@modules/react.js",
875
+ "react/compiler-runtime": "/@modules/react.js",
876
+ "react-dom": "/@modules/react.js",
877
+ "react-dom/client": "/@modules/react.js"
878
+ }
879
+ }
880
+ </script>
881
+ </head>
882
+ <body>
883
+ <script type="module">
884
+ window.__TESTSPECTRA_AUTO_IMPORT_MANIFEST__ = ${manifestJson};
885
+ window.__TESTSPECTRA_STEP_DELAY_MS__ = ${JSON.stringify(stepDelayMs)};
886
+ window.__TESTSPECTRA_SEMANTIC_REPORTER__ = true;
887
+ try {
888
+ const rdc = await import('react-dom/client');
889
+ window.__TESTSPECTRA_CREATE_ROOT__ = rdc.createRoot || rdc.default?.createRoot || rdc.default;
890
+ } catch (e) {
891
+ console.warn('[TestSpectra] createRoot pre-load warning:', e);
892
+ }
893
+ </script>
894
+ <script type="module" src="/runner.js"></script>
895
+ <script type="module" src="/setup.js"></script>
896
+ </body>
897
+ </html>`;
898
+ }
899
+ function respond(res, status, contentType, body) {
900
+ res.writeHead(status, { "Content-Type": contentType });
901
+ res.end(body);
902
+ }
903
+ function resolveReactPackageDir() {
904
+ let curr = path3.dirname(fileURLToPath2(import.meta.url));
905
+ while (curr !== path3.dirname(curr)) {
906
+ const pkgPath = path3.join(curr, "package.json");
907
+ if (fs.existsSync(pkgPath)) {
908
+ try {
909
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
910
+ if (pkg.name === "@testspectra/react") {
911
+ return curr;
912
+ }
913
+ } catch {
914
+ }
915
+ }
916
+ curr = path3.dirname(curr);
917
+ }
918
+ return path3.resolve(path3.dirname(fileURLToPath2(import.meta.url)), "../..");
919
+ }
920
+ function makeHandler(testRoot, packageDir, harnessHtml, setupEntrypoint, runnerEntrypoint) {
921
+ return async (req, res) => {
922
+ const url = new URL(req.url ?? "/", `http://127.0.0.1`);
923
+ if (url.pathname === "/") {
924
+ respond(res, 200, "text/html; charset=utf-8", harnessHtml);
925
+ return;
926
+ }
927
+ if (url.pathname === "/@modules/react.js" || url.pathname.startsWith("/@modules/react")) {
928
+ try {
929
+ const js = await getCachedBundle("module_react_combined", path3.join(testRoot, "__virtual_react_combined.js"), {
930
+ virtualContents: buildCombinedReactBundleCode(testRoot),
931
+ resolveDir: testRoot,
932
+ skipSharedExternals: true
933
+ });
934
+ respond(res, 200, "application/javascript; charset=utf-8", js);
935
+ } catch (err) {
936
+ respond(
937
+ res,
938
+ 500,
939
+ "application/javascript",
940
+ `throw new Error(${JSON.stringify(`Failed to bundle shared React runtime: ${String(err)}`)});`
941
+ );
942
+ }
943
+ return;
944
+ }
945
+ if (url.pathname === "/setup.js") {
946
+ try {
947
+ const js = await getCachedBundle("setup", setupEntrypoint);
948
+ respond(res, 200, "application/javascript; charset=utf-8", js);
949
+ } catch (err) {
950
+ respond(res, 500, "application/javascript", `// Bundle error: ${String(err)}`);
951
+ }
952
+ return;
953
+ }
954
+ if (url.pathname === "/runner.js") {
955
+ try {
956
+ const js = await getCachedBundle("runner", runnerEntrypoint);
957
+ respond(res, 200, "application/javascript; charset=utf-8", js);
958
+ } catch (err) {
959
+ respond(res, 500, "application/javascript", `// Bundle error: ${String(err)}`);
960
+ }
961
+ return;
962
+ }
963
+ if (url.pathname === "/test") {
964
+ const encodedFile = url.searchParams.get("file");
965
+ if (!encodedFile) {
966
+ respond(res, 400, "text/plain", "Missing ?file= parameter");
967
+ return;
968
+ }
969
+ const testFilePath = decodeURIComponent(encodedFile);
970
+ const resolved = path3.resolve(testFilePath);
971
+ if (!fs.existsSync(resolved)) {
972
+ respond(res, 404, "text/plain", `File not found: ${testFilePath}`);
973
+ return;
974
+ }
975
+ if (resolved.endsWith(".json")) {
976
+ try {
977
+ const raw = fs.readFileSync(resolved, "utf-8");
978
+ respond(res, 200, "application/json; charset=utf-8", raw);
979
+ } catch (err) {
980
+ respond(res, 500, "application/json", JSON.stringify({ error: String(err) }));
981
+ }
982
+ return;
983
+ }
984
+ try {
985
+ const js = await getCachedBundle(resolved, resolved);
986
+ respond(res, 200, "application/javascript; charset=utf-8", js);
987
+ } catch (err) {
988
+ const msg = String(err);
989
+ respond(res, 200, "application/javascript", `throw new Error(${JSON.stringify(`Test bundle error: ${msg}`)});`);
990
+ }
991
+ return;
992
+ }
993
+ respond(res, 404, "text/plain", "Not Found");
994
+ };
995
+ }
996
+ async function startBundleServer(opts = {}) {
997
+ const testRoot = path3.resolve(opts.testRoot ?? process.cwd());
998
+ const packageDir = opts.packageDir ?? resolveReactPackageDir();
999
+ const manifest = opts.manifest ?? { pageObjects: [], steps: [], fixtures: [] };
1000
+ const stepDelayMs = opts.stepDelayMs ?? 0;
1001
+ const setupEntrypoint = path3.join(packageDir, "src", "setup.ts");
1002
+ const runnerEntrypoint = path3.join(packageDir, "src", "cdp_runner", "in_page_runner.ts");
1003
+ const distSetup = path3.join(packageDir, "dist", "setup.js");
1004
+ if (fs.existsSync(distSetup)) {
1005
+ try {
1006
+ bundleCache.set("setup", fs.readFileSync(distSetup, "utf-8"));
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ const distRunner = path3.join(packageDir, "dist", "runner.js");
1011
+ if (fs.existsSync(distRunner)) {
1012
+ try {
1013
+ bundleCache.set("runner", fs.readFileSync(distRunner, "utf-8"));
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ if (!bundleCache.has("setup")) {
1018
+ getCachedBundle("setup", setupEntrypoint).catch(() => {
1019
+ });
1020
+ }
1021
+ if (!bundleCache.has("runner")) {
1022
+ getCachedBundle("runner", runnerEntrypoint).catch(() => {
1023
+ });
1024
+ }
1025
+ const harnessHtml = buildHarnessHtml(manifest, stepDelayMs);
1026
+ const handler = makeHandler(testRoot, packageDir, harnessHtml, setupEntrypoint, runnerEntrypoint);
1027
+ const server = http.createServer(handler);
1028
+ await new Promise((resolve, reject) => {
1029
+ server.listen(0, "127.0.0.1", () => resolve());
1030
+ server.once("error", reject);
1031
+ });
1032
+ const addr = server.address();
1033
+ const baseUrl = `http://127.0.0.1:${addr.port}`;
1034
+ return {
1035
+ url: baseUrl,
1036
+ stop: () => server.close()
1037
+ };
1038
+ }
1039
+
1040
+ // src/sanitizer.ts
1041
+ var VITE_FS_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\/@fs\/([A-Za-z]:\/)/g;
1042
+ var VITE_FS_UNIX_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\/@fs\//g;
1043
+ var VITE_ROOT_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\//g;
1044
+ var URL_QUERY_PATTERN = /(\.[a-zA-Z0-9]+)\?[^:\s'")]*/g;
1045
+ var INTERNAL_FRAME_PATTERNS = [
1046
+ /node_modules[\\/]vitest[\\/]/i,
1047
+ /node_modules[\\/]@vitest[\\/]/i,
1048
+ /node_modules[\\/]vite[\\/]/i,
1049
+ /node_modules[\\/]@vitejs[\\/]/i,
1050
+ /node_modules[\\/]playwright[\\/]/i,
1051
+ /node_modules[\\/]@playwright[\\/]/i,
1052
+ /node:internal[\\/]/i,
1053
+ /dist[\\/]setup\.js/i,
1054
+ /dist[\\/]vitest_reporter\.js/i,
1055
+ /tools[\\/]react[\\/]/i,
1056
+ /[\\/]vitest\.config\./i,
1057
+ /@vitest/i,
1058
+ /__vitest__/i
1059
+ ];
1060
+ function sanitizeViteUrl(text) {
1061
+ if (!text) return "";
1062
+ let result = text.replace(VITE_FS_URL_PATTERN, "$1");
1063
+ result = result.replace(VITE_FS_UNIX_URL_PATTERN, "/");
1064
+ result = result.replace(VITE_ROOT_URL_PATTERN, "./");
1065
+ result = result.replace(URL_QUERY_PATTERN, "$1");
1066
+ return result;
1067
+ }
1068
+ function sanitizeErrorMessage(msg) {
1069
+ if (!msg) return "";
1070
+ let sanitized = sanitizeViteUrl(msg);
1071
+ sanitized = sanitized.replace(/Failed to fetch dynamically imported module:\s*/gi, "Failed to load module: ");
1072
+ sanitized = sanitized.replace(/\[vite\]\s*(?:Internal server error:\s*)?/gi, "[TestSpectra] ");
1073
+ sanitized = sanitized.replace(/@vitest\/browser/gi, "TestSpectra Browser");
1074
+ sanitized = sanitized.replace(/@vitest\/runner/gi, "TestSpectra Runner");
1075
+ sanitized = sanitized.replace(/\bvitest\.config\.[a-z0-9]+/gi, "spectra.config.ts");
1076
+ sanitized = sanitized.replace(/\bVitest\b/g, "TestSpectra");
1077
+ sanitized = sanitized.replace(/\bvitest\b/g, "testspectra");
1078
+ return sanitized;
1079
+ }
1080
+ function sanitizeStackTrace(stack) {
1081
+ if (!stack) return "";
1082
+ const lines = stack.split("\n");
1083
+ const filtered = [];
1084
+ for (let i = 0; i < lines.length; i++) {
1085
+ const line = lines[i];
1086
+ if (i === 0) {
1087
+ filtered.push(sanitizeErrorMessage(line));
1088
+ continue;
1089
+ }
1090
+ const isInternal = INTERNAL_FRAME_PATTERNS.some((pattern) => pattern.test(line));
1091
+ if (isInternal) {
1092
+ continue;
1093
+ }
1094
+ filtered.push(sanitizeErrorMessage(line));
1095
+ }
1096
+ return filtered.join("\n").trim();
1097
+ }
1098
+ function sanitizeError(err) {
1099
+ if (!err) return "";
1100
+ if (typeof err === "string") {
1101
+ return sanitizeStackTrace(err);
1102
+ }
1103
+ if (typeof err === "object") {
1104
+ const errorObj = err;
1105
+ const stack = errorObj.stack;
1106
+ const message = errorObj.message || String(err);
1107
+ if (stack && typeof stack === "string") {
1108
+ const sanitizedStack = sanitizeStackTrace(stack);
1109
+ if (sanitizedStack) return sanitizedStack;
1110
+ }
1111
+ return sanitizeErrorMessage(message);
1112
+ }
1113
+ return sanitizeErrorMessage(String(err));
1114
+ }
1115
+ function isVitestNoise(line) {
1116
+ const trimmed = line.trim();
1117
+ if (!trimmed) return true;
1118
+ if (/^RUN\s+v\d+/i.test(trimmed)) return true;
1119
+ if (/^DEV\s+v\d+/i.test(trimmed)) return true;
1120
+ if (/^Test Files\s+\d+/i.test(trimmed)) return true;
1121
+ if (/^Tests\s+\d+/i.test(trimmed)) return true;
1122
+ if (/^Start at\s+\d+/i.test(trimmed)) return true;
1123
+ if (/^Duration\s+[\d.]+/i.test(trimmed)) return true;
1124
+ if (/^⎯{3,}/.test(trimmed)) return true;
1125
+ if (/^Re-optimizing dependencies/i.test(trimmed)) return true;
1126
+ if (/^JSON report written to/i.test(trimmed)) return true;
1127
+ if (/^\(?transform \d+ms/i.test(trimmed)) return true;
1128
+ if (/^[·✓❯⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s+/.test(trimmed)) return true;
1129
+ if (/vite-cjs-node-api-deprecated/i.test(trimmed)) return true;
1130
+ if (/CJS build of Vite's Node API is deprecated/i.test(trimmed)) return true;
1131
+ if (/^\[?vite\]?\s*(deprecation|warning)/i.test(trimmed)) return true;
1132
+ return false;
1133
+ }
417
1134
  export {
418
1135
  DomDriverBridge,
1136
+ isVitestNoise,
419
1137
  mount,
420
1138
  runCli,
421
- runComponentTests
1139
+ runComponentTests,
1140
+ sanitizeError,
1141
+ sanitizeErrorMessage,
1142
+ sanitizeStackTrace,
1143
+ sanitizeViteUrl,
1144
+ startBundleServer
422
1145
  };