@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 ADDED
@@ -0,0 +1,48 @@
1
+ # TestSpectra Commercial Software License Agreement
2
+
3
+ Copyright (c) 2026 TestSpectra. All rights reserved.
4
+
5
+ IMPORTANT NOTICE: This software is NOT open-source. Although distribution archives,
6
+ npm packages, and editor extensions are publicly downloadable, installation,
7
+ execution, and use of the Software are strictly conditioned upon obtaining a valid
8
+ Commercial License or an authorized Evaluation/Trial License from TestSpectra.
9
+
10
+ 1. GRANT OF LICENSE
11
+ Subject to payment of applicable license fees or an active authorized evaluation
12
+ period, TestSpectra grants you a non-exclusive, non-transferable, revocable license
13
+ to install and execute the Software solely for authoring, debugging, and executing
14
+ automated software tests.
15
+
16
+ 2. LICENSE ACTIVATION & ENFORCEMENT
17
+ - Use of the Software requires activation via a valid License Key, authorized CI token,
18
+ or active TestSpectra Cloud account.
19
+ - Any attempt to circumvent, disable, modify, reverse engineer, or tamper with the
20
+ license verification mechanisms, cryptographic signatures, or binary checks is
21
+ a material breach of this Agreement and automatically terminates your license.
22
+
23
+ 3. RESTRICTIONS
24
+ You shall not, and shall not permit any third party to:
25
+ - Decompile, disassemble, reverse engineer, or derive source code from the binaries,
26
+ compiled scripts, or libraries of the Software.
27
+ - Sublicense, sell, rent, lease, distribute, or host the Software as a service for third parties.
28
+ - Re-brand, white-label, or package the Software or any of its components as part of a
29
+ competing commercial QA, testing, or developer tooling product.
30
+ - Modify, alter, or remove any proprietary notices, copyright labels, or license headers.
31
+
32
+ 4. INTELLECTUAL PROPERTY
33
+ The Software, including its source code, architecture, Rust core engine, algorithms,
34
+ matchers, extensions, interfaces, and documentation, represents valuable intellectual
35
+ property and trade secrets of TestSpectra.
36
+
37
+ 5. TERMINATION
38
+ This license is effective until terminated. Your rights under this license will terminate
39
+ automatically without notice if you fail to comply with any of its terms, or upon expiration
40
+ of your active subscription or trial term. Upon termination, you must cease all use of
41
+ the Software and delete all copies in your possession.
42
+
43
+ 6. DISCLAIMER OF WARRANTY & LIMITATION OF LIABILITY
44
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
45
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
46
+ PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
47
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
48
+ ARISING FROM THE USE OF OR INABILITY TO USE THE SOFTWARE.
@@ -1,7 +1,7 @@
1
- #!/usr/bin/env node
2
- import { runCli } from '../dist/cli.js';
3
-
4
- runCli().catch((err) => {
5
- console.error(err);
6
- process.exit(1);
7
- });
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../dist/cli.js';
3
+
4
+ runCli().catch((err) => {
5
+ console.error(err);
6
+ process.exit(1);
7
+ });
package/dist/cli.js CHANGED
@@ -1,33 +1,180 @@
1
1
  // src/cli.ts
2
- import { spawn } from "node:child_process";
2
+ import { existsSync as existsSync2 } from "node:fs";
3
+ import path2 from "node:path";
3
4
  import { fileURLToPath } from "node:url";
