@testspectra/cli 1.1.11-rc.3 → 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/__tests__/run.test.js +27 -0
- 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 +6 -0
- package/dist/commands/run.js +51 -5
- 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 +60 -2
- 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 +15 -1
- package/dist/generator/tsconfig-generator.js +1 -0
- package/dist/generator/type-generator.js +2 -0
- package/dist/index.js +11 -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/run.js
CHANGED
|
@@ -4,9 +4,10 @@ import { ConfigLoader } from '../config/loader.js';
|
|
|
4
4
|
import { RustCoreBridge } from '../runner/bridge.js';
|
|
5
5
|
import { Reporter } from '../runner/reporter.js';
|
|
6
6
|
import { TypeGenerator } from '../types/generator.js';
|
|
7
|
-
import { DemoServer } from '../utils/demo-server.js';
|
|
7
|
+
import { DemoServer, DEFAULT_DEMO_PORT } from '../utils/demo-server.js';
|
|
8
8
|
import { DemoApiServer } from '../utils/api-server.js';
|
|
9
9
|
import { DevServerManager } from '../utils/dev-server-manager.js';
|
|
10
|
+
import { DemoStateManager } from '../utils/demo-state.js';
|
|
10
11
|
function renderSummaryCard(options) {
|
|
11
12
|
const width = 64;
|
|
12
13
|
const borderColor = options.isPassed ? '\x1b[38;2;52;211;153m' : '\x1b[38;2;248;113;113m';
|
|
@@ -69,6 +70,15 @@ function renderSummaryCard(options) {
|
|
|
69
70
|
if (options.networkReportPath) {
|
|
70
71
|
console.log(row(`${dim}Network${reset}`, `${dim}${options.networkReportPath}${reset}`));
|
|
71
72
|
}
|
|
73
|
+
if (options.videoPaths && options.videoPaths.length > 0) {
|
|
74
|
+
const reportsDir = path.dirname(options.reportPath);
|
|
75
|
+
const videoDir = (reportsDir !== '.'
|
|
76
|
+
? path.join(reportsDir, 'videos')
|
|
77
|
+
: path.dirname(options.videoPaths[0]) !== '.'
|
|
78
|
+
? path.dirname(options.videoPaths[0])
|
|
79
|
+
: 'videos').replace(/\\/g, '/');
|
|
80
|
+
console.log(row(`${dim}Videos${reset}`, `${yellow}${options.videoPaths.length} failed recorded${reset} ${dim}(${videoDir})${reset}`));
|
|
81
|
+
}
|
|
72
82
|
console.log(`${bot}\n`);
|
|
73
83
|
}
|
|
74
84
|
const TIMING_PHASE_LABELS = {
|
|
@@ -170,6 +180,13 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
170
180
|
console.warn(`\x1b[33m[TestSpectra] Warning: Target host "${options.host}" is not defined in spectra.config.ts (webConfig.hosts). Falling back to ${config.webConfig.baseUrl}\x1b[0m`);
|
|
171
181
|
}
|
|
172
182
|
}
|
|
183
|
+
else if (!options.demo) {
|
|
184
|
+
const activeDemo = await DemoStateManager.getActiveDemoServer(cwd);
|
|
185
|
+
if (activeDemo) {
|
|
186
|
+
config.webConfig.baseUrl = activeDemo.url;
|
|
187
|
+
console.log(`\x1b[36m[TestSpectra]\x1b[0m 🌐 Connected to active demo server at: \x1b[33m${activeDemo.url}\x1b[0m`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
173
190
|
let demoServer = null;
|
|
174
191
|
let apiServer = null;
|
|
175
192
|
const devServerManager = new DevServerManager();
|
|
@@ -178,11 +195,21 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
178
195
|
...(config.webConfig.devServers || []),
|
|
179
196
|
...(config.webConfig.devServer ? [config.webConfig.devServer] : []),
|
|
180
197
|
];
|
|
181
|
-
|
|
198
|
+
const shouldStartDevServers = options.devServer !== false &&
|
|
199
|
+
options.devServers !== false &&
|
|
200
|
+
!options.skipDevServers &&
|
|
201
|
+
process.env.SPECTRA_SKIP_DEV_SERVERS !== '1' &&
|
|
202
|
+
process.env.SPECTRA_SKIP_DEV_SERVERS !== 'true' &&
|
|
203
|
+
process.env.SPECTRA_SKIP_DEV_SERVER !== '1' &&
|
|
204
|
+
process.env.SPECTRA_SKIP_DEV_SERVER !== 'true' &&
|
|
205
|
+
process.env.TESTSPECTRA_SKIP_DEV_SERVERS !== '1' &&
|
|
206
|
+
process.env.TESTSPECTRA_SKIP_DEV_SERVERS !== 'true';
|
|
207
|
+
if (shouldStartDevServers && configuredDevServers.length > 0 && !options.demo) {
|
|
182
208
|
await devServerManager.startAll(configuredDevServers, options.host, cwd);
|
|
183
209
|
}
|
|
184
210
|
if (options.demo) {
|
|
185
|
-
|
|
211
|
+
// Try preferred static port first (DEFAULT_DEMO_PORT); if occupied, DemoServer automatically falls back to an ephemeral port
|
|
212
|
+
demoServer = new DemoServer(DEFAULT_DEMO_PORT);
|
|
186
213
|
try {
|
|
187
214
|
const { url } = await demoServer.start();
|
|
188
215
|
config.webConfig.baseUrl = url;
|
|
@@ -200,7 +227,7 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
200
227
|
console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo API server: ${err.message}\x1b[0m`);
|
|
201
228
|
}
|
|
202
229
|
}
|
|
203
|
-
if (options.headed) {
|
|
230
|
+
if (options.debug || options.headed) {
|
|
204
231
|
config.webConfig.headless = false;
|
|
205
232
|
}
|
|
206
233
|
else if (options.headless !== undefined) {
|
|
@@ -215,6 +242,13 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
215
242
|
config.iosConfig.stepDelay = delayNum;
|
|
216
243
|
}
|
|
217
244
|
}
|
|
245
|
+
if (options.video !== undefined) {
|
|
246
|
+
config.webConfig.video = options.video;
|
|
247
|
+
if (config.androidConfig)
|
|
248
|
+
config.androidConfig.video = options.video;
|
|
249
|
+
if (config.iosConfig)
|
|
250
|
+
config.iosConfig.video = options.video;
|
|
251
|
+
}
|
|
218
252
|
const platform = options.target || 'web';
|
|
219
253
|
const userConcurrency = options.concurrency ? parseInt(String(options.concurrency), 10) : undefined;
|
|
220
254
|
console.log(`\x1b[36m[TestSpectra]\x1b[0m Starting execution on target \x1b[1m${platform}\x1b[0m...`);
|
|
@@ -258,9 +292,11 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
258
292
|
licenseKey: options.licenseKey,
|
|
259
293
|
outputJsonPath,
|
|
260
294
|
specs: effectiveSpecPaths,
|
|
261
|
-
concurrency: userConcurrency,
|
|
295
|
+
concurrency: options.debug ? 1 : userConcurrency,
|
|
262
296
|
failedOnly: isFailedMode,
|
|
263
297
|
failedTarget,
|
|
298
|
+
debug: options.debug,
|
|
299
|
+
inspectPort: options.inspectPort ? Number(options.inspectPort) : undefined,
|
|
264
300
|
}, reporter);
|
|
265
301
|
const isPassed = result.status.toLowerCase() === 'passed';
|
|
266
302
|
const relReport = path.relative(cwd, outputJsonPath);
|
|
@@ -296,6 +332,7 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
296
332
|
finalDuration = totalMs < 1000 ? `${Math.round(totalMs)}ms` : `${(totalMs / 1000).toFixed(1)}s`;
|
|
297
333
|
}
|
|
298
334
|
const overallPassed = isPassed && totalFailed === 0;
|
|
335
|
+
const videoPaths = [];
|
|
299
336
|
if (fs.existsSync(outputJsonPath)) {
|
|
300
337
|
try {
|
|
301
338
|
const fullReport = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
|
|
@@ -307,6 +344,13 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
307
344
|
s.status === 'failed' ||
|
|
308
345
|
s.tests?.some((t) => t.passed === false || t.status === 'failed')).length;
|
|
309
346
|
passedSuites = fullReport.suites.length - failedSuites;
|
|
347
|
+
for (const s of fullReport.suites) {
|
|
348
|
+
for (const t of s.tests || []) {
|
|
349
|
+
if (t.videoPath) {
|
|
350
|
+
videoPaths.push(t.videoPath);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
310
354
|
}
|
|
311
355
|
if (fullReport.performance) {
|
|
312
356
|
renderSummaryCard({
|
|
@@ -323,6 +367,7 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
323
367
|
carriedPassed,
|
|
324
368
|
reportPath: relReport,
|
|
325
369
|
networkReportPath: relNetworkReport,
|
|
370
|
+
videoPaths,
|
|
326
371
|
});
|
|
327
372
|
renderTimingBreakdown({
|
|
328
373
|
...fullReport.performance,
|
|
@@ -354,6 +399,7 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
354
399
|
carriedPassed,
|
|
355
400
|
reportPath: relReport,
|
|
356
401
|
networkReportPath: relNetworkReport,
|
|
402
|
+
videoPaths,
|
|
357
403
|
});
|
|
358
404
|
if (!overallPassed || totalFailed > 0) {
|
|
359
405
|
process.exitCode = 1;
|
package/dist/commands/test.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export interface TestCommandOptions {
|
|
2
2
|
cwd?: string;
|
|
3
|
-
runner?: 'spectra' | 'vitest';
|
|
4
3
|
headed?: boolean;
|
|
5
4
|
headless?: boolean;
|
|
6
5
|
stepDelay?: string | number;
|
|
@@ -10,20 +9,21 @@ export interface TestCommandOptions {
|
|
|
10
9
|
concurrency?: string | number;
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
|
-
* Guarantees the `.testspectra` solution-tsconfig scaffolding exists before
|
|
12
|
+
* Guarantees the `.testspectra` solution-tsconfig scaffolding exists before the component
|
|
13
|
+
* runner starts.
|
|
14
14
|
*
|
|
15
|
-
* The bundled
|
|
16
|
-
* 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
|
|
17
17
|
* its `references` — including `./.testspectra/tsconfig.json`. That directory is gitignored, so on
|
|
18
18
|
* a clean clone / fresh CI checkout the reference points at a file that does not exist, esbuild
|
|
19
|
-
* throws `ENOENT`,
|
|
20
|
-
*
|
|
21
|
-
* 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.
|
|
22
21
|
*/
|
|
23
22
|
export declare function ensureTestSpectraScaffolding(cwd: string): void;
|
|
24
23
|
/**
|
|
25
|
-
* `spectra test` —
|
|
26
|
-
*
|
|
24
|
+
* `spectra test` — runs React component tests via the native CDP engine
|
|
25
|
+
* (`@testspectra/react` bundle server + Rust `core/orchestrator::component_runner`).
|
|
26
|
+
* Deliberately separate from `spectra run` (E2E via core/orchestrator): see
|
|
27
27
|
* docs/v2/features/component-testing/react/README.md §2 for why these are two independent
|
|
28
28
|
* runners rather than one extended pipeline.
|
|
29
29
|
*/
|
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
|
@@ -11,7 +11,7 @@ export interface DevServerConfig {
|
|
|
11
11
|
*/
|
|
12
12
|
command: string;
|
|
13
13
|
/**
|
|
14
|
-
* Target URL or healthcheck endpoint (e.g. "https://sso.
|
|
14
|
+
* Target URL or healthcheck endpoint (e.g. "https://sso.example.local:5174").
|
|
15
15
|
*/
|
|
16
16
|
url: string;
|
|
17
17
|
/**
|
|
@@ -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
|
*/
|
|
@@ -48,7 +88,7 @@ export interface WebConfig {
|
|
|
48
88
|
baseUrl: string;
|
|
49
89
|
/**
|
|
50
90
|
* Multi-host / multi-app mapping dictionary for micro-frontends and multi-entrypoint monorepos.
|
|
51
|
-
* Maps named aliases to full base URLs (e.g. `{ sso: "https://sso.
|
|
91
|
+
* Maps named aliases to full base URLs (e.g. `{ sso: "https://sso.example.local:5174", fam: "https://fam.example.local:5175" }`).
|
|
52
92
|
* Allows tests to navigate using `Spectra.navigate('sso:/login')` or CLI `--host sso`.
|
|
53
93
|
*/
|
|
54
94
|
hosts?: Record<string, string>;
|
|
@@ -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)) {
|