@testspectra/cli 1.1.11-rc.5 → 1.1.13

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.
Files changed (92) hide show
  1. package/demo-app/assets/index-DDYS5fLp.css +1 -0
  2. package/demo-app/assets/index-euCENPMy.js +154 -0
  3. package/demo-app/index.html +2 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/commands/__tests__/run-e2e.test.js +24 -6
  6. package/dist/commands/demo.d.ts +1 -0
  7. package/dist/commands/demo.js +26 -4
  8. package/dist/commands/doctor.js +23 -0
  9. package/dist/commands/run.d.ts +3 -0
  10. package/dist/commands/run.js +41 -4
  11. package/dist/commands/test.d.ts +9 -9
  12. package/dist/commands/test.js +40 -75
  13. package/dist/commands/update.d.ts +3 -0
  14. package/dist/commands/update.js +109 -0
  15. package/dist/config/loader.js +31 -4
  16. package/dist/config/schema.d.ts +58 -0
  17. package/dist/config/schema.js +4 -1
  18. package/dist/generator/__tests__/tsconfig-isolation.test.js +21 -0
  19. package/dist/generator/__tests__/type-generator.test.js +14 -0
  20. package/dist/generator/tsconfig-generator.js +1 -0
  21. package/dist/generator/type-generator.js +2 -0
  22. package/dist/index.js +8 -4
  23. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  24. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  25. package/dist/lifecycle/hook-discovery.js +60 -0
  26. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  27. package/dist/lifecycle/hook-dispatcher.js +190 -0
  28. package/dist/lifecycle/index.d.ts +3 -0
  29. package/dist/lifecycle/index.js +3 -0
  30. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  31. package/dist/lifecycle/parallel-grouping.js +151 -0
  32. package/dist/reporter/semantic-formatter.js +2 -0
  33. package/dist/reporter/types.d.ts +1 -1
  34. package/dist/runner/bridge.d.ts +2 -0
  35. package/dist/runner/bridge.js +4 -1
  36. package/dist/runner/reporter.d.ts +5 -43
  37. package/dist/runner/reporter.js +46 -179
  38. package/dist/utils/__tests__/demo-state.test.d.ts +1 -0
  39. package/dist/utils/__tests__/demo-state.test.js +96 -0
  40. package/dist/utils/api-server.d.ts +1 -1
  41. package/dist/utils/api-server.js +7 -3
  42. package/dist/utils/demo-server.d.ts +2 -0
  43. package/dist/utils/demo-server.js +18 -5
  44. package/dist/utils/demo-state.d.ts +40 -0
  45. package/dist/utils/demo-state.js +157 -0
  46. package/package.json +8 -7
  47. package/templates/default/fixtures/sampleDocument.pdf +14 -0
  48. package/templates/default/fixtures/sampleImage.png +0 -0
  49. package/templates/default/package.json +2 -2
  50. package/templates/default/page-objects/MatchersPage/common.ts +41 -0
  51. package/templates/default/page-objects/MatchersPage/mobile.ts +8 -3
  52. package/templates/default/page-objects/PlaygroundPage/mobile.ts +6 -0
  53. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
  54. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +3 -0
  55. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -0
  56. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  57. package/templates/default/specs/Playground/TC-0003-file-upload/web.test.ts +11 -0
  58. package/templates/default/spectra.config.ts +5 -1
  59. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  60. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  61. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  62. package/templates/nx/.nx/workspace-data/d/daemon.log +7104 -1012
  63. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  64. package/templates/nx/.nx/workspace-data/file-map.json +116 -104
  65. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  66. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  67. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +81 -130
  68. package/templates/nx/.nx/workspace-data/project-graph.json +83 -138
  69. package/templates/nx/package.json +2 -2
  70. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  71. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0003-file-upload/web.test.ts +11 -0
  72. package/templates/nx/shared/testing/fixtures/sampleDocument.pdf +14 -0
  73. package/templates/nx/shared/testing/fixtures/sampleImage.png +0 -0
  74. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +68 -26
  75. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +39 -17
  76. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
  77. package/templates/nx/spectra.config.ts +3 -3
  78. package/templates/react-component/fixtures/sampleDocument.pdf +14 -0
  79. package/templates/react-component/fixtures/sampleImage.png +0 -0
  80. package/templates/react-component/package.json +3 -3
  81. package/templates/react-component/page-objects/FileUploadComponent/web.ts +15 -0
  82. package/templates/react-component/specs/FileUploadComponent/TC-0001-upload-fixtures/web.test.tsx +18 -0
  83. package/templates/react-component/specs/UserListComponent/TC-0001-intercepted-user-list/spec.md +20 -0
  84. package/templates/react-component/specs/UserListComponent/TC-0001-intercepted-user-list/web.test.tsx +15 -0
  85. package/templates/react-component/specs/UserListComponent/suite.md +12 -0
  86. package/templates/react-component/src/components/FileUpload/FileUpload.tsx +44 -0
  87. package/templates/react-component/src/components/UserList/UserList.tsx +48 -0
  88. package/bin/.testspectra-runner.hash +0 -1
  89. package/demo-app/assets/index-BhlW-eXG.css +0 -1
  90. package/demo-app/assets/index-DOtRypCa.js +0 -154
  91. package/dist/commands/__tests__/doctor-scope.test.js +0 -24
  92. /package/dist/{commands/__tests__/doctor-scope.test.d.ts → lifecycle/__tests__/lifecycle-engine.test.d.ts} +0 -0
