@testspectra/cli 1.1.8-rc.24 → 1.1.8-rc.28
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/bin/.testspectra-runner.hash +1 -0
- package/dist/commands/__tests__/doctor-filter.test.js +37 -0
- package/dist/commands/__tests__/doctor-scope.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor-scope.test.js +24 -0
- package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
- package/dist/commands/__tests__/test-error-streaming.test.js +52 -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/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +16 -3
- package/dist/commands/install.js +3 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.js +114 -22
- package/dist/commands/upgrade.d.ts +8 -1
- package/dist/commands/upgrade.js +63 -13
- package/dist/generator/architecture-doc-generator.js +1 -3
- package/dist/index.js +30 -15
- 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 +45 -16
- package/dist/runner/reporter.js +11 -3
- package/dist/utils/background-update-check.js +1 -8
- package/dist/utils/dependency-updates.d.ts +15 -0
- package/dist/utils/dependency-updates.js +107 -0
- package/package.json +8 -9
- package/templates/default/package.json +2 -2
- 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 +983 -6269
- package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
- package/templates/nx/.nx/workspace-data/file-map.json +20 -34
- 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/project-graph.json +3 -1
- package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
- package/templates/nx/package.json +2 -2
- package/templates/react-component/package.json +3 -3
- package/templates/react-component/spectra.config.ts +0 -10
- package/dist/.tsbuildinfo +0 -1
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.js +0 -109
- package/dist/lifecycle/__tests__/lifecycle-engine.test.js +0 -290
- package/dist/lifecycle/hook-discovery.d.ts +0 -24
- package/dist/lifecycle/hook-discovery.js +0 -60
- package/dist/lifecycle/hook-dispatcher.d.ts +0 -34
- package/dist/lifecycle/hook-dispatcher.js +0 -190
- package/dist/lifecycle/index.d.ts +0 -3
- package/dist/lifecycle/index.js +0 -3
- package/dist/lifecycle/parallel-grouping.d.ts +0 -27
- package/dist/lifecycle/parallel-grouping.js +0 -151
- /package/dist/{lifecycle/__tests__/lifecycle-engine.test.d.ts → commands/__tests__/doctor-filter.test.d.ts} +0 -0
package/dist/commands/upgrade.js
CHANGED
|
@@ -3,9 +3,21 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { RustCoreBridge } from '../runner/bridge.js';
|
|
6
|
+
import { checkDriverUpdates } from '../utils/dependency-updates.js';
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
8
9
|
const PACKAGES = ['@testspectra/cli', '@testspectra/matchers', '@testspectra/react'];
|
|
10
|
+
/** Filters requested package names against the known catalog; empty means "all". */
|
|
11
|
+
function selectPackages(requested) {
|
|
12
|
+
if (requested.length === 0)
|
|
13
|
+
return [...PACKAGES];
|
|
14
|
+
const known = new Set(PACKAGES);
|
|
15
|
+
const unknown = requested.filter((p) => !known.has(p));
|
|
16
|
+
if (unknown.length > 0) {
|
|
17
|
+
console.warn(`\x1b[33mIgnoring unknown TestSpectra package(s): ${unknown.join(', ')}\x1b[0m`);
|
|
18
|
+
}
|
|
19
|
+
return requested.filter((p) => known.has(p));
|
|
20
|
+
}
|
|
9
21
|
function getLatestVersion(pkg) {
|
|
10
22
|
try {
|
|
11
23
|
const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
|
|
@@ -43,11 +55,27 @@ function detectPackageManager(cwd) {
|
|
|
43
55
|
return 'bun';
|
|
44
56
|
return 'npm';
|
|
45
57
|
}
|
|
46
|
-
function
|
|
58
|
+
function collectPackageUpdates(cwd, packages) {
|
|
59
|
+
return packages.map((pkg) => {
|
|
60
|
+
const current = getCurrentVersion(pkg, cwd);
|
|
61
|
+
const latest = getLatestVersion(pkg);
|
|
62
|
+
return { pkg, current, latest, needsUpdate: !!latest && latest !== current };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function printUpdateRow(label, current, latest, needsUpdate) {
|
|
66
|
+
const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
|
|
67
|
+
const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[90m(unknown)\x1b[0m';
|
|
68
|
+
const arrow = needsUpdate ? ` → ${latestStr}` : '';
|
|
69
|
+
const status = needsUpdate ? '\x1b[33m↑\x1b[0m' : '\x1b[32m✓\x1b[0m';
|
|
70
|
+
console.log(`\x1b[36m│\x1b[0m ${status} ${label.padEnd(28)} ${currentStr}${arrow}`);
|
|
71
|
+
}
|
|
72
|
+
function buildInstallCmd(pm, pkgSpecs, cwd) {
|
|
47
73
|
const list = pkgSpecs.join(' ');
|
|
48
74
|
switch (pm) {
|
|
49
|
-
case 'pnpm':
|
|
50
|
-
|
|
75
|
+
case 'pnpm': {
|
|
76
|
+
const isWorkspaceRoot = fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml'));
|
|
77
|
+
return isWorkspaceRoot ? `pnpm add -D -w ${list}` : `pnpm add -D ${list}`;
|
|
78
|
+
}
|
|
51
79
|
case 'bun':
|
|
52
80
|
return `bun add -d ${list}`;
|
|
53
81
|
case 'npm':
|
|
@@ -55,9 +83,24 @@ function buildInstallCmd(pm, pkgSpecs) {
|
|
|
55
83
|
return `npm install -D ${list}`;
|
|
56
84
|
}
|
|
57
85
|
}
|
|
58
|
-
export async function upgradeCommand(options = {}) {
|
|
86
|
+
export async function upgradeCommand(requestedPackages = [], options = {}) {
|
|
59
87
|
const cwd = process.cwd();
|
|
60
88
|
const pm = detectPackageManager(cwd);
|
|
89
|
+
const selected = selectPackages(requestedPackages);
|
|
90
|
+
const targeted = requestedPackages.length > 0;
|
|
91
|
+
if (targeted && selected.length === 0) {
|
|
92
|
+
console.error(`\x1b[31mNo known TestSpectra package matched: ${requestedPackages.join(', ')}. Known packages: ${PACKAGES.join(', ')}\x1b[0m`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Machine-readable mode for the VS Code Preflight panel: implies `--check`, never mutates.
|
|
97
|
+
if (options.json) {
|
|
98
|
+
const packages = collectPackageUpdates(cwd, selected);
|
|
99
|
+
const drivers = targeted ? [] : await checkDriverUpdates();
|
|
100
|
+
const updatesAvailable = [...packages, ...drivers].filter((u) => u.needsUpdate).length;
|
|
101
|
+
console.log(JSON.stringify({ packages, drivers, updatesAvailable }, null, 2));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
61
104
|
console.log('\x1b[36m┌ 🚀 TestSpectra Self-Upgrade Mechanism\x1b[0m');
|
|
62
105
|
console.log('\x1b[36m│\x1b[0m');
|
|
63
106
|
const updates = [];
|
|
@@ -66,11 +109,8 @@ export async function upgradeCommand(options = {}) {
|
|
|
66
109
|
if (!options.driversOnly) {
|
|
67
110
|
console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 1: Workspace Packages\x1b[0m (Manager: \x1b[33m${pm}\x1b[0m)`);
|
|
68
111
|
console.log('\x1b[36m│\x1b[0m Fetching latest versions from npm registry...');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const latest = getLatestVersion(pkg);
|
|
72
|
-
const needsUpdate = !!latest && latest !== current;
|
|
73
|
-
updates.push({ pkg, current, latest, needsUpdate });
|
|
112
|
+
updates.push(...collectPackageUpdates(cwd, selected));
|
|
113
|
+
for (const { pkg, current, latest, needsUpdate } of updates) {
|
|
74
114
|
const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
|
|
75
115
|
const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[31m(unavailable)\x1b[0m';
|
|
76
116
|
const arrow = needsUpdate ? ` → ${latestStr}` : '';
|
|
@@ -84,7 +124,7 @@ export async function upgradeCommand(options = {}) {
|
|
|
84
124
|
}
|
|
85
125
|
else if (toUpdate.length > 0) {
|
|
86
126
|
const pkgSpecs = toUpdate.map((u) => `${u.pkg}@${u.latest}`);
|
|
87
|
-
const installCmd = buildInstallCmd(pm, pkgSpecs);
|
|
127
|
+
const installCmd = buildInstallCmd(pm, pkgSpecs, cwd);
|
|
88
128
|
console.log(`\x1b[36m│\x1b[0m Running: \x1b[90m${installCmd}\x1b[0m`);
|
|
89
129
|
try {
|
|
90
130
|
execSync(installCmd, { cwd, stdio: 'inherit' });
|
|
@@ -101,11 +141,21 @@ export async function upgradeCommand(options = {}) {
|
|
|
101
141
|
console.log('\x1b[36m│\x1b[0m');
|
|
102
142
|
}
|
|
103
143
|
if (options.check) {
|
|
144
|
+
if (!targeted) {
|
|
145
|
+
const drivers = await checkDriverUpdates();
|
|
146
|
+
if (drivers.length > 0) {
|
|
147
|
+
console.log('\x1b[36m│\x1b[0m \x1b[1mDriver & Agent Status\x1b[0m');
|
|
148
|
+
for (const dep of drivers) {
|
|
149
|
+
printUpdateRow(dep.name, dep.current, dep.latest, dep.needsUpdate);
|
|
150
|
+
}
|
|
151
|
+
console.log('\x1b[36m│\x1b[0m');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
104
154
|
console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m\n');
|
|
105
155
|
return;
|
|
106
156
|
}
|
|
107
157
|
// Phase 2: Drivers and Binaries
|
|
108
|
-
if (!options.packagesOnly) {
|
|
158
|
+
if (!options.packagesOnly && !targeted) {
|
|
109
159
|
console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 2: System Binaries & Drivers\x1b[0m`);
|
|
110
160
|
try {
|
|
111
161
|
const binPath = RustCoreBridge.resolveBinaryPath();
|
|
@@ -120,8 +170,8 @@ export async function upgradeCommand(options = {}) {
|
|
|
120
170
|
env: {
|
|
121
171
|
...process.env,
|
|
122
172
|
TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
|
|
123
|
-
TEST_SPECTRA_FORCE_LATEST: '1'
|
|
124
|
-
}
|
|
173
|
+
TEST_SPECTRA_FORCE_LATEST: '1',
|
|
174
|
+
},
|
|
125
175
|
});
|
|
126
176
|
console.log('\x1b[36m│\x1b[0m \x1b[32m✓ Drivers updated successfully.\x1b[0m');
|
|
127
177
|
}
|
|
@@ -64,9 +64,7 @@ export function generateArchitectureDoc(cwd) {
|
|
|
64
64
|
}
|
|
65
65
|
const sharedScope = scopes.find((s) => s.isShared);
|
|
66
66
|
const featureE2eDirs = scopes.filter((s) => !s.isShared).map((s) => s.dir);
|
|
67
|
-
const sharedTestingRelPath = sharedScope
|
|
68
|
-
? path.relative(cwd, sharedScope.dir).replace(/\\/g, '/')
|
|
69
|
-
: 'shared/testing';
|
|
67
|
+
const sharedTestingRelPath = sharedScope ? path.relative(cwd, sharedScope.dir).replace(/\\/g, '/') : 'shared/testing';
|
|
70
68
|
const e2eFolderName = featureE2eDirs.length > 0 ? path.basename(featureE2eDirs[0]) : 'e2e';
|
|
71
69
|
const content = renderArchitectureDoc(path.join(docsTemplateDir, 'ARCHITECTURE.nx.md'), {
|
|
72
70
|
__SHARED_TESTING_REL_PATH__: sharedTestingRelPath,
|
package/dist/index.js
CHANGED
|
@@ -25,10 +25,10 @@ export * from './types/generator.js';
|
|
|
25
25
|
export * from '@testspectra/matchers';
|
|
26
26
|
export function createCliProgram() {
|
|
27
27
|
const program = new Command();
|
|
28
|
-
program.name('spectra').description('TestSpectra
|
|
28
|
+
program.name('spectra').description('TestSpectra Cross-Platform Test Runner CLI').version('1.0.0');
|
|
29
29
|
program
|
|
30
30
|
.command('init')
|
|
31
|
-
.description('Initialize
|
|
31
|
+
.description('Initialize spectra.config.ts and project directories')
|
|
32
32
|
.option('-t, --template <template>', 'Project template: default, nx, react-component')
|
|
33
33
|
.option('-f, --force', 'Overwrite existing configuration if present')
|
|
34
34
|
.action(initCommand);
|
|
@@ -41,15 +41,16 @@ export function createCliProgram() {
|
|
|
41
41
|
program
|
|
42
42
|
.command('install [dependency]')
|
|
43
43
|
.description('Install missing testing dependencies, browser drivers, and workspace packages')
|
|
44
|
-
.option('-f, --force', '
|
|
44
|
+
.option('-f, --force', 'Remove existing artifacts and perform a clean reinstall of the dependency')
|
|
45
45
|
.addHelpText('after', `
|
|
46
46
|
Examples:
|
|
47
47
|
$ spectra install Install all missing required dependencies automatically
|
|
48
48
|
$ spectra install chrome Install Google Chrome for Testing & matching Chromedriver
|
|
49
49
|
$ spectra install adb Install Google Android SDK Platform Tools (ADB)
|
|
50
50
|
$ spectra install bun Install Bun runtime
|
|
51
|
-
$ spectra install android-
|
|
52
|
-
$ 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`)
|
|
53
54
|
.action(installCommand);
|
|
54
55
|
program
|
|
55
56
|
.command('demo')
|
|
@@ -86,23 +87,31 @@ Examples:
|
|
|
86
87
|
.action(lintCommand);
|
|
87
88
|
program
|
|
88
89
|
.command('doctor')
|
|
89
|
-
.description('Verify local environment prerequisites (ADB, Spectra Android
|
|
90
|
+
.description('Verify local environment prerequisites (Bun, Git, ADB, Chrome, Spectra Android Agent, workspace packages)')
|
|
90
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')
|
|
91
94
|
.option('--fix', 'Attempt automatic fix / download of missing tools')
|
|
92
95
|
.option('--json', 'Output diagnostic results as JSON')
|
|
93
96
|
.action(doctorCommand);
|
|
94
97
|
program
|
|
95
|
-
.command('upgrade')
|
|
98
|
+
.command('upgrade [packages...]')
|
|
96
99
|
.alias('update')
|
|
97
100
|
.description('Upgrade TestSpectra ecosystem packages and system drivers to the latest versions')
|
|
98
101
|
.option('--check', 'Only check for available updates without installing')
|
|
99
102
|
.option('--packages-only', 'Only upgrade NPM packages in package.json')
|
|
100
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)`)
|
|
101
111
|
.action(upgradeCommand);
|
|
102
112
|
program
|
|
103
113
|
.command('devices')
|
|
104
|
-
.description('List connected Android
|
|
105
|
-
.option('-t, --target <target>', 'Filter scope: android, ios, web, all', 'all')
|
|
114
|
+
.description('List connected Android devices (ADB)')
|
|
106
115
|
.option('--json', 'Output results as JSON')
|
|
107
116
|
.action(devicesCommand);
|
|
108
117
|
program
|
|
@@ -180,23 +189,29 @@ Examples:
|
|
|
180
189
|
.action(runCommand);
|
|
181
190
|
program
|
|
182
191
|
.command('test')
|
|
183
|
-
.description('Run React component tests via @testspectra/react
|
|
192
|
+
.description('Run React component tests via @testspectra/react')
|
|
193
|
+
.option('-r, --runner <engine>', 'Component test runner engine: spectra (default, native CDP) or vitest (Playwright browser mode)', 'spectra')
|
|
184
194
|
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
185
195
|
.option('-s, --spec <path>', 'Spec file path (or substring filter) to execute')
|
|
186
196
|
.option('--headless', 'Run in headless mode (default)')
|
|
187
197
|
.option('--no-headless', 'Run in headed mode')
|
|
188
198
|
.option('--headed', 'Run the browser in visible GUI mode (alias for --no-headless)')
|
|
189
|
-
.option('--step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
|
|
199
|
+
.option('-d, --step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
|
|
190
200
|
.option('--delay <ms>', 'Alias for --step-delay')
|
|
191
|
-
.option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default).
|
|
192
|
-
.option('-o, --output <path>', 'Write a
|
|
201
|
+
.option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default).')
|
|
202
|
+
.option('-o, --output <path>', 'Write a JSON test report to this path')
|
|
193
203
|
.action(testCommand);
|
|
194
204
|
return program;
|
|
195
205
|
}
|
|
196
206
|
export async function runCli(args = process.argv) {
|
|
197
|
-
const { checkUpdateInBackground, displayUpdateNotificationIfNeeded } = await import('./utils/update-notifier.js');
|
|
198
|
-
checkUpdateInBackground();
|
|
199
207
|
const program = createCliProgram();
|
|
200
208
|
await program.parseAsync(args);
|
|
209
|
+
// Commands whose stdout must stay machine-readable or is purely diagnostic never emit the
|
|
210
|
+
// interactive "update available" banner.
|
|
211
|
+
const silentCommands = ['doctor', 'install', 'upgrade', 'update'];
|
|
212
|
+
if (silentCommands.includes(program.args[0]))
|
|
213
|
+
return;
|
|
214
|
+
const { checkUpdateInBackground, displayUpdateNotificationIfNeeded } = await import('./utils/update-notifier.js');
|
|
215
|
+
checkUpdateInBackground();
|
|
201
216
|
displayUpdateNotificationIfNeeded();
|
|
202
217
|
}
|
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,42 @@ 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
|
+
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
33
|
+
const isMonorepo = fs.existsSync(path.join(workspaceRoot, 'core/Cargo.toml'));
|
|
34
|
+
const candidatePaths = [
|
|
35
|
+
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
36
|
+
path.join(workspaceRoot, 'target/release', binaryName),
|
|
37
|
+
path.join(workspaceRoot, 'cli/bin', binaryName),
|
|
38
|
+
path.join(__dirname, '../../bin', binaryName),
|
|
39
|
+
path.join(workspaceRoot, 'core/cli/target/release', binaryName),
|
|
40
|
+
path.join(workspaceRoot, 'core/target/debug', binaryName),
|
|
41
|
+
path.join(workspaceRoot, 'target/debug', binaryName),
|
|
42
|
+
path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
|
|
43
|
+
path.join(__dirname, '../../bin/testspectra-runner'),
|
|
44
|
+
];
|
|
45
|
+
if (isMonorepo) {
|
|
46
|
+
for (const p of candidatePaths) {
|
|
47
|
+
if (fs.existsSync(p)) {
|
|
48
|
+
return p;
|
|
49
|
+
}
|
|
50
|
+
if (fs.existsSync(`${p}.exe`)) {
|
|
51
|
+
return `${p}.exe`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
29
55
|
// 1. Production install: resolve the prebuilt binary from the platform-specific
|
|
30
56
|
// optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
|
|
31
57
|
// published npm package ships native binaries for every supported platform without
|
|
@@ -52,20 +78,7 @@ export class RustCoreBridge {
|
|
|
52
78
|
// Optional dependency not installed — fall through to the workspace dev paths below.
|
|
53
79
|
}
|
|
54
80
|
}
|
|
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
|
-
];
|
|
81
|
+
// 2. Dev fallback if not already resolved above
|
|
69
82
|
for (const p of candidatePaths) {
|
|
70
83
|
if (fs.existsSync(p)) {
|
|
71
84
|
return p;
|
|
@@ -118,6 +131,8 @@ export class RustCoreBridge {
|
|
|
118
131
|
});
|
|
119
132
|
let runStatus = 'failed';
|
|
120
133
|
let runDuration = '0s';
|
|
134
|
+
let completePassedCount;
|
|
135
|
+
let completeFailedCount;
|
|
121
136
|
child.stdout.on('data', (data) => {
|
|
122
137
|
const lines = data.toString().split('\n');
|
|
123
138
|
for (const line of lines) {
|
|
@@ -160,6 +175,12 @@ export class RustCoreBridge {
|
|
|
160
175
|
else if (parsed.type === 'complete') {
|
|
161
176
|
runStatus = parsed.status;
|
|
162
177
|
runDuration = parsed.duration;
|
|
178
|
+
if (typeof parsed.passed_count === 'number') {
|
|
179
|
+
completePassedCount = parsed.passed_count;
|
|
180
|
+
}
|
|
181
|
+
if (typeof parsed.failed_count === 'number') {
|
|
182
|
+
completeFailedCount = parsed.failed_count;
|
|
183
|
+
}
|
|
163
184
|
}
|
|
164
185
|
else if (parsed.type === 'error') {
|
|
165
186
|
reporter.addLog({
|
|
@@ -195,6 +216,14 @@ export class RustCoreBridge {
|
|
|
195
216
|
});
|
|
196
217
|
child.on('close', (code) => {
|
|
197
218
|
let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
|
|
219
|
+
if (result.passedCount === 0 && result.failedCount === 0) {
|
|
220
|
+
if (completePassedCount !== undefined) {
|
|
221
|
+
result.passedCount = completePassedCount;
|
|
222
|
+
}
|
|
223
|
+
if (completeFailedCount !== undefined) {
|
|
224
|
+
result.failedCount = completeFailedCount;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
198
227
|
// Only trust a pre-existing outputJsonPath when it was actually written during THIS run
|
|
199
228
|
// (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
|
|
200
229
|
// native runner writes its own complete report once the pipeline reaches the report
|
package/dist/runner/reporter.js
CHANGED
|
@@ -355,10 +355,14 @@ export class Reporter {
|
|
|
355
355
|
const err = raw.replace('[TESTSPECTRA_TEST_ERROR]', '').trim();
|
|
356
356
|
const latest = Array.from(this.pendingTests.values()).pop();
|
|
357
357
|
if (latest) {
|
|
358
|
-
latest.errorMsg = err;
|
|
358
|
+
latest.errorMsg = latest.errorMsg ? `${latest.errorMsg}\n${err}` : err;
|
|
359
359
|
}
|
|
360
360
|
else {
|
|
361
|
-
|
|
361
|
+
const errLines = err.split('\n');
|
|
362
|
+
console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
|
|
363
|
+
for (let i = 1; i < errLines.length; i++) {
|
|
364
|
+
console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
|
|
365
|
+
}
|
|
362
366
|
}
|
|
363
367
|
return;
|
|
364
368
|
}
|
|
@@ -402,7 +406,11 @@ export class Reporter {
|
|
|
402
406
|
}
|
|
403
407
|
this.clearActiveSlots();
|
|
404
408
|
if (errorMsg) {
|
|
405
|
-
|
|
409
|
+
const errLines = errorMsg.split('\n');
|
|
410
|
+
console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
|
|
411
|
+
for (let i = 1; i < errLines.length; i++) {
|
|
412
|
+
console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
|
|
413
|
+
}
|
|
406
414
|
}
|
|
407
415
|
console.log(`${chalk.hex('#f87171')('✗')} ${chalk.white.bold(title)} ${chalk.gray(`(${duration}ms)`)}`);
|
|
408
416
|
for (const step of steps) {
|
|
@@ -19,13 +19,6 @@ function getLatestVersion(pkg) {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
function getCurrentVersion(pkg) {
|
|
23
|
-
// In a real scenario we'd check the workspace.
|
|
24
|
-
// For background check, we can check global or try to find it via npm ls,
|
|
25
|
-
// but simpler to just pull from the currently running CLI package.json for '@testspectra/cli'
|
|
26
|
-
// For background script it's tricky to know the workspace, so we just check if it's available.
|
|
27
|
-
return null; // A robust implementation would pass cwd to the background script
|
|
28
|
-
}
|
|
29
22
|
async function run() {
|
|
30
23
|
const updates = [];
|
|
31
24
|
for (const pkg of PACKAGES) {
|
|
@@ -43,7 +36,7 @@ async function run() {
|
|
|
43
36
|
const cacheData = {
|
|
44
37
|
timestamp: Date.now(),
|
|
45
38
|
latestVersions: updates,
|
|
46
|
-
updatesAvailable: updates // Main thread will filter this properly later or we just mock it for architecture
|
|
39
|
+
updatesAvailable: updates, // Main thread will filter this properly later or we just mock it for architecture
|
|
47
40
|
};
|
|
48
41
|
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
49
42
|
}
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testspectra/cli",
|
|
3
|
-
"version": "1.1.8-rc.
|
|
4
|
-
"description": "TestSpectra
|
|
3
|
+
"version": "1.1.8-rc.28",
|
|
4
|
+
"description": "TestSpectra Cross-Platform Test Runner CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@testspectra/matchers": "^1.1.8-rc.
|
|
30
|
-
"@testspectra/react": "^1.1.8-rc.
|
|
31
|
-
"@testspectra/skills": "^1.1.8-rc.
|
|
29
|
+
"@testspectra/matchers": "^1.1.8-rc.28",
|
|
30
|
+
"@testspectra/react": "^1.1.8-rc.28",
|
|
31
|
+
"@testspectra/skills": "^1.1.8-rc.28",
|
|
32
32
|
"chalk": "^5.3.0",
|
|
33
33
|
"commander": "^12.1.0",
|
|
34
34
|
"dotenv": "^16.4.5",
|
|
@@ -38,10 +38,9 @@
|
|
|
38
38
|
"zod": "^3.23.8"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@testspectra/cli-darwin-arm64": "1.1.8-rc.
|
|
42
|
-
"@testspectra/cli-linux-x64": "1.1.8-rc.
|
|
43
|
-
"@testspectra/cli-win32-
|
|
44
|
-
"@testspectra/cli-win32-x64": "1.1.8-rc.24"
|
|
41
|
+
"@testspectra/cli-darwin-arm64": "1.1.8-rc.28",
|
|
42
|
+
"@testspectra/cli-linux-x64": "1.1.8-rc.28",
|
|
43
|
+
"@testspectra/cli-win32-x64": "1.1.8-rc.28"
|
|
45
44
|
},
|
|
46
45
|
"devDependencies": {
|
|
47
46
|
"@types/node": "^20.14.0",
|