@testspectra/react 1.1.8-rc.3 → 1.1.8-rc.30
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/LICENSE.md +48 -0
- package/bin/testspectra-react.js +7 -7
- package/dist/cli.js +220 -24
- package/dist/index.d.ts +19 -1
- package/dist/index.js +766 -43
- package/dist/runner.js +1 -0
- package/dist/setup.js +75 -0
- package/dist/vitest_reporter.js +227 -0
- package/package.json +16 -15
package/dist/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// src/dom_driver_bridge.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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
|
|
344
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
388
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
};
|