@testspectra/cli 1.1.11-rc.5 → 1.1.12
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/demo-app/assets/index-DDYS5fLp.css +1 -0
- package/demo-app/assets/index-euCENPMy.js +154 -0
- package/demo-app/index.html +2 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/commands/__tests__/run-e2e.test.js +24 -6
- package/dist/commands/demo.d.ts +1 -0
- package/dist/commands/demo.js +26 -4
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +41 -4
- package/dist/commands/test.d.ts +9 -9
- package/dist/commands/test.js +40 -75
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +109 -0
- package/dist/config/loader.js +31 -4
- package/dist/config/schema.d.ts +58 -0
- package/dist/config/schema.js +4 -1
- package/dist/generator/__tests__/tsconfig-isolation.test.js +21 -0
- package/dist/generator/__tests__/type-generator.test.js +14 -0
- package/dist/generator/tsconfig-generator.js +1 -0
- package/dist/generator/type-generator.js +2 -0
- package/dist/index.js +8 -4
- package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
- package/dist/lifecycle/hook-discovery.d.ts +24 -0
- package/dist/lifecycle/hook-discovery.js +60 -0
- package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
- package/dist/lifecycle/hook-dispatcher.js +190 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/parallel-grouping.d.ts +27 -0
- package/dist/lifecycle/parallel-grouping.js +151 -0
- package/dist/reporter/semantic-formatter.js +2 -0
- package/dist/reporter/types.d.ts +1 -1
- package/dist/runner/bridge.d.ts +2 -0
- package/dist/runner/bridge.js +4 -1
- package/dist/runner/reporter.d.ts +5 -43
- package/dist/runner/reporter.js +46 -179
- package/dist/utils/__tests__/demo-state.test.d.ts +1 -0
- package/dist/utils/__tests__/demo-state.test.js +96 -0
- package/dist/utils/api-server.d.ts +1 -1
- package/dist/utils/api-server.js +7 -3
- package/dist/utils/demo-server.d.ts +2 -0
- package/dist/utils/demo-server.js +18 -5
- package/dist/utils/demo-state.d.ts +40 -0
- package/dist/utils/demo-state.js +157 -0
- package/package.json +9 -8
- package/templates/default/fixtures/sampleDocument.pdf +14 -0
- package/templates/default/fixtures/sampleImage.png +0 -0
- package/templates/default/package.json +2 -2
- package/templates/default/page-objects/MatchersPage/common.ts +41 -0
- package/templates/default/page-objects/MatchersPage/mobile.ts +8 -3
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +6 -0
- package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +3 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0003-file-upload/web.test.ts +11 -0
- package/templates/default/spectra.config.ts +5 -1
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +7063 -1012
- package/templates/nx/.nx/workspace-data/file-map.json +181 -169
- package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/parsed-lock-file.json +81 -130
- package/templates/nx/.nx/workspace-data/project-graph.json +83 -138
- package/templates/nx/package.json +2 -2
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0003-file-upload/web.test.ts +11 -0
- package/templates/nx/shared/testing/fixtures/sampleDocument.pdf +14 -0
- package/templates/nx/shared/testing/fixtures/sampleImage.png +0 -0
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +68 -26
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +39 -17
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
- package/templates/nx/spectra.config.ts +3 -3
- package/templates/react-component/fixtures/sampleDocument.pdf +14 -0
- package/templates/react-component/fixtures/sampleImage.png +0 -0
- package/templates/react-component/package.json +3 -3
- package/templates/react-component/page-objects/FileUploadComponent/web.ts +15 -0
- package/templates/react-component/specs/FileUploadComponent/TC-0001-upload-fixtures/web.test.tsx +18 -0
- package/templates/react-component/specs/UserListComponent/TC-0001-intercepted-user-list/spec.md +20 -0
- package/templates/react-component/specs/UserListComponent/TC-0001-intercepted-user-list/web.test.tsx +15 -0
- package/templates/react-component/specs/UserListComponent/suite.md +12 -0
- package/templates/react-component/src/components/FileUpload/FileUpload.tsx +44 -0
- package/templates/react-component/src/components/UserList/UserList.tsx +48 -0
- package/bin/.testspectra-runner.hash +0 -1
- package/demo-app/assets/index-BhlW-eXG.css +0 -1
- package/demo-app/assets/index-DOtRypCa.js +0 -154
- package/dist/commands/__tests__/doctor-scope.test.js +0 -24
- /package/dist/{commands/__tests__/doctor-scope.test.d.ts → lifecycle/__tests__/lifecycle-engine.test.d.ts} +0 -0
package/dist/commands/test.js
CHANGED
|
@@ -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
|
|
12
|
+
* Guarantees the `.testspectra` solution-tsconfig scaffolding exists before the component
|
|
13
|
+
* runner starts.
|
|
13
14
|
*
|
|
14
|
-
* The bundled
|
|
15
|
-
* When it does,
|
|
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`,
|
|
19
|
-
*
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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` —
|
|
50
|
-
*
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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,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
|
+
}
|
package/dist/config/loader.js
CHANGED
|
@@ -41,12 +41,39 @@ export class ConfigLoader {
|
|
|
41
41
|
}
|
|
42
42
|
else if (configPath.endsWith('.ts') || configPath.endsWith('.js')) {
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -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.
|
package/dist/config/schema.js
CHANGED
|
@@ -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
|
});
|
|
@@ -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(
|
|
58
|
-
.option('-p, --port <port>',
|
|
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
|
|
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)')
|