@@ -9,15 +9,15 @@ import { RustCoreBridge } from '../runner/bridge.js';
9
9
  import { TypeGenerator } from '../generator/type-generator.js';
10
10
  import { runSystemChecks } from './doctor.js';
11
11
  /**
12
- * Guarantees the `.testspectra` solution-tsconfig scaffolding exists before Vitest starts.
12
+ * Guarantees the `.testspectra` solution-tsconfig scaffolding exists before the component
13
+ * runner starts.
13
14
  *
14
- * The bundled Vitest browser runner transpiles raw TypeScript specs/page-objects Just-In-Time.
15
- * When it does, `vite:esbuild` walks the nearest `tsconfig.json` and tries to read every entry in
15
+ * The bundled component runner transpiles raw TypeScript specs/page-objects Just-In-Time.
16
+ * When it does, esbuild walks the nearest `tsconfig.json` and tries to read every entry in
16
17
  * its `references` — including `./.testspectra/tsconfig.json`. That directory is gitignored, so on
17
18
  * a clean clone / fresh CI checkout the reference points at a file that does not exist, esbuild
18
- * throws `ENOENT`, Vite answers HTTP 500, and every suite dies with "Failed to fetch dynamically
19
- * imported module" before a single assertion runs. Regenerating the scaffolding up front (same
20
- * output as `spectra sync-types`) keeps the reference resolvable.
19
+ * throws `ENOENT`, and every suite dies before a single assertion runs. Regenerating the
20
+ * scaffolding up front (same output as `spectra sync-types`) keeps the reference resolvable.
21
21
  */
