@testspectra/cli 1.1.8-rc.9 → 1.1.10
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/README.md +11 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/doctor-filter.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor-filter.test.js +37 -0
- package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
- package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
- package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
- package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +95 -0
- package/dist/commands/add.js +3 -3
- package/dist/commands/devices.js +1 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.js +17 -0
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +17 -3
- package/dist/commands/init.js +5 -159
- package/dist/commands/install.js +5 -0
- package/dist/commands/license.js +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +22 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.js +115 -21
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +197 -0
- package/dist/config/schema.d.ts +53 -0
- package/dist/config/schema.js +2 -0
- package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
- package/dist/generator/architecture-doc-generator.d.ts +14 -0
- package/dist/generator/architecture-doc-generator.js +80 -0
- package/dist/generator/index.d.ts +1 -0
- package/dist/generator/index.js +1 -0
- package/dist/generator/tsconfig-generator.js +1 -1
- package/dist/index.js +59 -16
- package/dist/linter/discovery.js +2 -2
- package/dist/linter/schemas/spec.schema.d.ts +6 -6
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +58 -16
- package/dist/runner/reporter.d.ts +1 -0
- package/dist/runner/reporter.js +12 -6
- package/dist/utils/__tests__/dev-server-manager.test.d.ts +1 -0
- package/dist/utils/__tests__/dev-server-manager.test.js +72 -0
- package/dist/utils/background-update-check.d.ts +1 -0
- package/dist/utils/background-update-check.js +43 -0
- package/dist/utils/dependency-updates.d.ts +15 -0
- package/dist/utils/dependency-updates.js +107 -0
- package/dist/utils/dev-server-manager.d.ts +40 -0
- package/dist/utils/dev-server-manager.js +257 -0
- package/dist/utils/update-notifier.d.ts +2 -0
- package/dist/utils/update-notifier.js +67 -0
- package/package.json +10 -9
- package/templates/default/package.json +2 -2
- package/templates/docs/ARCHITECTURE.default.md +26 -0
- package/templates/docs/ARCHITECTURE.nx.md +147 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +745 -0
- package/templates/nx/.nx/workspace-data/d/server-process.json +1 -1
- package/templates/nx/.nx/workspace-data/file-map.json +172 -186
- 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 +85 -138
- package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
- package/templates/nx/package.json +3 -3
- package/templates/nx/pnpm-lock.yaml +191 -214
- package/templates/nx/spectra.config.ts +13 -0
- package/templates/react-component/package.json +3 -3
- package/templates/react-component/page-objects/BadgeComponent/web.ts +4 -4
- package/templates/react-component/page-objects/ButtonComponent/web.ts +3 -3
- package/templates/react-component/page-objects/InputComponent/web.ts +1 -1
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +0 -3
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +0 -9
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +1 -1
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +4 -8
- package/templates/react-component/spectra.config.ts +0 -10
- package/templates/react-component/src/components/Badge/Badge.tsx +3 -3
- package/templates/react-component/src/components/Button/Button.tsx +5 -3
- package/templates/react-component/src/components/Input/Input.tsx +2 -3
package/dist/index.js
CHANGED
|
@@ -7,12 +7,13 @@ import { initCommand } from './commands/init.js';
|
|
|
7
7
|
import { installCommand } from './commands/install.js';
|
|
8
8
|
import { runCommand } from './commands/run.js';
|
|
9
9
|
import { testCommand } from './commands/test.js';
|
|
10
|
-
import {
|
|
10
|
+
import { upgradeCommand } from './commands/upgrade.js';
|
|
11
11
|
import { watchCommand } from './commands/watch.js';
|
|
12
12
|
import { syncTypesCommand } from './commands/sync-types.js';
|
|
13
13
|
import { refactorCommand } from './commands/refactor.js';
|
|
14
14
|
import { lintCommand } from './commands/lint.js';
|
|
15
15
|
import { licenseCommand } from './commands/license.js';
|
|
16
|
+
import { docsUpdateCommand } from './commands/docs.js';
|
|
16
17
|
import { skillsInitCommand, skillsListCommand, skillsAddCommand, skillsUpdateCommand, skillsRemoveCommand, } from './commands/skills.js';
|
|
17
18
|
export * from './config/schema.js';
|
|
18
19
|
export * from './config/loader.js';
|
|
@@ -24,10 +25,10 @@ export * from './types/generator.js';
|
|
|
24
25
|
export * from '@testspectra/matchers';
|
|
25
26
|
export function createCliProgram() {
|
|
26
27
|
const program = new Command();
|
|
27
|
-
program.name('spectra').description('TestSpectra
|
|
28
|
+
program.name('spectra').description('TestSpectra Cross-Platform Test Runner CLI').version('1.0.0');
|
|
28
29
|
program
|
|
29
30
|
.command('init')
|
|
30
|
-
.description('Initialize
|
|
31
|
+
.description('Initialize spectra.config.ts and project directories')
|
|
31
32
|
.option('-t, --template <template>', 'Project template: default, nx, react-component')
|
|
32
33
|
.option('-f, --force', 'Overwrite existing configuration if present')
|
|
33
34
|
.action(initCommand);
|
|
@@ -40,15 +41,16 @@ export function createCliProgram() {
|
|
|
40
41
|
program
|
|
41
42
|
.command('install [dependency]')
|
|
42
43
|
.description('Install missing testing dependencies, browser drivers, and workspace packages')
|
|
43
|
-
.option('-f, --force', '
|
|
44
|
+
.option('-f, --force', 'Remove existing artifacts and perform a clean reinstall of the dependency')
|
|
44
45
|
.addHelpText('after', `
|
|
45
46
|
Examples:
|
|
46
47
|
$ spectra install Install all missing required dependencies automatically
|
|
47
48
|
$ spectra install chrome Install Google Chrome for Testing & matching Chromedriver
|
|
48
49
|
$ spectra install adb Install Google Android SDK Platform Tools (ADB)
|
|
49
50
|
$ spectra install bun Install Bun runtime
|
|
50
|
-
$ spectra install android-
|
|
51
|
-
$ spectra install
|
|
51
|
+
$ spectra install android-agent Install Spectra Android Native Agent (spectra-agent.jar)
|
|
52
|
+
$ spectra install android-agent -f Clean reinstall Spectra Android Native Agent
|
|
53
|
+
$ spectra install packages Install TestSpectra CLI, Matchers & Android Agent packages`)
|
|
52
54
|
.action(installCommand);
|
|
53
55
|
program
|
|
54
56
|
.command('demo')
|
|
@@ -85,20 +87,31 @@ Examples:
|
|
|
85
87
|
.action(lintCommand);
|
|
86
88
|
program
|
|
87
89
|
.command('doctor')
|
|
88
|
-
.description('Verify local environment prerequisites (ADB, Spectra Android
|
|
90
|
+
.description('Verify local environment prerequisites (Bun, Git, ADB, Chrome, Spectra Android Agent, workspace packages)')
|
|
89
91
|
.option('-v, --verbose', 'Show full installation paths for all detected runtimes and packages')
|
|
92
|
+
.option('--scope <category>', 'Only check one category: core, web, mobile (default: all)')
|
|
93
|
+
.option('--only <keys>', 'Only check specific dependencies (comma-separated): bun, git, cli, chrome, matchers, adb, agent')
|
|
90
94
|
.option('--fix', 'Attempt automatic fix / download of missing tools')
|
|
91
95
|
.option('--json', 'Output diagnostic results as JSON')
|
|
92
96
|
.action(doctorCommand);
|
|
93
97
|
program
|
|
94
|
-
.command('
|
|
95
|
-
.
|
|
98
|
+
.command('upgrade [packages...]')
|
|
99
|
+
.alias('update')
|
|
100
|
+
.description('Upgrade TestSpectra ecosystem packages and system drivers to the latest versions')
|
|
96
101
|
.option('--check', 'Only check for available updates without installing')
|
|
97
|
-
.
|
|
102
|
+
.option('--packages-only', 'Only upgrade NPM packages in package.json')
|
|
103
|
+
.option('--drivers-only', 'Only upgrade system binaries and drivers (ADB, Agent, Chrome)')
|
|
104
|
+
.option('--json', 'Output the update check (packages + drivers) as JSON (implies --check, no changes applied)')
|
|
105
|
+
.addHelpText('after', `
|
|
106
|
+
Examples:
|
|
107
|
+
$ spectra upgrade Upgrade all packages and system drivers
|
|
108
|
+
$ spectra upgrade @testspectra/cli Upgrade only the CLI package
|
|
109
|
+
$ spectra upgrade @testspectra/cli @testspectra/matchers Upgrade specific packages
|
|
110
|
+
$ spectra upgrade --check Check what is outdated (packages + drivers)`)
|
|
111
|
+
.action(upgradeCommand);
|
|
98
112
|
program
|
|
99
113
|
.command('devices')
|
|
100
|
-
.description('List connected Android
|
|
101
|
-
.option('-t, --target <target>', 'Filter scope: android, ios, web, all', 'all')
|
|
114
|
+
.description('List connected Android devices (ADB)')
|
|
102
115
|
.option('--json', 'Output results as JSON')
|
|
103
116
|
.action(devicesCommand);
|
|
104
117
|
program
|
|
@@ -147,11 +160,19 @@ Examples:
|
|
|
147
160
|
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
148
161
|
.option('-a, --agent <ids>', 'One-off override: comma-separated agent(s) to target, bypassing the saved "init" selection (claude, opencode, kilo, agents, gemini)')
|
|
149
162
|
.action(skillsRemoveCommand);
|
|
163
|
+
const docs = program.command('docs').description('Manage generated TestSpectra workspace documentation');
|
|
164
|
+
docs
|
|
165
|
+
.command('update')
|
|
166
|
+
.description('Regenerate ARCHITECTURE.md from the current workspace state, without re-running init')
|
|
167
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
168
|
+
.action(docsUpdateCommand);
|
|
150
169
|
program
|
|
151
170
|
.command('run [paths...]')
|
|
152
171
|
.description('Execute test suite or individual test case')
|
|
153
172
|
.option('-s, --spec <path>', 'Spec file path to execute')
|
|
154
173
|
.option('-t, --target <target>', 'Target platform: web, android, ios, common', 'web')
|
|
174
|
+
.option('--host <name>', 'Target specific named host defined in spectra.config.ts (webConfig.hosts)')
|
|
175
|
+
.option('--base-url <url>', 'Override webConfig.baseUrl')
|
|
155
176
|
.option('-d, --device <device>', 'Target device name or UDID')
|
|
156
177
|
.option('--all-devices', 'Distribute test execution across all connected devices')
|
|
157
178
|
.option('--multi-device', 'Distribute test execution across all connected devices (alias for --all-devices)')
|
|
@@ -170,20 +191,42 @@ Examples:
|
|
|
170
191
|
.action(runCommand);
|
|
171
192
|
program
|
|
172
193
|
.command('test')
|
|
173
|
-
.description('Run React component tests via @testspectra/react
|
|
194
|
+
.description('Run React component tests via @testspectra/react')
|
|
195
|
+
.option('-r, --runner <engine>', 'Component test runner engine: spectra (default, native CDP) or vitest (Playwright browser mode)', 'spectra')
|
|
174
196
|
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
175
197
|
.option('-s, --spec <path>', 'Spec file path (or substring filter) to execute')
|
|
176
198
|
.option('--headless', 'Run in headless mode (default)')
|
|
177
199
|
.option('--no-headless', 'Run in headed mode')
|
|
178
200
|
.option('--headed', 'Run the browser in visible GUI mode (alias for --no-headless)')
|
|
179
|
-
.option('--step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
|
|
201
|
+
.option('-d, --step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
|
|
180
202
|
.option('--delay <ms>', 'Alias for --step-delay')
|
|
181
|
-
.option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default).
|
|
182
|
-
.option('-o, --output <path>', 'Write a
|
|
203
|
+
.option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default).')
|
|
204
|
+
.option('-o, --output <path>', 'Write a JSON test report to this path')
|
|
183
205
|
.action(testCommand);
|
|
184
206
|
return program;
|
|
185
207
|
}
|
|
186
208
|
export async function runCli(args = process.argv) {
|
|
187
209
|
const program = createCliProgram();
|
|
188
210
|
await program.parseAsync(args);
|
|
211
|
+
// Commands whose stdout must stay machine-readable or is purely diagnostic never emit the
|
|
212
|
+
// interactive "update available" banner.
|
|
213
|
+
const isJson = args.includes('--json');
|
|
214
|
+
const silentCommands = [
|
|
215
|
+
'doctor',
|
|
216
|
+
'install',
|
|
217
|
+
'upgrade',
|
|
218
|
+
'update',
|
|
219
|
+
'license',
|
|
220
|
+
'devices',
|
|
221
|
+
'lint',
|
|
222
|
+
'sync-types',
|
|
223
|
+
'refactor',
|
|
224
|
+
'watch',
|
|
225
|
+
];
|
|
226
|
+
const isSilent = isJson || silentCommands.includes(program.args[0]);
|
|
227
|
+
const { checkUpdateInBackground, displayUpdateNotificationIfNeeded } = await import('./utils/update-notifier.js');
|
|
228
|
+
checkUpdateInBackground();
|
|
229
|
+
if (!isSilent) {
|
|
230
|
+
displayUpdateNotificationIfNeeded();
|
|
231
|
+
}
|
|
189
232
|
}
|
package/dist/linter/discovery.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { ConfigLoader } from '../config/loader.js';
|
|
3
4
|
/**
|
|
4
5
|
* Scans a workspace directory for all `project.json` files that have the
|
|
5
6
|
* "testspectra:shared" tag, and returns their parent folder paths.
|
|
@@ -96,8 +97,7 @@ export function isInsideSharedLibrary(filePath, workspaceDir) {
|
|
|
96
97
|
export function findProjectRoot(startDir) {
|
|
97
98
|
let current = path.resolve(startDir);
|
|
98
99
|
while (true) {
|
|
99
|
-
if (fs.existsSync(path.join(current,
|
|
100
|
-
fs.existsSync(path.join(current, 'spectra.config.js'))) {
|
|
100
|
+
if (ConfigLoader.CONFIG_FILE_NAMES.some((name) => fs.existsSync(path.join(current, name)))) {
|
|
101
101
|
return current;
|
|
102
102
|
}
|
|
103
103
|
const parent = path.dirname(current);
|
|
@@ -11,11 +11,11 @@ export declare const CoverageSchema: z.ZodObject<{
|
|
|
11
11
|
web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
12
12
|
mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
|
-
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
15
14
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
16
|
-
}, {
|
|
17
15
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
16
|
+
}, {
|
|
18
17
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
18
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
19
19
|
}>;
|
|
20
20
|
export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
21
21
|
id: z.ZodString;
|
|
@@ -27,11 +27,11 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
|
27
27
|
web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
28
28
|
mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
29
29
|
}, "strip", z.ZodTypeAny, {
|
|
30
|
-
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
31
30
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
32
|
-
}, {
|
|
33
31
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
32
|
+
}, {
|
|
34
33
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
34
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
37
37
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -41,8 +41,8 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
|
41
41
|
priority: "Critical" | "High" | "Medium" | "Low";
|
|
42
42
|
caseType: "Positive" | "Negative" | "Edge";
|
|
43
43
|
coverage?: {
|
|
44
|
-
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
45
44
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
45
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
46
46
|
} | undefined;
|
|
47
47
|
tags?: string[] | undefined;
|
|
48
48
|
}, {
|
|
@@ -52,8 +52,8 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
|
52
52
|
priority: "Critical" | "High" | "Medium" | "Low";
|
|
53
53
|
caseType: "Positive" | "Negative" | "Edge";
|
|
54
54
|
coverage?: {
|
|
55
|
-
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
56
55
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
56
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
57
57
|
} | undefined;
|
|
58
58
|
tags?: string[] | undefined;
|
|
59
59
|
}>;
|
package/dist/runner/bridge.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Reporter, TestRunResult } from './reporter.js';
|
|
|
3
3
|
export interface RunOptions {
|
|
4
4
|
baseDir: string;
|
|
5
5
|
appDataPath: string;
|
|
6
|
-
platform: 'web' | 'android' | 'ios' | 'common' | 'mobile';
|
|
6
|
+
platform: 'web' | 'android' | 'ios' | 'common' | 'mobile' | 'component';
|
|
7
7
|
suite?: string;
|
|
8
8
|
testCases?: Array<{
|
|
9
9
|
id: string;
|
package/dist/runner/bridge.js
CHANGED
|
@@ -16,16 +16,54 @@ function platformPackageName() {
|
|
|
16
16
|
return '@testspectra/cli-darwin-arm64';
|
|
17
17
|
if (platform === 'linux' && arch === 'x64')
|
|
18
18
|
return '@testspectra/cli-linux-x64';
|
|
19
|
-
if (platform === 'win32' && arch === 'arm64')
|
|
20
|
-
return '@testspectra/cli-win32-arm64';
|
|
21
19
|
if (platform === 'win32' && arch === 'x64')
|
|
22
20
|
return '@testspectra/cli-win32-x64';
|
|
21
|
+
// win32/arm64 has no published native binary (scripts/release-cli-local.sh no longer builds
|
|
22
|
+
// it) — falls through to `pkgName === null` below, which throws a clear "not currently
|
|
23
|
+
// supported" error instead of pointing at an optional dependency that doesn't exist.
|
|
23
24
|
return null;
|
|
24
25
|
}
|
|
25
26
|
export class RustCoreBridge {
|
|
26
27
|
static resolveBinaryPath() {
|
|
27
28
|
const isWindows = process.platform === 'win32';
|
|
28
29
|
const binaryName = isWindows ? 'testspectra-runner.exe' : 'testspectra-runner';
|
|
30
|
+
// In local monorepo development (where core/Cargo.toml exists), prioritize the locally-built
|
|
31
|
+
// binary in core/target/release or cli/bin over any stale optionalDependency in node_modules.
|
|
32
|
+
let workspaceRoot = null;
|
|
33
|
+
let curr = __dirname;
|
|
34
|
+
while (curr !== path.dirname(curr)) {
|
|
35
|
+
if (fs.existsSync(path.join(curr, 'core/Cargo.toml')) || fs.existsSync(path.join(curr, 'pnpm-workspace.yaml'))) {
|
|
36
|
+
workspaceRoot = curr;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
curr = path.dirname(curr);
|
|
40
|
+
}
|
|
41
|
+
const candidatePaths = [
|
|
42
|
+
...(workspaceRoot
|
|
43
|
+
? [
|
|
44
|
+
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
45
|
+
path.join(workspaceRoot, 'target/release', binaryName),
|
|
46
|
+
path.join(workspaceRoot, 'cli/bin', binaryName),
|
|
47
|
+
path.join(workspaceRoot, 'core/cli/target/release', binaryName),
|
|
48
|
+
path.join(workspaceRoot, 'core/target/debug', binaryName),
|
|
49
|
+
path.join(workspaceRoot, 'target/debug', binaryName),
|
|
50
|
+
path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
|
|
51
|
+
]
|
|
52
|
+
: []),
|
|
53
|
+
path.join(__dirname, '../bin', binaryName),
|
|
54
|
+
path.join(__dirname, '../../bin', binaryName),
|
|
55
|
+
path.join(__dirname, '../../bin/testspectra-runner'),
|
|
56
|
+
];
|
|
57
|
+
if (workspaceRoot) {
|
|
58
|
+
for (const p of candidatePaths) {
|
|
59
|
+
if (fs.existsSync(p)) {
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync(`${p}.exe`)) {
|
|
63
|
+
return `${p}.exe`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
29
67
|
// 1. Production install: resolve the prebuilt binary from the platform-specific
|
|
30
68
|
// optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
|
|
31
69
|
// published npm package ships native binaries for every supported platform without
|
|
@@ -52,20 +90,7 @@ export class RustCoreBridge {
|
|
|
52
90
|
// Optional dependency not installed — fall through to the workspace dev paths below.
|
|
53
91
|
}
|
|
54
92
|
}
|
|
55
|
-
// 2.
|
|
56
|
-
// target, as produced by scripts/ensure-runner-binary.js during local development.
|
|
57
|
-
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
58
|
-
const candidatePaths = [
|
|
59
|
-
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
60
|
-
path.join(workspaceRoot, 'target/release', binaryName),
|
|
61
|
-
path.join(workspaceRoot, 'cli/bin', binaryName),
|
|
62
|
-
path.join(__dirname, '../../bin', binaryName),
|
|
63
|
-
path.join(workspaceRoot, 'core/cli/target/release', binaryName),
|
|
64
|
-
path.join(workspaceRoot, 'core/target/debug', binaryName),
|
|
65
|
-
path.join(workspaceRoot, 'target/debug', binaryName),
|
|
66
|
-
path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
|
|
67
|
-
path.join(__dirname, '../../bin/testspectra-runner'),
|
|
68
|
-
];
|
|
93
|
+
// 2. Dev fallback if not already resolved above
|
|
69
94
|
for (const p of candidatePaths) {
|
|
70
95
|
if (fs.existsSync(p)) {
|
|
71
96
|
return p;
|
|
@@ -114,9 +139,12 @@ export class RustCoreBridge {
|
|
|
114
139
|
return new Promise((resolve, reject) => {
|
|
115
140
|
const child = spawn(binPath, [payload], {
|
|
116
141
|
env: runnerEnv,
|
|
142
|
+
windowsHide: true,
|
|
117
143
|
});
|
|
118
144
|
let runStatus = 'failed';
|
|
119
145
|
let runDuration = '0s';
|
|
146
|
+
let completePassedCount;
|
|
147
|
+
let completeFailedCount;
|
|
120
148
|
child.stdout.on('data', (data) => {
|
|
121
149
|
const lines = data.toString().split('\n');
|
|
122
150
|
for (const line of lines) {
|
|
@@ -159,6 +187,12 @@ export class RustCoreBridge {
|
|
|
159
187
|
else if (parsed.type === 'complete') {
|
|
160
188
|
runStatus = parsed.status;
|
|
161
189
|
runDuration = parsed.duration;
|
|
190
|
+
if (typeof parsed.passed_count === 'number') {
|
|
191
|
+
completePassedCount = parsed.passed_count;
|
|
192
|
+
}
|
|
193
|
+
if (typeof parsed.failed_count === 'number') {
|
|
194
|
+
completeFailedCount = parsed.failed_count;
|
|
195
|
+
}
|
|
162
196
|
}
|
|
163
197
|
else if (parsed.type === 'error') {
|
|
164
198
|
reporter.addLog({
|
|
@@ -194,6 +228,14 @@ export class RustCoreBridge {
|
|
|
194
228
|
});
|
|
195
229
|
child.on('close', (code) => {
|
|
196
230
|
let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
|
|
231
|
+
if (result.passedCount === 0 && result.failedCount === 0) {
|
|
232
|
+
if (completePassedCount !== undefined) {
|
|
233
|
+
result.passedCount = completePassedCount;
|
|
234
|
+
}
|
|
235
|
+
if (completeFailedCount !== undefined) {
|
|
236
|
+
result.failedCount = completeFailedCount;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
197
239
|
// Only trust a pre-existing outputJsonPath when it was actually written during THIS run
|
|
198
240
|
// (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
|
|
199
241
|
// native runner writes its own complete report once the pipeline reaches the report
|
package/dist/runner/reporter.js
CHANGED
|
@@ -155,6 +155,7 @@ export class Reporter {
|
|
|
155
155
|
networkEvents = [];
|
|
156
156
|
pendingTests = new Map();
|
|
157
157
|
workerToTest = new Map();
|
|
158
|
+
pendingErrorMsg;
|
|
158
159
|
passedCount = 0;
|
|
159
160
|
failedCount = 0;
|
|
160
161
|
isInteractive;
|
|
@@ -355,10 +356,10 @@ export class Reporter {
|
|
|
355
356
|
const err = raw.replace('[TESTSPECTRA_TEST_ERROR]', '').trim();
|
|
356
357
|
const latest = Array.from(this.pendingTests.values()).pop();
|
|
357
358
|
if (latest) {
|
|
358
|
-
latest.errorMsg = err;
|
|
359
|
+
latest.errorMsg = latest.errorMsg ? `${latest.errorMsg}\n${err}` : err;
|
|
359
360
|
}
|
|
360
361
|
else {
|
|
361
|
-
|
|
362
|
+
this.pendingErrorMsg = this.pendingErrorMsg ? `${this.pendingErrorMsg}\n${err}` : err;
|
|
362
363
|
}
|
|
363
364
|
return;
|
|
364
365
|
}
|
|
@@ -394,16 +395,14 @@ export class Reporter {
|
|
|
394
395
|
this.failedCount++;
|
|
395
396
|
const testCase = this.pendingTests.get(title) || this.resolveTestCase(title);
|
|
396
397
|
const steps = testCase?.steps || [];
|
|
397
|
-
const errorMsg = testCase?.errorMsg;
|
|
398
|
+
const errorMsg = testCase?.errorMsg || this.pendingErrorMsg;
|
|
399
|
+
this.pendingErrorMsg = undefined;
|
|
398
400
|
const workerId = testCase?.workerId;
|
|
399
401
|
this.pendingTests.delete(title);
|
|
400
402
|
if (workerId !== undefined) {
|
|
401
403
|
this.workerToTest.delete(workerId);
|
|
402
404
|
}
|
|
403
405
|
this.clearActiveSlots();
|
|
404
|
-
if (errorMsg) {
|
|
405
|
-
console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errorMsg}\x1b[0m`);
|
|
406
|
-
}
|
|
407
406
|
console.log(`${chalk.hex('#f87171')('✗')} ${chalk.white.bold(title)} ${chalk.gray(`(${duration}ms)`)}`);
|
|
408
407
|
for (const step of steps) {
|
|
409
408
|
if (step.passed) {
|
|
@@ -413,6 +412,13 @@ export class Reporter {
|
|
|
413
412
|
console.log(` ${chalk.red('✖')} ${chalk.red(step.text)} ${chalk.gray(`(${step.duration}ms)`)}`);
|
|
414
413
|
}
|
|
415
414
|
}
|
|
415
|
+
if (errorMsg) {
|
|
416
|
+
const errLines = errorMsg.split('\n');
|
|
417
|
+
console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
|
|
418
|
+
for (let i = 1; i < errLines.length; i++) {
|
|
419
|
+
console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
416
422
|
console.log('');
|
|
417
423
|
this.renderActiveSlots();
|
|
418
424
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { DevServerManager } from '../dev-server-manager.js';
|
|
4
|
+
describe('DevServerManager', () => {
|
|
5
|
+
it('parses command strings into binary and argv array without shell', () => {
|
|
6
|
+
expect(DevServerManager.parseCommand('pnpm --filter sso dev')).toEqual({
|
|
7
|
+
bin: 'pnpm',
|
|
8
|
+
args: ['--filter', 'sso', 'dev'],
|
|
9
|
+
});
|
|
10
|
+
expect(DevServerManager.parseCommand('nx run auth-e2e:serve --port=5174')).toEqual({
|
|
11
|
+
bin: 'nx',
|
|
12
|
+
args: ['run', 'auth-e2e:serve', '--port=5174'],
|
|
13
|
+
});
|
|
14
|
+
expect(DevServerManager.parseCommand('node "./scripts/serve.js" --mode "dev test"')).toEqual({
|
|
15
|
+
bin: 'node',
|
|
16
|
+
args: ['./scripts/serve.js', '--mode', 'dev test'],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('probes listening HTTP server and detects readiness', async () => {
|
|
20
|
+
const server = http.createServer((_req, res) => {
|
|
21
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
22
|
+
res.end('OK');
|
|
23
|
+
});
|
|
24
|
+
await new Promise((resolve) => {
|
|
25
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
26
|
+
});
|
|
27
|
+
const addr = server.address();
|
|
28
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
29
|
+
const isReady = await DevServerManager.probeUrl(url, 1000);
|
|
30
|
+
expect(isReady).toBe(true);
|
|
31
|
+
const isNonExistentReady = await DevServerManager.probeUrl('http://127.0.0.1:49999', 200);
|
|
32
|
+
expect(isNonExistentReady).toBe(false);
|
|
33
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
34
|
+
});
|
|
35
|
+
it('reuses existing running server without spawning new process', async () => {
|
|
36
|
+
const server = http.createServer((_req, res) => {
|
|
37
|
+
res.writeHead(200);
|
|
38
|
+
res.end('OK');
|
|
39
|
+
});
|
|
40
|
+
await new Promise((resolve) => {
|
|
41
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
42
|
+
});
|
|
43
|
+
const addr = server.address();
|
|
44
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
45
|
+
const manager = new DevServerManager();
|
|
46
|
+
const result = await manager.ensureServer({
|
|
47
|
+
name: 'test-server',
|
|
48
|
+
command: 'echo "should not run"',
|
|
49
|
+
url,
|
|
50
|
+
reuseExistingServer: true,
|
|
51
|
+
});
|
|
52
|
+
expect(result.reused).toBe(true);
|
|
53
|
+
expect(result.name).toBe('test-server');
|
|
54
|
+
expect(result.url).toBe(url);
|
|
55
|
+
await manager.stopAll();
|
|
56
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
57
|
+
});
|
|
58
|
+
it('filters dev servers by target host filter', async () => {
|
|
59
|
+
const manager = new DevServerManager();
|
|
60
|
+
const spy = vi.spyOn(manager, 'ensureServer').mockResolvedValue({
|
|
61
|
+
name: 'sso',
|
|
62
|
+
url: 'https://sso.local:5174',
|
|
63
|
+
reused: true,
|
|
64
|
+
});
|
|
65
|
+
await manager.startAll([
|
|
66
|
+
{ name: 'sso', command: 'pnpm sso:dev', url: 'https://sso.local:5174' },
|
|
67
|
+
{ name: 'fam', command: 'pnpm fam:dev', url: 'https://fam.local:5175' },
|
|
68
|
+
], 'sso');
|
|
69
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ name: 'sso' }), expect.any(String));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
const cachePath = process.argv[2];
|
|
4
|
+
if (!cachePath)
|
|
5
|
+
process.exit(0);
|
|
6
|
+
const PACKAGES = ['@testspectra/cli', '@testspectra/matchers'];
|
|
7
|
+
function getLatestVersion(pkg) {
|
|
8
|
+
try {
|
|
9
|
+
const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
|
|
10
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
11
|
+
timeout: 10000,
|
|
12
|
+
})
|
|
13
|
+
.toString()
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/"/g, '');
|
|
16
|
+
return out || null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function run() {
|
|
23
|
+
const updates = [];
|
|
24
|
+
for (const pkg of PACKAGES) {
|
|
25
|
+
const latest = getLatestVersion(pkg);
|
|
26
|
+
if (latest) {
|
|
27
|
+
// Hardcode a check or let the main thread do the actual diff
|
|
28
|
+
// For now, let's just record the latest versions.
|
|
29
|
+
updates.push({ pkg, latest });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// To properly do a diff, we should read package.json of the project.
|
|
33
|
+
// Since we don't have cwd here, we just save the latest versions and let the main thread diff it,
|
|
34
|
+
// OR we can pass the current versions as arguments to this script.
|
|
35
|
+
// For simplicity, we just save the latest versions.
|
|
36
|
+
const cacheData = {
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
latestVersions: updates,
|
|
39
|
+
updatesAvailable: updates, // Main thread will filter this properly later or we just mock it for architecture
|
|
40
|
+
};
|
|
41
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
run().catch(() => { });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface DependencyUpdateStatus {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: 'package' | 'driver';
|
|
4
|
+
identifier: string;
|
|
5
|
+
current: string | null;
|
|
6
|
+
latest: string | null;
|
|
7
|
+
needsUpdate: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Best-effort "installed vs latest" report for the managed system drivers and the Spectra Android
|
|
11
|
+
* Agent, keyed by the exact `name` `spectra doctor` reports so the VS Code Preflight panel can
|
|
12
|
+
* attach a per-row update badge. `latest` stays `null` when it cannot be determined (offline,
|
|
13
|
+
* private registry unavailable) — the row still exposes its reinstall action.
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkDriverUpdates(): Promise<DependencyUpdateStatus[]>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { runSystemChecks } from '../commands/doctor.js';
|
|
3
|
+
const CHROME_FOR_TESTING_API = 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json';
|
|
4
|
+
const BUN_RELEASES_API = 'https://api.github.com/repos/oven-sh/bun/releases/latest';
|
|
5
|
+
const PLATFORM_TOOLS_XML = 'https://dl.google.com/android/repository/repository2-3.xml';
|
|
6
|
+
function normalizeVersion(raw) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return null;
|
|
9
|
+
const match = raw.match(/(\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?)/);
|
|
10
|
+
return match ? match[1] : null;
|
|
11
|
+
}
|
|
12
|
+
async function fetchJson(url) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
signal: AbortSignal.timeout(8000),
|
|
16
|
+
headers: { 'User-Agent': 'testspectra-cli' },
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
return null;
|
|
20
|
+
return await res.json();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function latestChromeVersion() {
|
|
27
|
+
const json = await fetchJson(CHROME_FOR_TESTING_API);
|
|
28
|
+
return json?.channels?.Stable?.version ?? null;
|
|
29
|
+
}
|
|
30
|
+
async function latestBunVersion() {
|
|
31
|
+
const json = await fetchJson(BUN_RELEASES_API);
|
|
32
|
+
const tag = json?.tag_name;
|
|
33
|
+
return typeof tag === 'string' ? tag.replace(/^bun-v/, '') : null;
|
|
34
|
+
}
|
|
35
|
+
async function latestAdbVersion() {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(PLATFORM_TOOLS_XML, {
|
|
38
|
+
signal: AbortSignal.timeout(8000),
|
|
39
|
+
headers: { 'User-Agent': 'testspectra-cli' },
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
return null;
|
|
43
|
+
const xml = await res.text();
|
|
44
|
+
const block = xml.match(/<remotePackage[^>]*path="platform-tools"[\s\S]*?<\/remotePackage>/);
|
|
45
|
+
if (!block)
|
|
46
|
+
return null;
|
|
47
|
+
const rev = block[0].match(/<revision>[\s\S]*?<major>(\d+)<\/major>[\s\S]*?<minor>(\d+)<\/minor>[\s\S]*?<micro>(\d+)<\/micro>/);
|
|
48
|
+
return rev ? `${rev[1]}.${rev[2]}.${rev[3]}` : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function latestAndroidAgentVersion() {
|
|
55
|
+
try {
|
|
56
|
+
return (execFileSync('npm', ['view', '@testspectra/android-agent', 'version', '--json'], {
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
timeout: 10000,
|
|
59
|
+
})
|
|
60
|
+
.toString()
|
|
61
|
+
.trim()
|
|
62
|
+
.replace(/"/g, '') || null);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Best-effort "installed vs latest" report for the managed system drivers and the Spectra Android
|
|
70
|
+
* Agent, keyed by the exact `name` `spectra doctor` reports so the VS Code Preflight panel can
|
|
71
|
+
* attach a per-row update badge. `latest` stays `null` when it cannot be determined (offline,
|
|
72
|
+
* private registry unavailable) — the row still exposes its reinstall action.
|
|
73
|
+
*/
|
|
74
|
+
export async function checkDriverUpdates() {
|
|
75
|
+
let installed;
|
|
76
|
+
try {
|
|
77
|
+
const result = await runSystemChecks();
|
|
78
|
+
installed = result.dependencies;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const [chrome, bun, adb] = await Promise.all([latestChromeVersion(), latestBunVersion(), latestAdbVersion()]);
|
|
84
|
+
const agent = latestAndroidAgentVersion();
|
|
85
|
+
const candidates = [
|
|
86
|
+
{ name: 'Google Chrome', latest: chrome },
|
|
87
|
+
{ name: 'Bun', latest: bun },
|
|
88
|
+
{ name: 'ADB (Android Debug Bridge)', latest: adb },
|
|
89
|
+
{ name: 'Spectra Android Agent', latest: agent },
|
|
90
|
+
];
|
|
91
|
+
const updates = [];
|
|
92
|
+
for (const { name, latest } of candidates) {
|
|
93
|
+
const dep = installed.find((d) => d.name === name);
|
|
94
|
+
if (!dep?.installed)
|
|
95
|
+
continue;
|
|
96
|
+
const current = normalizeVersion(dep.version);
|
|
97
|
+
updates.push({
|
|
98
|
+
name,
|
|
99
|
+
kind: 'driver',
|
|
100
|
+
identifier: name,
|
|
101
|
+
current,
|
|
102
|
+
latest,
|
|
103
|
+
needsUpdate: !!latest && !!current && current !== latest,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return updates;
|
|
107
|
+
}
|