5
+ import spawn from "cross-spawn";
6
+
7
+ // src/config_generator.ts
8
+ import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { pathToFileURL } from "node:url";
13
+ function resolveEsmUrl(specifier, packageDir) {
14
+ const cjsPath = createRequire(path.join(packageDir, "package.json")).resolve(specifier);
15
+ const esmPath = cjsPath.replace(/\.cjs$/, ".js");
16
+ return pathToFileURL(existsSync(esmPath) ? esmPath : cjsPath).href;
17
+ }
18
+ function buildAutoImportManifest(root) {
19
+ function scanEntityDir(subdir, filePattern) {
20
+ const dir = path.join(root, subdir);
21
+ if (!existsSync(dir)) return [];
22
+ const entries = [];
23
+ for (const name of readdirSync(dir, { withFileTypes: true })) {
24
+ if (!name.isDirectory()) continue;
25
+ if (name.name.startsWith(".") || name.name === "node_modules" || name.name === "dist" || name.name === "__screenshots__" || name.name === "coverage") {
26
+ continue;
27
+ }
28
+ for (const platform of ["web", "common"]) {
29
+ for (const ext of ["tsx", "ts"]) {
30
+ const candidate = path.join(dir, name.name, filePattern(platform).replace(/\.tsx?$/, `.${ext}`));
31
+ if (existsSync(candidate)) {
32
+ entries.push({ name: name.name, path: candidate.replace(/\\/g, "/") });
33
+ break;
34
+ }
35
+ }
36
+ if (entries.at(-1)?.name === name.name) break;
37
+ }
38
+ }
39
+ return entries;
40
+ }
41
+ function scanFixtures() {
42
+ const dir = path.join(root, "fixtures");
43
+ if (!existsSync(dir)) return [];
44
+ return readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => ({ name: f.replace(/\.json$/, ""), path: path.join(dir, f).replace(/\\/g, "/") }));
45
+ }
46
+ return {
47
+ pageObjects: scanEntityDir("page-objects", (p) => `${p}.tsx`),
48
+ steps: scanEntityDir("support/steps", (p) => `${p}.step.tsx`),
49
+ fixtures: scanFixtures()
50
+ };
51
+ }
52
+ function generateVitestConfig(opts) {
53
+ const reactPluginUrl = resolveEsmUrl("@vitejs/plugin-react", opts.packageDir);
54
+ const vitestConfigUrl = resolveEsmUrl("vitest/config", opts.packageDir);
55
+ const manifest = buildAutoImportManifest(opts.testRoot);
56
+ const setupFile = path.join(opts.packageDir, "dist/setup.js").replace(/\\/g, "/");
57
+ const reporterFile = path.join(opts.packageDir, "dist/vitest_reporter.js").replace(/\\/g, "/");
58
+ const reporterUrl = pathToFileURL(reporterFile).href;
59
+ const normalizedPackageDir = opts.packageDir.replace(/\\/g, "/");
60
+ const normalizedTestRoot = opts.testRoot.replace(/\\/g, "/");
61
+ const configContent = `import react from '${reactPluginUrl}';
62
+ import { defineConfig } from '${vitestConfigUrl}';
63
+ ${opts.semanticReporter ? `import { SpectraVitestReporter } from '${reporterUrl}';` : ""}
64
+
65
+ export default defineConfig({
66
+ root: ${JSON.stringify(normalizedTestRoot)},
67
+ plugins: [react()],
68
+ define: {
69
+ __TESTSPECTRA_AUTO_IMPORT_MANIFEST__: ${JSON.stringify(JSON.stringify(manifest))},
70
+ __TESTSPECTRA_STEP_DELAY_MS__: ${JSON.stringify(opts.stepDelayMs)},
71
+ __TESTSPECTRA_SEMANTIC_REPORTER__: ${JSON.stringify(opts.semanticReporter)},
72
+ },
73
+ optimizeDeps: {
74
+ include: [
75
+ 'react',
76
+ 'react-dom',
77
+ 'react-dom/client',
78
+ '@testing-library/react',
79
+ '@testing-library/user-event',
80
+ ],
81
+ },
82
+ server: {
83
+ fs: {
84
+ allow: [
85
+ ${JSON.stringify(normalizedPackageDir)},
86
+ ${JSON.stringify(normalizedTestRoot)},
87
+ ],
88
+ },
89
+ },
90
+ test: {
91
+ globals: true,
92
+ setupFiles: [${JSON.stringify(setupFile)}],
93
+ reporters: ${opts.semanticReporter ? `[
94
+ new SpectraVitestReporter({ testRoot: ${JSON.stringify(normalizedTestRoot)} }),
95
+ ${opts.outputFile ? `['json', { outputFile: ${JSON.stringify(opts.outputFile)} }],` : ""}
96
+ ]` : opts.outputFile ? `['default', ['json', { outputFile: ${JSON.stringify(opts.outputFile)} }]]` : "'default'"},
97
+ dir: ${JSON.stringify(normalizedTestRoot)},
98
+ include: [
99
+ 'specs/**/web.test.tsx',
100
+ 'specs/**/*.test.tsx',
101
+ 'specs/**/web.test.ts',
102
+ 'specs/**/*.test.ts',
103
+ ],
104
+ exclude: [
105
+ '**/node_modules/**',
106
+ '**/dist/**',
107
+ '**/__screenshots__/**',
108
+ '**/.testspectra/**',
109
+ '**/.git/**',
110
+ '**/coverage/**',
111
+ ],
112
+ browser: {
113
+ enabled: true,
114
+ provider: 'playwright',
115
+ name: 'chromium',
116
+ headless: ${opts.headless},
117
+ fileParallelism: ${opts.fileParallelism},
118
+ ${opts.browserExecutable ? `providerOptions: { launch: { executablePath: ${JSON.stringify(opts.browserExecutable)} } },` : ""}
119
+ },
120
+ },
121
+ });
122
+ `;
123
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "testspectra-react-"));
124
+ const configPath = path.join(tmpDir, "vitest.config.mjs");
125
+ writeFileSync(configPath, configContent, "utf-8");
126
+ return {
127
+ configPath,
128
+ cleanup: () => {
129
+ try {
130
+ rmSync(tmpDir, { recursive: true, force: true });
131
+ } catch {
132
+ }
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/cli.ts
4
138
  async function runComponentTests(options = {}) {
5
- const cwd = options.cwd ?? process.cwd();
6
- const configPath = fileURLToPath(new URL("../vitest.config.ts", import.meta.url));
139
+ const runner = options.runner ?? "vitest";
140
+ if (runner === "spectra") {
141
+ console.log("TestSpectra native component testing is orchestrated by the Rust Core Engine.");
142
+ console.log("Use `spectra test` to run tests with full parallel CDP orchestration.");
143
+ return true;
144
+ }
145
+ const rawCwd = options.cwd ?? process.cwd();
146
+ const cwd = path2.resolve(rawCwd);
147
+ const packageDir = fileURLToPath(new URL("..", import.meta.url));
7
148
  const ext = process.platform === "win32" ? ".cmd" : "";
8
- const vitestBin = fileURLToPath(new URL(`../node_modules/.bin/vitest${ext}`, import.meta.url));
149
+ const localVitestBin = fileURLToPath(new URL(`../node_modules/.bin/vitest${ext}`, import.meta.url));
150
+ const consumerVitestBin = path2.join(cwd, `node_modules/.bin/vitest${ext}`);
151
+ const vitestBin = existsSync2(localVitestBin) ? localVitestBin : consumerVitestBin;
152
+ const semanticReporter = Boolean(options.onLine);
153
+ const { configPath, cleanup } = generateVitestConfig({
154
+ packageDir,
155
+ testRoot: cwd,
156
+ headless: options.headless !== false,
157
+ stepDelayMs: options.stepDelayMs ?? 0,
158
+ semanticReporter,
159
+ fileParallelism: options.concurrency !== 1,
160
+ browserExecutable: options.browserExecutablePath,
161
+ outputFile: options.outputFile
162
+ });
9
163
  const args = ["run", "--config", configPath];
10
- if (options.outputFile) {
11
- args.push("--reporter=default", "--reporter=json", `--outputFile.json=${options.outputFile}`);
12
- }
13
164
  if (options.spec) {
14
- args.push(options.spec);
165
+ let specPattern = options.spec.replace(/\\/g, "/");
166
+ if (path2.isAbsolute(options.spec)) {
167
+ specPattern = path2.relative(cwd, options.spec).replace(/\\/g, "/");
168
+ }
169
+ args.push(specPattern);
15
170
  }
16
- const semanticReporter = Boolean(options.onLine);
17
171
  return new Promise((resolve) => {
18
172
  const child = spawn(vitestBin, args, {
19
173
  stdio: semanticReporter ? ["inherit", "pipe", "pipe"] : "inherit",
20
174
  cwd,
21
175
  env: {
22
- ...process.env,
23
- TESTSPECTRA_TEST_ROOT: cwd,
24
- TESTSPECTRA_HEADLESS: options.headless === false ? "false" : "true",
25
- TESTSPECTRA_STEP_DELAY: String(options.stepDelayMs ?? 0),
26
- TESTSPECTRA_FILE_PARALLELISM: options.concurrency === 1 ? "false" : "true",
27
- ...options.browserExecutablePath ? { TESTSPECTRA_BROWSER_EXECUTABLE: options.browserExecutablePath } : {},
28
- TESTSPECTRA_SEMANTIC_REPORTER: semanticReporter ? "1" : "0"
29
- },
30
- shell: process.platform === "win32"
176
+ ...process.env
177
+ }
31
178
  });
32
179
  if (semanticReporter) {
33
180
  const handleData = (buf) => {
@@ -38,16 +185,30 @@ async function runComponentTests(options = {}) {
38
185
  child.stdout?.on("data", handleData);
39
186
  child.stderr?.on("data", handleData);
40
187
  }
41
- child.on("exit", (code) => resolve(code === 0));
188
+ child.on("exit", (code) => {
189
+ cleanup();
190
+ resolve(code === 0);
191
+ });
42
192
  child.on("error", (err) => {
193
+ cleanup();
43
194
  console.error(err);
44
195
  resolve(false);
45
196
  });
46
197
  });
47
198
  }
48
199
  function getFlagValue(argv, ...names) {
49
- const index = argv.findIndex((a) => names.includes(a));
50
- return index !== -1 ? argv[index + 1] : void 0;
200
+ for (let i = 0; i < argv.length; i++) {
201
+ const arg = argv[i];
202
+ for (const name of names) {
203
+ if (arg === name) {
204
+ return argv[i + 1];
205
+ }
206
+ if (arg.startsWith(`${name}=`)) {
207
+ return arg.slice(name.length + 1);
208
+ }
209
+ }
210
+ }
211
+ return void 0;
51
212
  }
52
213
  async function runCli(argv = process.argv.slice(2)) {
53
214
  const [command] = argv;
@@ -58,13 +219,40 @@ async function runCli(argv = process.argv.slice(2)) {
58
219
  let headless = true;
59
220
  if (argv.includes("--headed") || argv.includes("--no-headless")) headless = false;
60
221
  else if (argv.includes("--headless")) headless = true;
61
- const rawDelay = getFlagValue(argv, "--step-delay", "--delay");
222
+ const rawRunner = getFlagValue(argv, "--runner", "-r");
223
+ const runner = rawRunner === "vitest" ? "vitest" : "spectra";
224
+ const rawDelay = getFlagValue(argv, "--step-delay", "--delay", "-d");
62
225
  let stepDelayMs;
63
226
  if (rawDelay !== void 0) {
64
227
  const parsed = parseInt(String(rawDelay), 10);
65
228
  if (!isNaN(parsed) && parsed >= 0) stepDelayMs = parsed;
66
229
  }
67
- const spec = getFlagValue(argv, "--spec", "-s");
230
+ let spec = getFlagValue(argv, "--spec", "-s");
231
+ if (!spec) {
232
+ const flagsWithValues = /* @__PURE__ */ new Set([
233
+ "--runner",
234
+ "-r",
235
+ "--step-delay",
236
+ "--delay",
237
+ "-d",
238
+ "--spec",
239
+ "-s",
240
+ "--output",
241
+ "-o",
242
+ "--concurrency",
243
+ "-c",
244
+ "--browser-executable"
245
+ ]);
246
+ for (let i = 1; i < argv.length; i++) {
247
+ const arg = argv[i];
248
+ if (arg.startsWith("-")) {
249
+ if (flagsWithValues.has(arg)) i++;
250
+ } else {
251
+ spec = arg;
252
+ break;
253
+ }
254
+ }
255
+ }
68
256
  const outputFile = getFlagValue(argv, "--output", "-o");
69
257
  const rawConcurrency = getFlagValue(argv, "--concurrency", "-c");
70
258
  let concurrency;
@@ -73,7 +261,15 @@ async function runCli(argv = process.argv.slice(2)) {
73
261
  if (!isNaN(parsed) && parsed >= 1) concurrency = parsed;
74
262
  }
75
263
  const browserExecutablePath = getFlagValue(argv, "--browser-executable");
76
- const ok = await runComponentTests({ headless, stepDelayMs, spec, outputFile, concurrency, browserExecutablePath });
264
+ const ok = await runComponentTests({
265
+ runner,
266
+ headless,
267
+ stepDelayMs,
268
+ spec,
269
+ outputFile,
270
+ concurrency,
271
+ browserExecutablePath
272
+ });
77
273
  process.exit(ok ? 0 : 1);
78
274
  }
79
275
  export {
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare class DomDriverBridge implements PlatformDriverBridge {
7
7
  export declare function mount<P = any>(element: any): Promise<any>;
8
8
 
9
9
  export interface RunComponentTestsOptions {
10
+ runner?: 'spectra' | 'vitest';
10
11
  cwd?: string;
11
12
  headless?: boolean;
12
13
  stepDelayMs?: number;
@@ -17,6 +18,23 @@ export interface RunComponentTestsOptions {
17
18
  onLine?: (line: string) => void;
18
19
  }
19
20
 
21
+ export interface BundleServer {
22
+ url: string;
23
+ stop(): void;
24
+ }
25
+
26
+ export interface BundleServerOptions {
27
+ testRoot?: string;
28
+ packageDir?: string;
29
+ manifest?: any;
30
+ stepDelayMs?: number;
31
+ }
32
+
33
+ export declare function startBundleServer(opts?: BundleServerOptions): Promise<BundleServer>;
20
34
  export declare function runComponentTests(options?: RunComponentTestsOptions): Promise<boolean>;
21
35
  export declare function runCli(argv?: string[]): Promise<void>;
22
- export type { ExpectStatic } from 'vitest';
36
+ export declare function sanitizeError(err: unknown): string;
37
+ export declare function sanitizeErrorMessage(msg: string): string;
38
+ export declare function sanitizeViteUrl(text: string): string;
39
+ export declare function sanitizeStackTrace(stack: string): string;
40
+ export declare function isVitestNoise(line: string): boolean;