@testspectra/react 1.1.8-rc.25 → 1.1.8-rc.29
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/cli.js +49 -12
- package/dist/index.d.ts +19 -1
- package/dist/index.js +387 -28
- package/dist/runner.js +1 -0
- package/dist/setup.js +106 -1408
- package/dist/vitest_reporter.js +227 -0
- package/package.json +4 -4
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);
|
|
@@ -80,20 +85,20 @@ var DomDriverBridge = class {
|
|
|
80
85
|
// ─── Element Actions ─────────────────────────────────────────────────────
|
|
81
86
|
async click(selector, index = null) {
|
|
82
87
|
const el = await this.waitForEl(selector, index);
|
|
83
|
-
await
|
|
88
|
+
await getUser().click(el);
|
|
84
89
|
}
|
|
85
90
|
async doubleClick(selector, index = null) {
|
|
86
91
|
const el = await this.waitForEl(selector, index);
|
|
87
|
-
await
|
|
92
|
+
await getUser().dblClick(el);
|
|
88
93
|
}
|
|
89
94
|
async rightClick(selector, index = null) {
|
|
90
95
|
const el = await this.waitForEl(selector, index);
|
|
91
|
-
await
|
|
96
|
+
await getUser().pointer({ keys: "[MouseRight]", target: el });
|
|
92
97
|
}
|
|
93
98
|
async setValue(selector, value, index = null) {
|
|
94
99
|
const el = await this.waitForEl(selector, index);
|
|
95
100
|
if (!isFormEl(el)) throw new Error("Element is not a form field: " + JSON.stringify(selector));
|
|
96
|
-
const u =
|
|
101
|
+
const u = getUser();
|
|
97
102
|
await u.clear(el);
|
|
98
103
|
if (String(value).length > 0) {
|
|
99
104
|
await u.type(el, String(value));
|
|
@@ -101,7 +106,7 @@ var DomDriverBridge = class {
|
|
|
101
106
|
}
|
|
102
107
|
async clearValue(selector, index = null) {
|
|
103
108
|
const el = await this.waitForEl(selector, index);
|
|
104
|
-
await
|
|
109
|
+
await getUser().clear(el);
|
|
105
110
|
}
|
|
106
111
|
async select(selector, option, index = null) {
|
|
107
112
|
const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
|
|
@@ -121,7 +126,7 @@ var DomDriverBridge = class {
|
|
|
121
126
|
}
|
|
122
127
|
async hover(selector, index = null) {
|
|
123
128
|
const el = await this.waitForEl(selector, index);
|
|
124
|
-
await
|
|
129
|
+
await getUser().hover(el);
|
|
125
130
|
}
|
|
126
131
|
async focus(selector, index = null) {
|
|
127
132
|
const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
|
|
@@ -345,7 +350,7 @@ async function mount(element) {
|
|
|
345
350
|
{ type: "action", key: "mount", target: componentName, args: [] },
|
|
346
351
|
async () => {
|
|
347
352
|
cachedRoot.render(element);
|
|
348
|
-
await new Promise((r) =>
|
|
353
|
+
await new Promise((r) => queueMicrotask(r));
|
|
349
354
|
return globalThis.__TS_RUNTIME__.createElementProxy(`#${ROOT_ID}`);
|
|
350
355
|
}
|
|
351
356
|
);
|
|
@@ -363,6 +368,11 @@ import { createRequire } from "node:module";
|
|
|
363
368
|
import os from "node:os";
|
|
364
369
|
import path from "node:path";
|
|
365
370
|
import { pathToFileURL } from "node:url";
|
|
371
|
+
function resolveEsmUrl(specifier, packageDir) {
|
|
372
|
+
const cjsPath = createRequire(path.join(packageDir, "package.json")).resolve(specifier);
|
|
373
|
+
const esmPath = cjsPath.replace(/\.cjs$/, ".js");
|
|
374
|
+
return pathToFileURL(existsSync(esmPath) ? esmPath : cjsPath).href;
|
|
375
|
+
}
|
|
366
376
|
function buildAutoImportManifest(root) {
|
|
367
377
|
function scanEntityDir(subdir, filePattern) {
|
|
368
378
|
const dir = path.join(root, subdir);
|
|
@@ -395,15 +405,17 @@ function buildAutoImportManifest(root) {
|
|
|
395
405
|
};
|
|
396
406
|
}
|
|
397
407
|
function generateVitestConfig(opts) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const vitestConfigUrl = pathToFileURL(req.resolve("vitest/config")).href;
|
|
408
|
+
const reactPluginUrl = resolveEsmUrl("@vitejs/plugin-react", opts.packageDir);
|
|
409
|
+
const vitestConfigUrl = resolveEsmUrl("vitest/config", opts.packageDir);
|
|
401
410
|
const manifest = buildAutoImportManifest(opts.testRoot);
|
|
402
411
|
const setupFile = path.join(opts.packageDir, "dist/setup.js").replace(/\\/g, "/");
|
|
412
|
+
const reporterFile = path.join(opts.packageDir, "dist/vitest_reporter.js").replace(/\\/g, "/");
|
|
413
|
+
const reporterUrl = pathToFileURL(reporterFile).href;
|
|
403
414
|
const normalizedPackageDir = opts.packageDir.replace(/\\/g, "/");
|
|
404
415
|
const normalizedTestRoot = opts.testRoot.replace(/\\/g, "/");
|
|
405
416
|
const configContent = `import react from '${reactPluginUrl}';
|
|
406
417
|
import { defineConfig } from '${vitestConfigUrl}';
|
|
418
|
+
${opts.semanticReporter ? `import { SpectraVitestReporter } from '${reporterUrl}';` : ""}
|
|
407
419
|
|
|
408
420
|
export default defineConfig({
|
|
409
421
|
root: ${JSON.stringify(normalizedPackageDir)},
|
|
@@ -433,7 +445,10 @@ export default defineConfig({
|
|
|
433
445
|
test: {
|
|
434
446
|
globals: true,
|
|
435
447
|
setupFiles: [${JSON.stringify(setupFile)}],
|
|
436
|
-
reporters: ${opts.semanticReporter ?
|
|
448
|
+
reporters: ${opts.semanticReporter ? `[
|
|
449
|
+
new SpectraVitestReporter({ testRoot: ${JSON.stringify(normalizedTestRoot)} }),
|
|
450
|
+
${opts.outputFile ? `['json', { outputFile: ${JSON.stringify(opts.outputFile)} }],` : ""}
|
|
451
|
+
]` : opts.outputFile ? `['default', ['json', { outputFile: ${JSON.stringify(opts.outputFile)} }]]` : "'default'"},
|
|
437
452
|
dir: ${JSON.stringify(normalizedTestRoot)},
|
|
438
453
|
include: ['specs/**/web.test.tsx', 'specs/**/*.test.tsx'],
|
|
439
454
|
browser: {
|
|
@@ -463,6 +478,12 @@ export default defineConfig({
|
|
|
463
478
|
|
|
464
479
|
// src/cli.ts
|
|
465
480
|
async function runComponentTests(options = {}) {
|
|
481
|
+
const runner = options.runner ?? "vitest";
|
|
482
|
+
if (runner === "spectra") {
|
|
483
|
+
console.log("TestSpectra native component testing is orchestrated by the Rust Core Engine.");
|
|
484
|
+
console.log("Use `spectra test` to run tests with full parallel CDP orchestration.");
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
466
487
|
const rawCwd = options.cwd ?? process.cwd();
|
|
467
488
|
const cwd = path2.resolve(rawCwd);
|
|
468
489
|
const packageDir = fileURLToPath(new URL("..", import.meta.url));
|
|
@@ -478,12 +499,10 @@ async function runComponentTests(options = {}) {
|
|
|
478
499
|
stepDelayMs: options.stepDelayMs ?? 0,
|
|
479
500
|
semanticReporter,
|
|
480
501
|
fileParallelism: options.concurrency !== 1,
|
|
481
|
-
browserExecutable: options.browserExecutablePath
|
|
502
|
+
browserExecutable: options.browserExecutablePath,
|
|
503
|
+
outputFile: options.outputFile
|
|
482
504
|
});
|
|
483
505
|
const args = ["run", "--config", configPath];
|
|
484
|
-
if (options.outputFile) {
|
|
485
|
-
args.push("--reporter=default", "--reporter=json", `--outputFile.json=${options.outputFile}`);
|
|
486
|
-
}
|
|
487
506
|
if (options.spec) {
|
|
488
507
|
let specPattern = options.spec.replace(/\\/g, "/");
|
|
489
508
|
if (path2.isAbsolute(options.spec)) {
|
|
@@ -520,8 +539,18 @@ async function runComponentTests(options = {}) {
|
|
|
520
539
|
});
|
|
521
540
|
}
|
|
522
541
|
function getFlagValue(argv, ...names) {
|
|
523
|
-
|
|
524
|
-
|
|
542
|
+
for (let i = 0; i < argv.length; i++) {
|
|
543
|
+
const arg = argv[i];
|
|
544
|
+
for (const name of names) {
|
|
545
|
+
if (arg === name) {
|
|
546
|
+
return argv[i + 1];
|
|
547
|
+
}
|
|
548
|
+
if (arg.startsWith(`${name}=`)) {
|
|
549
|
+
return arg.slice(name.length + 1);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return void 0;
|
|
525
554
|
}
|
|
526
555
|
async function runCli(argv = process.argv.slice(2)) {
|
|
527
556
|
const [command] = argv;
|
|
@@ -532,7 +561,9 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
532
561
|
let headless = true;
|
|
533
562
|
if (argv.includes("--headed") || argv.includes("--no-headless")) headless = false;
|
|
534
563
|
else if (argv.includes("--headless")) headless = true;
|
|
535
|
-
const
|
|
564
|
+
const rawRunner = getFlagValue(argv, "--runner", "-r");
|
|
565
|
+
const runner = rawRunner === "vitest" ? "vitest" : "spectra";
|
|
566
|
+
const rawDelay = getFlagValue(argv, "--step-delay", "--delay", "-d");
|
|
536
567
|
let stepDelayMs;
|
|
537
568
|
if (rawDelay !== void 0) {
|
|
538
569
|
const parsed = parseInt(String(rawDelay), 10);
|
|
@@ -541,8 +572,11 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
541
572
|
let spec = getFlagValue(argv, "--spec", "-s");
|
|
542
573
|
if (!spec) {
|
|
543
574
|
const flagsWithValues = /* @__PURE__ */ new Set([
|
|
575
|
+
"--runner",
|
|
576
|
+
"-r",
|
|
544
577
|
"--step-delay",
|
|
545
578
|
"--delay",
|
|
579
|
+
"-d",
|
|
546
580
|
"--spec",
|
|
547
581
|
"-s",
|
|
548
582
|
"--output",
|
|
@@ -569,12 +603,337 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
569
603
|
if (!isNaN(parsed) && parsed >= 1) concurrency = parsed;
|
|
570
604
|
}
|
|
571
605
|
const browserExecutablePath = getFlagValue(argv, "--browser-executable");
|
|
572
|
-
const ok = await runComponentTests({
|
|
606
|
+
const ok = await runComponentTests({
|
|
607
|
+
runner,
|
|
608
|
+
headless,
|
|
609
|
+
stepDelayMs,
|
|
610
|
+
spec,
|
|
611
|
+
outputFile,
|
|
612
|
+
concurrency,
|
|
613
|
+
browserExecutablePath
|
|
614
|
+
});
|
|
573
615
|
process.exit(ok ? 0 : 1);
|
|
574
616
|
}
|
|
617
|
+
|
|
618
|
+
// src/cdp_runner/bundle_server.ts
|
|
619
|
+
import fs from "node:fs";
|
|
620
|
+
import http from "node:http";
|
|
621
|
+
import path3 from "node:path";
|
|
622
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
623
|
+
import * as esbuild from "esbuild";
|
|
624
|
+
var bundleCache = /* @__PURE__ */ new Map();
|
|
625
|
+
var inFlightBuilds = /* @__PURE__ */ new Map();
|
|
626
|
+
async function buildBundle(entrypoint, opts = {}) {
|
|
627
|
+
const result = await esbuild.build({
|
|
628
|
+
entryPoints: [entrypoint],
|
|
629
|
+
bundle: true,
|
|
630
|
+
format: "esm",
|
|
631
|
+
platform: "browser",
|
|
632
|
+
target: "esnext",
|
|
633
|
+
external: opts.external ?? [],
|
|
634
|
+
minify: false,
|
|
635
|
+
write: false,
|
|
636
|
+
// Inline source maps so Chrome DevTools can map errors back to .tsx source lines.
|
|
637
|
+
sourcemap: "inline",
|
|
638
|
+
// React 17+ automatic JSX runtime — test files do not need `import React from 'react'`.
|
|
639
|
+
jsx: "automatic",
|
|
640
|
+
jsxImportSource: "react"
|
|
641
|
+
});
|
|
642
|
+
if (result.errors.length > 0) {
|
|
643
|
+
const msgs = result.errors.map((e) => e.text).join("\n");
|
|
644
|
+
throw new Error(`esbuild failed for ${entrypoint}:
|
|
645
|
+
${msgs}`);
|
|
646
|
+
}
|
|
647
|
+
return result.outputFiles[0].text;
|
|
648
|
+
}
|
|
649
|
+
async function getCachedBundle(cacheKey, entrypoint, opts) {
|
|
650
|
+
const cached = bundleCache.get(cacheKey);
|
|
651
|
+
if (cached) return cached;
|
|
652
|
+
const existingInFlight = inFlightBuilds.get(cacheKey);
|
|
653
|
+
if (existingInFlight) return existingInFlight;
|
|
654
|
+
const buildPromise = buildBundle(entrypoint, opts).then(
|
|
655
|
+
(js) => {
|
|
656
|
+
bundleCache.set(cacheKey, js);
|
|
657
|
+
inFlightBuilds.delete(cacheKey);
|
|
658
|
+
return js;
|
|
659
|
+
},
|
|
660
|
+
(err) => {
|
|
661
|
+
inFlightBuilds.delete(cacheKey);
|
|
662
|
+
throw err;
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
inFlightBuilds.set(cacheKey, buildPromise);
|
|
666
|
+
return buildPromise;
|
|
667
|
+
}
|
|
668
|
+
function buildHarnessHtml(manifest, stepDelayMs) {
|
|
669
|
+
const manifestJson = JSON.stringify(manifest);
|
|
670
|
+
return `<!DOCTYPE html>
|
|
671
|
+
<html>
|
|
672
|
+
<head><meta charset="utf-8"><title>Spectra Component Test</title></head>
|
|
673
|
+
<body>
|
|
674
|
+
<script>
|
|
675
|
+
window.__TESTSPECTRA_AUTO_IMPORT_MANIFEST__ = ${manifestJson};
|
|
676
|
+
window.__TESTSPECTRA_STEP_DELAY_MS__ = ${JSON.stringify(stepDelayMs)};
|
|
677
|
+
window.__TESTSPECTRA_SEMANTIC_REPORTER__ = true;
|
|
678
|
+
</script>
|
|
679
|
+
<script type="module" src="/runner.js"></script>
|
|
680
|
+
<script type="module" src="/setup.js"></script>
|
|
681
|
+
</body>
|
|
682
|
+
</html>`;
|
|
683
|
+
}
|
|
684
|
+
function respond(res, status, contentType, body) {
|
|
685
|
+
res.writeHead(status, { "Content-Type": contentType });
|
|
686
|
+
res.end(body);
|
|
687
|
+
}
|
|
688
|
+
function resolveReactPackageDir() {
|
|
689
|
+
let curr = path3.dirname(fileURLToPath2(import.meta.url));
|
|
690
|
+
while (curr !== path3.dirname(curr)) {
|
|
691
|
+
const pkgPath = path3.join(curr, "package.json");
|
|
692
|
+
if (fs.existsSync(pkgPath)) {
|
|
693
|
+
try {
|
|
694
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
695
|
+
if (pkg.name === "@testspectra/react") {
|
|
696
|
+
return curr;
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
curr = path3.dirname(curr);
|
|
702
|
+
}
|
|
703
|
+
return path3.resolve(path3.dirname(fileURLToPath2(import.meta.url)), "../..");
|
|
704
|
+
}
|
|
705
|
+
function makeHandler(testRoot, packageDir, harnessHtml, setupEntrypoint, runnerEntrypoint) {
|
|
706
|
+
return async (req, res) => {
|
|
707
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
708
|
+
if (url.pathname === "/") {
|
|
709
|
+
respond(res, 200, "text/html; charset=utf-8", harnessHtml);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (url.pathname.startsWith("/@fs/")) {
|
|
713
|
+
const rawFsPath = url.pathname.slice("/@fs/".length);
|
|
714
|
+
const fsPath = rawFsPath.startsWith("/") ? rawFsPath : `/${rawFsPath}`;
|
|
715
|
+
const resolved = path3.resolve(fsPath);
|
|
716
|
+
if (resolved.endsWith(".json")) {
|
|
717
|
+
try {
|
|
718
|
+
const raw = fs.readFileSync(resolved, "utf-8");
|
|
719
|
+
respond(res, 200, "application/json; charset=utf-8", raw);
|
|
720
|
+
} catch (err) {
|
|
721
|
+
respond(res, 500, "application/json", JSON.stringify({ error: String(err) }));
|
|
722
|
+
}
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const js = await getCachedBundle(resolved, resolved);
|
|
727
|
+
respond(res, 200, "application/javascript; charset=utf-8", js);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
respond(
|
|
730
|
+
res,
|
|
731
|
+
200,
|
|
732
|
+
"application/javascript",
|
|
733
|
+
`throw new Error(${JSON.stringify(`Module bundle error for ${fsPath}: ${String(err)}`)});`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (url.pathname === "/setup.js") {
|
|
739
|
+
try {
|
|
740
|
+
const js = await getCachedBundle("setup", setupEntrypoint);
|
|
741
|
+
respond(res, 200, "application/javascript; charset=utf-8", js);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
respond(res, 500, "application/javascript", `// Bundle error: ${String(err)}`);
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (url.pathname === "/runner.js") {
|
|
748
|
+
try {
|
|
749
|
+
const js = await getCachedBundle("runner", runnerEntrypoint);
|
|
750
|
+
respond(res, 200, "application/javascript; charset=utf-8", js);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
respond(res, 500, "application/javascript", `// Bundle error: ${String(err)}`);
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (url.pathname === "/test") {
|
|
757
|
+
const encodedFile = url.searchParams.get("file");
|
|
758
|
+
if (!encodedFile) {
|
|
759
|
+
respond(res, 400, "text/plain", "Missing ?file= parameter");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const testFilePath = decodeURIComponent(encodedFile);
|
|
763
|
+
const resolved = path3.resolve(testFilePath);
|
|
764
|
+
const rootResolved = path3.resolve(testRoot);
|
|
765
|
+
if (!resolved.startsWith(rootResolved + path3.sep) && resolved !== rootResolved) {
|
|
766
|
+
respond(res, 403, "text/plain", "Forbidden: path outside test root");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (resolved.endsWith(".json")) {
|
|
770
|
+
try {
|
|
771
|
+
const raw = fs.readFileSync(resolved, "utf-8");
|
|
772
|
+
respond(res, 200, "application/json; charset=utf-8", raw);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
respond(res, 500, "application/json", JSON.stringify({ error: String(err) }));
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
const js = await getCachedBundle(resolved, resolved);
|
|
780
|
+
respond(res, 200, "application/javascript; charset=utf-8", js);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
const msg = String(err);
|
|
783
|
+
respond(res, 200, "application/javascript", `throw new Error(${JSON.stringify(`Test bundle error: ${msg}`)});`);
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
respond(res, 404, "text/plain", "Not Found");
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
async function startBundleServer(opts = {}) {
|
|
791
|
+
const testRoot = path3.resolve(opts.testRoot ?? process.cwd());
|
|
792
|
+
const packageDir = opts.packageDir ?? resolveReactPackageDir();
|
|
793
|
+
const manifest = opts.manifest ?? { pageObjects: [], steps: [], fixtures: [] };
|
|
794
|
+
const stepDelayMs = opts.stepDelayMs ?? 0;
|
|
795
|
+
const setupEntrypoint = path3.join(packageDir, "src", "setup.ts");
|
|
796
|
+
const runnerEntrypoint = path3.join(packageDir, "src", "cdp_runner", "in_page_runner.ts");
|
|
797
|
+
const distSetup = path3.join(packageDir, "dist", "setup.js");
|
|
798
|
+
if (fs.existsSync(distSetup)) {
|
|
799
|
+
try {
|
|
800
|
+
bundleCache.set("setup", fs.readFileSync(distSetup, "utf-8"));
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const distRunner = path3.join(packageDir, "dist", "runner.js");
|
|
805
|
+
if (fs.existsSync(distRunner)) {
|
|
806
|
+
try {
|
|
807
|
+
bundleCache.set("runner", fs.readFileSync(distRunner, "utf-8"));
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (!bundleCache.has("setup")) {
|
|
812
|
+
getCachedBundle("setup", setupEntrypoint).catch(() => {
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
if (!bundleCache.has("runner")) {
|
|
816
|
+
getCachedBundle("runner", runnerEntrypoint).catch(() => {
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
const harnessHtml = buildHarnessHtml(manifest, stepDelayMs);
|
|
820
|
+
const handler = makeHandler(testRoot, packageDir, harnessHtml, setupEntrypoint, runnerEntrypoint);
|
|
821
|
+
const server = http.createServer(handler);
|
|
822
|
+
await new Promise((resolve, reject) => {
|
|
823
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
824
|
+
server.once("error", reject);
|
|
825
|
+
});
|
|
826
|
+
const addr = server.address();
|
|
827
|
+
const baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
828
|
+
return {
|
|
829
|
+
url: baseUrl,
|
|
830
|
+
stop: () => server.close()
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/sanitizer.ts
|
|
835
|
+
var VITE_FS_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\/@fs\/([A-Za-z]:\/)/g;
|
|
836
|
+
var VITE_FS_UNIX_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\/@fs\//g;
|
|
837
|
+
var VITE_ROOT_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\//g;
|
|
838
|
+
var URL_QUERY_PATTERN = /(\.[a-zA-Z0-9]+)\?[^:\s'")]*/g;
|
|
839
|
+
var INTERNAL_FRAME_PATTERNS = [
|
|
840
|
+
/node_modules[\\/]vitest[\\/]/i,
|
|
841
|
+
/node_modules[\\/]@vitest[\\/]/i,
|
|
842
|
+
/node_modules[\\/]vite[\\/]/i,
|
|
843
|
+
/node_modules[\\/]@vitejs[\\/]/i,
|
|
844
|
+
/node_modules[\\/]playwright[\\/]/i,
|
|
845
|
+
/node_modules[\\/]@playwright[\\/]/i,
|
|
846
|
+
/node:internal[\\/]/i,
|
|
847
|
+
/dist[\\/]setup\.js/i,
|
|
848
|
+
/dist[\\/]vitest_reporter\.js/i,
|
|
849
|
+
/tools[\\/]react[\\/]/i,
|
|
850
|
+
/[\\/]vitest\.config\./i,
|
|
851
|
+
/@vitest/i,
|
|
852
|
+
/__vitest__/i
|
|
853
|
+
];
|
|
854
|
+
function sanitizeViteUrl(text) {
|
|
855
|
+
if (!text) return "";
|
|
856
|
+
let result = text.replace(VITE_FS_URL_PATTERN, "$1");
|
|
857
|
+
result = result.replace(VITE_FS_UNIX_URL_PATTERN, "/");
|
|
858
|
+
result = result.replace(VITE_ROOT_URL_PATTERN, "./");
|
|
859
|
+
result = result.replace(URL_QUERY_PATTERN, "$1");
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
function sanitizeErrorMessage(msg) {
|
|
863
|
+
if (!msg) return "";
|
|
864
|
+
let sanitized = sanitizeViteUrl(msg);
|
|
865
|
+
sanitized = sanitized.replace(/Failed to fetch dynamically imported module:\s*/gi, "Failed to load module: ");
|
|
866
|
+
sanitized = sanitized.replace(/\[vite\]\s*(?:Internal server error:\s*)?/gi, "[TestSpectra] ");
|
|
867
|
+
sanitized = sanitized.replace(/@vitest\/browser/gi, "TestSpectra Browser");
|
|
868
|
+
sanitized = sanitized.replace(/@vitest\/runner/gi, "TestSpectra Runner");
|
|
869
|
+
sanitized = sanitized.replace(/\bvitest\.config\.[a-z0-9]+/gi, "spectra.config.ts");
|
|
870
|
+
sanitized = sanitized.replace(/\bVitest\b/g, "TestSpectra");
|
|
871
|
+
sanitized = sanitized.replace(/\bvitest\b/g, "testspectra");
|
|
872
|
+
return sanitized;
|
|
873
|
+
}
|
|
874
|
+
function sanitizeStackTrace(stack) {
|
|
875
|
+
if (!stack) return "";
|
|
876
|
+
const lines = stack.split("\n");
|
|
877
|
+
const filtered = [];
|
|
878
|
+
for (let i = 0; i < lines.length; i++) {
|
|
879
|
+
const line = lines[i];
|
|
880
|
+
if (i === 0) {
|
|
881
|
+
filtered.push(sanitizeErrorMessage(line));
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const isInternal = INTERNAL_FRAME_PATTERNS.some((pattern) => pattern.test(line));
|
|
885
|
+
if (isInternal) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
filtered.push(sanitizeErrorMessage(line));
|
|
889
|
+
}
|
|
890
|
+
return filtered.join("\n").trim();
|
|
891
|
+
}
|
|
892
|
+
function sanitizeError(err) {
|
|
893
|
+
if (!err) return "";
|
|
894
|
+
if (typeof err === "string") {
|
|
895
|
+
return sanitizeStackTrace(err);
|
|
896
|
+
}
|
|
897
|
+
if (typeof err === "object") {
|
|
898
|
+
const errorObj = err;
|
|
899
|
+
const stack = errorObj.stack;
|
|
900
|
+
const message = errorObj.message || String(err);
|
|
901
|
+
if (stack && typeof stack === "string") {
|
|
902
|
+
const sanitizedStack = sanitizeStackTrace(stack);
|
|
903
|
+
if (sanitizedStack) return sanitizedStack;
|
|
904
|
+
}
|
|
905
|
+
return sanitizeErrorMessage(message);
|
|
906
|
+
}
|
|
907
|
+
return sanitizeErrorMessage(String(err));
|
|
908
|
+
}
|
|
909
|
+
function isVitestNoise(line) {
|
|
910
|
+
const trimmed = line.trim();
|
|
911
|
+
if (!trimmed) return true;
|
|
912
|
+
if (/^RUN\s+v\d+/i.test(trimmed)) return true;
|
|
913
|
+
if (/^DEV\s+v\d+/i.test(trimmed)) return true;
|
|
914
|
+
if (/^Test Files\s+\d+/i.test(trimmed)) return true;
|
|
915
|
+
if (/^Tests\s+\d+/i.test(trimmed)) return true;
|
|
916
|
+
if (/^Start at\s+\d+/i.test(trimmed)) return true;
|
|
917
|
+
if (/^Duration\s+[\d.]+/i.test(trimmed)) return true;
|
|
918
|
+
if (/^⎯{3,}/.test(trimmed)) return true;
|
|
919
|
+
if (/^Re-optimizing dependencies/i.test(trimmed)) return true;
|
|
920
|
+
if (/^JSON report written to/i.test(trimmed)) return true;
|
|
921
|
+
if (/^\(?transform \d+ms/i.test(trimmed)) return true;
|
|
922
|
+
if (/^[·✓❯⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s+/.test(trimmed)) return true;
|
|
923
|
+
if (/vite-cjs-node-api-deprecated/i.test(trimmed)) return true;
|
|
924
|
+
if (/CJS build of Vite's Node API is deprecated/i.test(trimmed)) return true;
|
|
925
|
+
if (/^\[?vite\]?\s*(deprecation|warning)/i.test(trimmed)) return true;
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
575
928
|
export {
|
|
576
929
|
DomDriverBridge,
|
|
930
|
+
isVitestNoise,
|
|
577
931
|
mount,
|
|
578
932
|
runCli,
|
|
579
|
-
runComponentTests
|
|
933
|
+
runComponentTests,
|
|
934
|
+
sanitizeError,
|
|
935
|
+
sanitizeErrorMessage,
|
|
936
|
+
sanitizeStackTrace,
|
|
937
|
+
sanitizeViteUrl,
|
|
938
|
+
startBundleServer
|
|
580
939
|
};
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var i=[],a=[],T=[];globalThis.it=function(n,t){i.push({title:n,fn:t})};globalThis.beforeEach=function(n){a.push(n)};globalThis.afterEach=function(n){T.push(n)};function _(){let n=globalThis.Spectra;if(typeof n?.unmount=="function")n.unmount();else{let t=document.getElementById("spectra-root");t&&(t.innerHTML="")}}function r(n,t){console.log(`[${n}] ${t}`)}globalThis.__SPECTRA_RUNNER__={async run(n,t){r("TESTSPECTRA_SUITE_START",JSON.stringify({file:n}));let c=globalThis.__bootstrapAutoImports;if(typeof c=="function")try{await c(t)}catch(o){r("TESTSPECTRA_SUITE_ERROR",JSON.stringify({file:n,error:`Failed to bootstrap auto imports: ${String(o)}`}));return}let l=encodeURIComponent(n);try{await import(`/test?file=${l}`)}catch(o){r("TESTSPECTRA_SUITE_ERROR",JSON.stringify({file:n,error:String(o)}));return}let f=0,S=0;for(let{title:o,fn:g}of i){r("TESTSPECTRA_TEST_START",o),globalThis.__CURRENT_TEST_TITLE__=o;let E={task:{name:o}},u=performance.now();try{for(let e of a)await e(E);await g();for(let e of T)await e(E);let s=Math.round(performance.now()-u);r("TESTSPECTRA_TEST_PASS",`${o} (${s}ms)`),f++}catch(s){let e=Math.round(performance.now()-u),R=s instanceof Error?s.message:String(s);r("TESTSPECTRA_TEST_ERROR",R),r("TESTSPECTRA_TEST_FAIL",`${o} (${e}ms)`),S++}finally{_()}}i.length=0,a.length=0,T.length=0,r("TESTSPECTRA_SUITE_END",JSON.stringify({file:n,passed:f,failed:S}))}};
|