22
22
  export function ensureTestSpectraScaffolding(cwd) {
23
23
  const masterConfigPath = path.join(cwd, '.testspectra', 'tsconfig.json');
@@ -28,17 +28,16 @@ export function ensureTestSpectraScaffolding(cwd) {
28
28
  /**
29
29
  * Finds the Chrome-for-Testing binary that `spectra doctor` already installs/manages for the
30
30
  * E2E CDP driver (`core/driver-cdp`) — same install, same binary. `@testspectra/react` must not
31
- * contain any browser-discovery logic of its own (see the package-level comment in
32
- * tools/react/vitest.config.ts): this is the one place that resolves the path, passed down to
33
- * the bundled Vitest/Playwright runner as a plain argument (env var), so component testing never
34
- * requires a separate `playwright install` download alongside TestSpectra's own browser.
31
+ * contain any browser-discovery logic of its own: this is the one place that resolves the path,
32
+ * passed down to the native CDP component runner as a plain argument, so component testing never
33
+ * requires a separate browser download alongside TestSpectra's own browser.
35
34
  */
36
35
  async function resolveBrowserExecutable(cwd) {
37
36
  try {
38
37
  // `--only chrome` runs exactly one check — no adb/bun/git spawns, no Matchers lookup — since
39
38
  // this call exists solely to locate the managed browser binary.
40
39
  const { dependencies } = await runSystemChecks(cwd, { only: 'chrome' });
41
- const chrome = dependencies.find((d) => d.category === 'web' && d.name.includes('Chrome'));
40
+ const chrome = dependencies.find((d) => d.category === 'web' && (d.name.includes('Chrome') || d.name.includes('Chromium')));
42
41
  return chrome?.installed && chrome.location ? chrome.location : null;
43
42
  }
44
43
  catch {
@@ -46,8 +45,9 @@ async function resolveBrowserExecutable(cwd) {
46
45
  }
47
46
  }
48
47
  /**
49
- * `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
50
- * component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
48
+ * `spectra test` — runs React component tests via the native CDP engine
49
+ * (`@testspectra/react` bundle server + Rust `core/orchestrator::component_runner`).
50
+ * Deliberately separate from `spectra run` (E2E via core/orchestrator): see
51
51
  * docs/v2/features/component-testing/react/README.md §2 for why these are two independent
52
52
  * runners rather than one extended pipeline.
53
53
  */
@@ -116,71 +116,36 @@ export async function testCommand(options = {}) {
116
116
  // / setup.ts), so the same Reporter class parses them with zero changes on either side.
117
117
  const reporter = new Reporter();
118
118
  const runStartedAt = Date.now();
119
+ logPhase('Starting component test runner (Spectra Native CDP Engine)…');
120
+ const bundleServer = await reactPkg.startBundleServer({
121
+ testRoot: cwd,
122
+ stepDelayMs: stepDelayMs ?? 0,
123
+ });
119
124
  let ok = false;
120
- if (options.runner === 'vitest') {
121
- logPhase('Starting component test runner (Vitest Browser Mode)…');
122
- ok = await reactPkg.runComponentTests({
123
- runner: 'vitest',
124
- cwd,
125
- headless,
126
- stepDelayMs,
127
- spec: options.spec,
128
- outputFile,
129
- concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
130
- browserExecutablePath,
131
- onLine: (line) => {
132
- const cleanLine = line.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '').trim();
133
- if (!cleanLine)
134
- return;
135
- if (cleanLine.startsWith('[TESTSPECTRA_')) {
136
- reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'INFO', message: cleanLine });
137
- if (!reporter.isInteractiveMode()) {
138
- console.log(cleanLine);
139
- }
140
- return;
141
- }
142
- if (reactPkg.isVitestNoise?.(cleanLine)) {
143
- return;
144
- }
145
- const sanitized = reactPkg.sanitizeErrorMessage?.(cleanLine) ?? cleanLine;
146
- reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'ERROR', message: sanitized });
147
- if (!reporter.isInteractiveMode()) {
148
- console.error(chalk.red(sanitized));
149
- }
125
+ try {
126
+ const home = process.env.HOME || process.env.USERPROFILE || '';
127
+ const globalAppDataPath = path.join(home, '.testspectra');
128
+ const appDataPath = globalAppDataPath;
129
+ const runResult = await RustCoreBridge.run({
130
+ baseDir: cwd,
131
+ appDataPath,
132
+ platform: 'component',
133
+ config: {
134
+ bundleServerUrl: bundleServer.url,
135
+ webConfig: {
136
+ headless,
137
+ stepDelay: stepDelayMs,
138
+ browserExecutablePath,
139
+ },
150
140
  },
151
- });
141
+ specs: options.spec ? [options.spec] : undefined,
142
+ concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
143
+ outputJsonPath: outputFile,
144
+ }, reporter);
145
+ ok = runResult.status.toLowerCase() === 'passed';
152
146
  }
153
- else {
154
- logPhase('Starting component test runner (Spectra Native CDP Engine)…');
155
- const bundleServer = await reactPkg.startBundleServer({
156
- testRoot: cwd,
157
- stepDelayMs: stepDelayMs ?? 0,
158
- });
159
- try {
160
- const home = process.env.HOME || process.env.USERPROFILE || '';
161
- const globalAppDataPath = path.join(home, '.testspectra');
162
- const appDataPath = globalAppDataPath;
163
- const runResult = await RustCoreBridge.run({
164
- baseDir: cwd,
165
- appDataPath,
166
- platform: 'component',
167
- config: {
168
- bundleServerUrl: bundleServer.url,
169
- webConfig: {
170
- headless,
171
- stepDelay: stepDelayMs,
172
- browserExecutablePath,
173
- },
174
- },
175
- specs: options.spec ? [options.spec] : undefined,
176
- concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
177
- outputJsonPath: outputFile,
178
- }, reporter);
179
- ok = runResult.status.toLowerCase() === 'passed';
180
- }
181
- finally {
182
- bundleServer.stop();
183
- }
147
+ finally {
148
+ bundleServer.stop();
184
149
  }
185
150
  const durationMs = Date.now() - runStartedAt;
186
151
  const result = reporter.generateResult(ok ? 'passed' : 'failed', `${(durationMs / 1000).toFixed(1)}s`);
@@ -0,0 +1,3 @@
1
+ export declare function updateCommand(options?: {
2
+ check?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import { execSync, execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const PACKAGES = ['@testspectra/cli', '@testspectra/matchers'];
8
+ function getLatestVersion(pkg) {
9
+ try {
10
+ const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
11
+ stdio: ['ignore', 'pipe', 'ignore'],
12
+ timeout: 10000,
13
+ })
14
+ .toString()
15
+ .trim()
16
+ .replace(/"/g, '');
17
+ return out || null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function getCurrentVersion(pkg, cwd) {
24
+ // Walk up to find the installed version
25
+ let cur = path.resolve(cwd);
26
+ while (cur && cur !== path.dirname(cur)) {
27
+ const pkgJson = path.join(cur, 'node_modules', pkg, 'package.json');
28
+ if (fs.existsSync(pkgJson)) {
29
+ try {
30
+ const p = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
31
+ return p.version ?? null;
32
+ }
33
+ catch { }
34
+ }
35
+ cur = path.dirname(cur);
36
+ }
37
+ return null;
38
+ }
39
+ function detectPackageManager(cwd) {
40
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
41
+ return 'pnpm';
42
+ if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
43
+ return 'bun';
44
+ return 'npm';
45
+ }
46
+ function buildInstallCmd(pm, pkgSpecs) {
47
+ const list = pkgSpecs.join(' ');
48
+ switch (pm) {
49
+ case 'pnpm':
50
+ return `pnpm add -D ${list}`;
51
+ case 'bun':
52
+ return `bun add -d ${list}`;
53
+ case 'npm':
54
+ default:
55
+ return `npm install -D ${list}`;
56
+ }
57
+ }
58
+ export async function updateCommand(options = {}) {
59
+ const cwd = process.cwd();
60
+ const pm = detectPackageManager(cwd);
61
+ console.log('\x1b[36m┌ 🔄 TestSpectra Self-Update\x1b[0m');
62
+ console.log('\x1b[36m│\x1b[0m');
63
+ console.log(`\x1b[36m│\x1b[0m Package Manager: \x1b[33m${pm}\x1b[0m`);
64
+ console.log('\x1b[36m│\x1b[0m');
65
+ console.log('\x1b[36m│\x1b[0m Fetching latest versions from npm registry...');
66
+ const updates = [];
67
+ for (const pkg of PACKAGES) {
68
+ const current = getCurrentVersion(pkg, cwd);
69
+ const latest = getLatestVersion(pkg);
70
+ const needsUpdate = !!latest && latest !== current;
71
+ updates.push({ pkg, current, latest, needsUpdate });
72
+ const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
73
+ const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[31m(unavailable)\x1b[0m';
74
+ const arrow = needsUpdate ? ` → ${latestStr}` : '';
75
+ const status = !latest ? '\x1b[31m✗\x1b[0m' : needsUpdate ? '\x1b[33m↑\x1b[0m' : '\x1b[32m✓\x1b[0m';
76
+ console.log(`\x1b[36m│\x1b[0m ${status} ${pkg.padEnd(28)} ${currentStr}${arrow}`);
77
+ }
78
+ const toUpdate = updates.filter((u) => u.needsUpdate && u.latest);
79
+ console.log('\x1b[36m│\x1b[0m');
80
+ if (toUpdate.length === 0) {
81
+ console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
82
+ console.log('\x1b[32m✓ All TestSpectra packages are already up-to-date!\x1b[0m\n');
83
+ return;
84
+ }
85
+ if (options.check) {
86
+ console.log(`\x1b[36m│\x1b[0m \x1b[33m${toUpdate.length} update(s) available. Run \x1b[36mspectra update\x1b[33m to install.\x1b[0m`);
87
+ console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m\n');
88
+ return;
89
+ }
90
+ const pkgSpecs = toUpdate.map((u) => `${u.pkg}@${u.latest}`);
91
+ const installCmd = buildInstallCmd(pm, pkgSpecs);
92
+ console.log(`\x1b[36m│\x1b[0m Running: \x1b[90m${installCmd}\x1b[0m`);
93
+ console.log('\x1b[36m│\x1b[0m');
94
+ try {
95
+ execSync(installCmd, { cwd, stdio: 'inherit' });
96
+ console.log('\x1b[36m│\x1b[0m');
97
+ console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
98
+ console.log('\x1b[32m🎉 TestSpectra updated successfully!\x1b[0m');
99
+ for (const u of toUpdate) {
100
+ console.log(` \x1b[32m✓\x1b[0m ${u.pkg}: v${u.current ?? '?'} → v${u.latest}`);
101
+ }
102
+ console.log('');
103
+ }
104
+ catch (err) {
105
+ console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
106
+ console.error(`\x1b[31m✗ Update failed: ${err.message}\x1b[0m\n`);
107
+ process.exit(1);
108
+ }
109
+ }
@@ -41,12 +41,39 @@ export class ConfigLoader {
41
41
  }
42
42
  else if (configPath.endsWith('.ts') || configPath.endsWith('.js')) {
43
43
  try {
44
- const fileUrl = new URL(`file://${path.resolve(configPath)}`).href;
45
- const mod = await import(fileUrl);
46
- loaded = mod.default || mod.config || mod;
44
+ const raw = fs.readFileSync(configPath, 'utf-8');
45
+ const ts = (await import('typescript')).default || (await import('typescript'));
46
+ const transpiled = ts.transpileModule(raw, {
47
+ compilerOptions: {
48
+ module: ts.ModuleKind.CommonJS,
49
+ target: ts.ScriptTarget.ES2022,
50
+ },
51
+ });
52
+ const mod = { exports: {} };
53
+ const mockRequire = (id) => {
54
+ if (id === '@testspectra/cli' || id.endsWith('defineConfig')) {
55
+ return { defineConfig: (c) => c };
56
+ }
57
+ try {
58
+ return require(id);
59
+ }
60
+ catch {
61
+ return {};
62
+ }
63
+ };
64
+ const fn = new Function('exports', 'require', 'module', '__filename', '__dirname', transpiled.outputText);
65
+ fn(mod.exports, mockRequire, mod, configPath, path.dirname(configPath));
66
+ loaded = mod.exports.default || mod.exports.config || mod.exports;
47
67
  }
48
68
  catch (e) {
49
- console.warn(`[TestSpectra] Warning: Failed to import ${configPath}:`, e);
69
+ try {
70
+ const fileUrl = new URL(`file://${path.resolve(configPath)}`).href;
71
+ const mod = await import(fileUrl);
72
+ loaded = mod.default || mod.config || mod;
73
+ }
74
+ catch {
75
+ console.warn(`[TestSpectra] Warning: Failed to import ${configPath}:`, e);
76
+ }
50
77
  }
51
78
  }
52
79
  }
@@ -37,6 +37,46 @@ export interface DevServerConfig {
37
37
  */
38
38
  env?: Record<string, string>;
39
39
  }
40
+ /**
41
+ * Configuration options for failure-only video recording.
42
+ */
43
+ export interface VideoConfig {
44
+ /**
45
+ * Video container format ('webm' | 'mp4').
46
+ * @default "webm"
47
+ */
48
+ format?: 'webm' | 'mp4';
49
+ /**
50
+ * Target frame rate in frames per second (15 | 30 | 60).
51
+ * @default 30
52
+ */
53
+ fps?: 15 | 30 | 60 | number;
54
+ /**
55
+ * Compression quality (1 - 100).
56
+ * @default 80
57
+ */
58
+ quality?: number;
59
+ /**
60
+ * Maximum video width in pixels.
61
+ * @default 1280
62
+ */
63
+ maxWidth?: number;
64
+ /**
65
+ * Maximum video height in pixels.
66
+ * @default 720
67
+ */
68
+ maxHeight?: number;
69
+ /**
70
+ * Bitrate in Mbps (Android screen recording).
71
+ * @default 4
72
+ */
73
+ bitRateMbps?: number;
74
+ /**
75
+ * Maximum recording time limit per test case in seconds (Android screen recording).
76
+ * @default 180
77
+ */
78
+ timeLimitSeconds?: number;
79
+ }
40
80
  /**
41
81
  * Configuration for web automation and browser test runners.
42
82
  */
@@ -90,6 +130,14 @@ export interface WebConfig {
90
130
  * @default "testcase"
91
131
  */
92
132
  parallelizationMode: 'testcase' | 'suite';
133
+ /**
134
+ * Failure-only video recording configuration.
135
+ * - `true`: Records screencast and persists video only when a test case fails (default).
136
+ * - `false`: Disables video recording completely for maximum execution speed.
137
+ * - `VideoConfig`: Detailed video format and quality settings.
138
+ * @default true
139
+ */
140
+ video?: boolean | VideoConfig;
93
141
  }
94
142
  /**
95
143
  * Target Android device specification for multi-device parallel execution.
@@ -203,6 +251,11 @@ export interface AndroidConfig {
203
251
  * @default 0
204
252
  */
205
253
  stepDelay?: number;
254
+ /**
255
+ * Failure-only video recording configuration.
256
+ * @default true
257
+ */
258
+ video?: boolean | VideoConfig;
206
259
  }
207
260
  /**
208
261
  * Configuration for iOS mobile test automation with Appium & XCUITest.
@@ -273,6 +326,11 @@ export interface IosConfig {
273
326
  * @default "suite"
274
327
  */
275
328
  parallelizationMode: 'testcase' | 'suite';
329
+ /**
330
+ * Failure-only video recording configuration.
331
+ * @default true
332
+ */
333
+ video?: boolean | VideoConfig;
276
334
  }
277
335
  /**
278
336
  * Specific domain filter for real-time network interception and recording.
@@ -28,6 +28,7 @@ export const DEFAULT_CONFIG_DATA = {
28
28
  headless: true,
29
29
  implicitWait: '5000',
30
30
  parallelizationMode: 'testcase',
31
+ video: true,
31
32
  },
32
33
  androidConfig: {
33
34
  driverServer: 'http://127.0.0.1:4800',
@@ -36,6 +37,7 @@ export const DEFAULT_CONFIG_DATA = {
36
37
  noReset: false,
37
38
  implicitWait: 10000,
38
39
  parallelizationMode: 'suite',
40
+ video: true,
39
41
  devices: [
40
42
  {
41
43
  id: 'emulator-5554',
@@ -53,6 +55,7 @@ export const DEFAULT_CONFIG_DATA = {
53
55
  noReset: false,
54
56
  implicitWait: 10000,
55
57
  parallelizationMode: 'suite',
58
+ video: true,
56
59
  devices: [
57
60
  {
58
61
  id: 'iphone-15-pro',
@@ -68,6 +71,6 @@ export const DEFAULT_CONFIG_DATA = {
68
71
  fastResponseTime: '200',
69
72
  normalResponseTime: '1000',
70
73
  monitoredDomains: [],
71
- environmentVariables: {},
74
+ environmentVariables: { DEMO_API_MODE: 'sandbox' },
72
75
  },
73
76
  };
@@ -262,6 +262,27 @@ describe('Platform TSConfigs & Type Isolation (docs/v2/cli/tsconfig-platform-iso
262
262
  expect(threw).toBe(true);
263
263
  expect(errorMessage).toContain("Cannot find name 'AuthLoginPage'");
264
264
  });
265
+ it('feature-scoped configs enable JSX so .tsx component tests type-check (regression: getScopedConfig omitted jsx)', () => {
266
+ const featureDir = path.join(tmpDir, 'packages', 'catalog', 'e2e');
267
+ fs.mkdirSync(path.join(featureDir, 'page-objects', 'RowComponent'), { recursive: true });
268
+ fs.writeFileSync(path.join(featureDir, 'page-objects', 'RowComponent', 'web.ts'), 'export default class RowComponent { static title = "row"; }');
269
+ fs.mkdirSync(path.join(featureDir, 'specs'), { recursive: true });
270
+ TypeGenerator.generateAll(tmpDir);
271
+ const scopedWebConfig = JSON.parse(fs.readFileSync(path.join(tmpDir, '.testspectra', 'tsconfig', 'features', 'catalog', 'web.json'), 'utf-8'));
272
+ expect(scopedWebConfig.compilerOptions.jsx).toBe('react-jsx');
273
+ });
274
+ it('E2E: compiles a .tsx component test inside a feature scope via tsc -b with 0 errors', () => {
275
+ linkNodeModules(tmpDir);
276
+ const featureDir = path.join(tmpDir, 'packages', 'catalog', 'e2e');
277
+ fs.mkdirSync(path.join(featureDir, 'page-objects', 'RowComponent'), { recursive: true });
278
+ fs.writeFileSync(path.join(featureDir, 'page-objects', 'RowComponent', 'web.ts'), 'export default class RowComponent { static title = "row"; }');
279
+ fs.mkdirSync(path.join(featureDir, 'specs', 'Row', 'TC-0001'), { recursive: true });
280
+ fs.writeFileSync(path.join(featureDir, 'specs', 'Row', 'TC-0001', 'web.test.tsx'), 'const el = <div>{RowComponent.title}</div>;\n');
281
+ TypeGenerator.generateAll(tmpDir);
282
+ expect(() => {
283
+ execSync(`npx tsc -b ${tmpDir}`, { stdio: 'pipe' });
284
+ }).not.toThrow();
285
+ });
265
286
  it('E2E: compiles the full Nx monorepo template via tsc -b with 0 errors', async () => {
266
287
  let root = process.cwd();
267
288
  while (root && root !== path.dirname(root)) {
@@ -421,4 +421,18 @@ async function verifyZeroImport() {
421
421
  // Verify packages/features/auth is NOT in scopes
422
422
  expect(scopes.some((s) => s.dir === path.resolve(featureLibDir))).toBe(false);
423
423
  });
424
+ it('generates ambient types for non-JSON fixtures including images and PDF files', () => {
425
+ const fixDir = path.join(tmpDir, 'fixtures');
426
+ fs.mkdirSync(fixDir, { recursive: true });
427
+ fs.writeFileSync(path.join(fixDir, 'sampleImage.png'), 'fake-png-bytes');
428
+ fs.writeFileSync(path.join(fixDir, 'sampleDocument.pdf'), 'fake-pdf-bytes');
429
+ fs.writeFileSync(path.join(fixDir, 'dataset.csv'), 'id,name\n1,Alice');
430
+ fs.writeFileSync(path.join(fixDir, 'users.json'), JSON.stringify({ count: 1 }));
431
+ TypeGenerator.generateAll(tmpDir);
432
+ const fixturesDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'fixtures.d.ts'), 'utf-8');
433
+ expect(fixturesDts).toContain('readonly sampleImage: string;');
434
+ expect(fixturesDts).toContain('readonly sampleDocument: string;');
435
+ expect(fixturesDts).toContain('readonly dataset: string;');
436
+ expect(fixturesDts).toContain('readonly users: typeof import(');
437
+ });
424
438
  });
@@ -260,6 +260,7 @@ export class TsConfigGenerator {
260
260
  target: 'ES2022',
261
261
  module: 'NodeNext',
262
262
  moduleResolution: 'NodeNext',
263
+ jsx: 'react-jsx',
263
264
  composite: true,
264
265
  noEmit: true,
265
266
  skipLibCheck: true,
@@ -579,6 +579,8 @@ export class TypeGenerator {
579
579
  scanFixtureDir(path.join(dir, entry.name));
580
580
  }
581
581
  else if (entry.isFile()) {
582
+ if (entry.name.endsWith('.d.ts'))
583
+ continue;
582
584
  const parsed = path.parse(entry.name);
583
585
  const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, '_');
584
586
  if (declaredFixtures.has(varName))
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { addCommand } from './commands/add.js';
3
3
  import { demoCommand } from './commands/demo.js';
4
+ import { DEFAULT_DEMO_PORT } from './utils/demo-server.js';
4
5
  import { devicesCommand } from './commands/devices.js';
5
6
  import { doctorCommand } from './commands/doctor.js';
6
7
  import { initCommand } from './commands/init.js';
@@ -54,8 +55,8 @@ Examples:
54
55
  .action(installCommand);
55
56
  program
56
57
  .command('demo')
57
- .description('Launch the built-in TestSpectra Demo Web App server on localhost:5173')
58
- .option('-p, --port <port>', 'Port to bind server (default: 5173)')
58
+ .description(`Launch the built-in TestSpectra Demo Web App server on localhost:${DEFAULT_DEMO_PORT}`)
59
+ .option('-p, --port <port>', `Port to bind server (default: ${DEFAULT_DEMO_PORT})`)
59
60
  .action(demoCommand);
60
61
  program
61
62
  .command('watch')
@@ -187,15 +188,18 @@ Examples:
187
188
  .option('--skip-dev-servers', 'Skip auto-starting local development servers')
188
189
  .option('--step-delay <ms>', 'Delay between consecutive actions and commands in milliseconds (slow motion)')
189
190
  .option('--delay <ms>', 'Alias for --step-delay')
191
+ .option('--video', 'Enable failure video recording (default: true)')
192
+ .option('--no-video', 'Disable video recording completely')
190
193
  .option('--failed [targets]', 'Re-run failed tests from previous run (accepts comma-separated test IDs or spec paths)')
191
194
  .option('-c, --concurrency <number>', 'Maximum parallel worker concurrency (auto-detected if omitted)')
195
+ .option('--debug', 'Run in debug mode (headed browser, single worker, inspector attached for breakpoints)')
196
+ .option('--inspect-port <port>', 'Bun inspector port for debug mode (default: 9229)')
192
197
  .option('-w, --workdir <dir>', 'Custom app data / temp workdir')
193
198
  .option('-o, --output <path>', 'Custom test report JSON output path')
194
199
  .action(runCommand);
195
200
  program
196
201
  .command('test')
197
- .description('Run React component tests via @testspectra/react')
198
- .option('-r, --runner <engine>', 'Component test runner engine: spectra (default, native CDP) or vitest (Playwright browser mode)', 'spectra')
202
+ .description('Run React component tests via the native CDP engine')
199
203
  .option('-w, --cwd <dir>', 'Custom workspace directory')
200
204
  .option('-s, --spec <path>', 'Spec file path (or substring filter) to execute')
201
205
  .option('--headless', 'Run in headless mode (default)')