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