@testspectra/cli 1.1.8-rc.9 → 1.1.11-rc.3
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 +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/d/daemon.log +1012 -5748
- package/templates/nx/.nx/workspace-data/file-map.json +171 -185
- 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 +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/.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/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/server-process.json +0 -3
- /package/dist/{lifecycle/__tests__/lifecycle-engine.test.d.ts → commands/__tests__/doctor-filter.test.d.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { DevServerConfig } from '../config/schema.js';
|
|
3
|
+
export interface RunningDevServer {
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
process?: ChildProcess;
|
|
7
|
+
reused: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class DevServerManager {
|
|
10
|
+
private spawnedProcesses;
|
|
11
|
+
private cleanupRegistered;
|
|
12
|
+
constructor();
|
|
13
|
+
/**
|
|
14
|
+
* Safely splits a shell command string into binary executable and argument array without shell expansion.
|
|
15
|
+
*/
|
|
16
|
+
static parseCommand(command: string): {
|
|
17
|
+
bin: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Probes a URL to verify if the server is actively listening and responsive.
|
|
22
|
+
* Performs a rapid TCP connection test followed by an HTTP/HTTPS request.
|
|
23
|
+
*/
|
|
24
|
+
static probeUrl(targetUrl: string, timeoutMs?: number): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Ensures an individual dev server is running. If already responding and reuseExistingServer !== false,
|
|
27
|
+
* reuses the instance. Otherwise, spawns the process and polls until ready.
|
|
28
|
+
*/
|
|
29
|
+
ensureServer(config: DevServerConfig, rootCwd?: string): Promise<RunningDevServer>;
|
|
30
|
+
/**
|
|
31
|
+
* Starts all matching dev servers in parallel.
|
|
32
|
+
*/
|
|
33
|
+
startAll(configs?: DevServerConfig[], targetHostFilter?: string, rootCwd?: string): Promise<RunningDevServer[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Safely terminates all spawned dev server child processes.
|
|
36
|
+
*/
|
|
37
|
+
stopAll(): Promise<void>;
|
|
38
|
+
private killProcess;
|
|
39
|
+
private registerExitHooks;
|
|
40
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
|
+
export class DevServerManager {
|
|
7
|
+
spawnedProcesses = new Map();
|
|
8
|
+
cleanupRegistered = false;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.registerExitHooks();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Safely splits a shell command string into binary executable and argument array without shell expansion.
|
|
14
|
+
*/
|
|
15
|
+
static parseCommand(command) {
|
|
16
|
+
const trimmed = command.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
throw new Error('Dev server command cannot be empty');
|
|
19
|
+
}
|
|
20
|
+
const tokens = [];
|
|
21
|
+
let current = '';
|
|
22
|
+
let inQuotes = false;
|
|
23
|
+
let quoteChar = '';
|
|
24
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
25
|
+
const char = trimmed[i];
|
|
26
|
+
if ((char === '"' || char === "'") && (i === 0 || trimmed[i - 1] !== '\\')) {
|
|
27
|
+
if (inQuotes && char === quoteChar) {
|
|
28
|
+
inQuotes = false;
|
|
29
|
+
quoteChar = '';
|
|
30
|
+
}
|
|
31
|
+
else if (!inQuotes) {
|
|
32
|
+
inQuotes = true;
|
|
33
|
+
quoteChar = char;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
current += char;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (/\s/.test(char) && !inQuotes) {
|
|
40
|
+
if (current.length > 0) {
|
|
41
|
+
tokens.push(current);
|
|
42
|
+
current = '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
current += char;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (current.length > 0) {
|
|
50
|
+
tokens.push(current);
|
|
51
|
+
}
|
|
52
|
+
const bin = tokens[0];
|
|
53
|
+
const args = tokens.slice(1);
|
|
54
|
+
return { bin, args };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Probes a URL to verify if the server is actively listening and responsive.
|
|
58
|
+
* Performs a rapid TCP connection test followed by an HTTP/HTTPS request.
|
|
59
|
+
*/
|
|
60
|
+
static async probeUrl(targetUrl, timeoutMs = 1500) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = new URL(targetUrl);
|
|
63
|
+
const isHttps = parsed.protocol === 'https:';
|
|
64
|
+
const port = parsed.port ? parseInt(parsed.port, 10) : isHttps ? 443 : 80;
|
|
65
|
+
const hostname = parsed.hostname;
|
|
66
|
+
// 1. TCP Probe (Fast check)
|
|
67
|
+
const tcpReachable = await new Promise((resolve) => {
|
|
68
|
+
const socket = net.createConnection({ host: hostname, port, timeout: timeoutMs }, () => {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
resolve(true);
|
|
71
|
+
});
|
|
72
|
+
socket.on('error', () => {
|
|
73
|
+
socket.destroy();
|
|
74
|
+
resolve(false);
|
|
75
|
+
});
|
|
76
|
+
socket.on('timeout', () => {
|
|
77
|
+
socket.destroy();
|
|
78
|
+
resolve(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
if (!tcpReachable) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// 2. HTTP/HTTPS Probe (Verify application readiness, bypass self-signed certs)
|
|
85
|
+
return await new Promise((resolve) => {
|
|
86
|
+
const transport = isHttps ? https : http;
|
|
87
|
+
const req = transport.request(targetUrl, {
|
|
88
|
+
method: 'HEAD',
|
|
89
|
+
timeout: timeoutMs,
|
|
90
|
+
rejectUnauthorized: false,
|
|
91
|
+
}, (res) => {
|
|
92
|
+
// Any valid HTTP response means server is alive and accepting traffic
|
|
93
|
+
res.resume();
|
|
94
|
+
resolve(true);
|
|
95
|
+
});
|
|
96
|
+
req.on('error', () => resolve(false));
|
|
97
|
+
req.on('timeout', () => {
|
|
98
|
+
req.destroy();
|
|
99
|
+
resolve(false);
|
|
100
|
+
});
|
|
101
|
+
req.end();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Ensures an individual dev server is running. If already responding and reuseExistingServer !== false,
|
|
110
|
+
* reuses the instance. Otherwise, spawns the process and polls until ready.
|
|
111
|
+
*/
|
|
112
|
+
async ensureServer(config, rootCwd = process.cwd()) {
|
|
113
|
+
const serverName = config.name || config.url;
|
|
114
|
+
const timeoutMs = config.timeout ?? 60000;
|
|
115
|
+
const reuse = config.reuseExistingServer !== false;
|
|
116
|
+
// Check if server is already running
|
|
117
|
+
if (reuse) {
|
|
118
|
+
const isRunning = await DevServerManager.probeUrl(config.url, 1000);
|
|
119
|
+
if (isRunning) {
|
|
120
|
+
console.log(`\x1b[36m[TestSpectra DevServer]\x1b[0m ⚡ Reusing active server \x1b[1m"${serverName}"\x1b[0m at \x1b[33m${config.url}\x1b[0m`);
|
|
121
|
+
return {
|
|
122
|
+
name: serverName,
|
|
123
|
+
url: config.url,
|
|
124
|
+
reused: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log(`\x1b[36m[TestSpectra DevServer]\x1b[0m 🚀 Launching dev server \x1b[1m"${serverName}"\x1b[0m (\x1b[90m${config.command}\x1b[0m)...`);
|
|
129
|
+
const { bin, args } = DevServerManager.parseCommand(config.command);
|
|
130
|
+
const execCwd = config.cwd ? path.resolve(rootCwd, config.cwd) : rootCwd;
|
|
131
|
+
const logBuffer = [];
|
|
132
|
+
const pushLog = (data) => {
|
|
133
|
+
const lines = data.toString().split('\n');
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (line.trim()) {
|
|
136
|
+
logBuffer.push(line);
|
|
137
|
+
if (logBuffer.length > 50)
|
|
138
|
+
logBuffer.shift();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const child = spawn(bin, args, {
|
|
143
|
+
cwd: execCwd,
|
|
144
|
+
env: { ...process.env, ...config.env },
|
|
145
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
146
|
+
detached: process.platform !== 'win32',
|
|
147
|
+
});
|
|
148
|
+
child.stdout?.on('data', pushLog);
|
|
149
|
+
child.stderr?.on('data', pushLog);
|
|
150
|
+
this.spawnedProcesses.set(serverName, {
|
|
151
|
+
child,
|
|
152
|
+
name: serverName,
|
|
153
|
+
url: config.url,
|
|
154
|
+
logBuffer,
|
|
155
|
+
});
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
let isReady = false;
|
|
158
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
159
|
+
if (child.exitCode !== null) {
|
|
160
|
+
const errorLogs = logBuffer.slice(-20).join('\n');
|
|
161
|
+
throw new Error(`Dev server "${serverName}" terminated prematurely with exit code ${child.exitCode}.\n` +
|
|
162
|
+
`Last output:\n${errorLogs || '(no output)'}`);
|
|
163
|
+
}
|
|
164
|
+
isReady = await DevServerManager.probeUrl(config.url, 500);
|
|
165
|
+
if (isReady) {
|
|
166
|
+
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
167
|
+
console.log(`\x1b[32m[TestSpectra DevServer] ✓ Server "${serverName}" ready at ${config.url} (${durationSec}s)\x1b[0m`);
|
|
168
|
+
return {
|
|
169
|
+
name: serverName,
|
|
170
|
+
url: config.url,
|
|
171
|
+
process: child,
|
|
172
|
+
reused: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
176
|
+
}
|
|
177
|
+
// Timeout exceeded — dump logs and kill child
|
|
178
|
+
this.killProcess(child);
|
|
179
|
+
this.spawnedProcesses.delete(serverName);
|
|
180
|
+
const errorLogs = logBuffer.slice(-25).join('\n');
|
|
181
|
+
throw new Error(`Dev server "${serverName}" timed out after ${timeoutMs}ms waiting for ${config.url}.\n` +
|
|
182
|
+
`Last output:\n${errorLogs || '(no output)'}`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Starts all matching dev servers in parallel.
|
|
186
|
+
*/
|
|
187
|
+
async startAll(configs = [], targetHostFilter, rootCwd = process.cwd()) {
|
|
188
|
+
if (!configs || configs.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
let targetConfigs = configs;
|
|
192
|
+
if (targetHostFilter) {
|
|
193
|
+
const matched = configs.filter((c) => c.name === targetHostFilter);
|
|
194
|
+
if (matched.length > 0) {
|
|
195
|
+
targetConfigs = matched;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return Promise.all(targetConfigs.map((cfg) => this.ensureServer(cfg, rootCwd)));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Safely terminates all spawned dev server child processes.
|
|
202
|
+
*/
|
|
203
|
+
async stopAll() {
|
|
204
|
+
if (this.spawnedProcesses.size === 0)
|
|
205
|
+
return;
|
|
206
|
+
for (const entry of this.spawnedProcesses.values()) {
|
|
207
|
+
try {
|
|
208
|
+
this.killProcess(entry.child);
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
this.spawnedProcesses.clear();
|
|
213
|
+
}
|
|
214
|
+
killProcess(child) {
|
|
215
|
+
if (child.pid && !child.killed) {
|
|
216
|
+
if (process.platform !== 'win32') {
|
|
217
|
+
try {
|
|
218
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
try {
|
|
222
|
+
child.kill('SIGTERM');
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
try {
|
|
229
|
+
child.kill('SIGTERM');
|
|
230
|
+
}
|
|
231
|
+
catch { }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
registerExitHooks() {
|
|
236
|
+
if (this.cleanupRegistered)
|
|
237
|
+
return;
|
|
238
|
+
this.cleanupRegistered = true;
|
|
239
|
+
const cleanup = () => {
|
|
240
|
+
for (const entry of this.spawnedProcesses.values()) {
|
|
241
|
+
try {
|
|
242
|
+
this.killProcess(entry.child);
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
process.once('exit', cleanup);
|
|
248
|
+
process.once('SIGINT', () => {
|
|
249
|
+
cleanup();
|
|
250
|
+
process.exit(130);
|
|
251
|
+
});
|
|
252
|
+
process.once('SIGTERM', () => {
|
|
253
|
+
cleanup();
|
|
254
|
+
process.exit(143);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
|
+
export function checkUpdateInBackground() {
|
|
9
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
10
|
+
const spectraDir = path.join(home, '.testspectra');
|
|
11
|
+
const cachePath = path.join(spectraDir, 'update-cache.json');
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(spectraDir)) {
|
|
14
|
+
fs.mkdirSync(spectraDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
if (fs.existsSync(cachePath)) {
|
|
17
|
+
const stats = fs.statSync(cachePath);
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (now - stats.mtimeMs < CACHE_TTL_MS) {
|
|
20
|
+
// Cache is still valid, don't spawn background check
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Ignore fs errors
|
|
27
|
+
}
|
|
28
|
+
// Spawn background check
|
|
29
|
+
const checkerScript = path.join(__dirname, 'background-update-check.js');
|
|
30
|
+
if (fs.existsSync(checkerScript)) {
|
|
31
|
+
const child = spawn(process.execPath, [checkerScript, cachePath], {
|
|
32
|
+
detached: true,
|
|
33
|
+
stdio: 'ignore',
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
});
|
|
36
|
+
child.unref();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function displayUpdateNotificationIfNeeded() {
|
|
40
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
41
|
+
const cachePath = path.join(home, '.testspectra', 'update-cache.json');
|
|
42
|
+
if (!fs.existsSync(cachePath))
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
46
|
+
if (cache.updatesAvailable && cache.updatesAvailable.length > 0) {
|
|
47
|
+
console.log('\n\x1b[33m╭─────────────────────────────────────────────────────────────────╮\x1b[0m');
|
|
48
|
+
console.log('\x1b[33m│\x1b[0m \x1b[33m│\x1b[0m');
|
|
49
|
+
const pkg = cache.updatesAvailable[0];
|
|
50
|
+
const msg = `Update available: ${pkg.pkg} ${pkg.current ?? '0.0.0'} → ${pkg.latest}`;
|
|
51
|
+
const padding = Math.max(0, 63 - msg.length);
|
|
52
|
+
const padLeft = Math.floor(padding / 2);
|
|
53
|
+
const padRight = padding - padLeft;
|
|
54
|
+
console.log(`\x1b[33m│\x1b[0m${' '.repeat(padLeft)}\x1b[37m${msg}\x1b[0m${' '.repeat(padRight)}\x1b[33m│\x1b[0m`);
|
|
55
|
+
const cmdMsg = 'Run `spectra upgrade` to update CLI, Matchers, and Drivers';
|
|
56
|
+
const cmdPadding = Math.max(0, 63 - cmdMsg.length);
|
|
57
|
+
const cmdPadLeft = Math.floor(cmdPadding / 2);
|
|
58
|
+
const cmdPadRight = cmdPadding - cmdPadLeft;
|
|
59
|
+
console.log(`\x1b[33m│\x1b[0m${' '.repeat(cmdPadLeft)}\x1b[36m${cmdMsg}\x1b[0m${' '.repeat(cmdPadRight)}\x1b[33m│\x1b[0m`);
|
|
60
|
+
console.log('\x1b[33m│\x1b[0m \x1b[33m│\x1b[0m');
|
|
61
|
+
console.log('\x1b[33m╰─────────────────────────────────────────────────────────────────╯\x1b[0m\n');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore errors reading cache
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testspectra/cli",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "TestSpectra
|
|
3
|
+
"version": "1.1.11-rc.3",
|
|
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,11 +26,12 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@testspectra/matchers": "^1.1.
|
|
30
|
-
"@testspectra/react": "^1.1.
|
|
31
|
-
"@testspectra/skills": "^1.1.
|
|
29
|
+
"@testspectra/matchers": "^1.1.11-rc.3",
|
|
30
|
+
"@testspectra/react": "^1.1.11-rc.3",
|
|
31
|
+
"@testspectra/skills": "^1.1.11-rc.3",
|
|
32
32
|
"chalk": "^5.3.0",
|
|
33
33
|
"commander": "^12.1.0",
|
|
34
|
+
"cross-spawn": "^7.0.6",
|
|
34
35
|
"dotenv": "^16.4.5",
|
|
35
36
|
"ora": "^8.0.1",
|
|
36
37
|
"typescript": "^5.6.3",
|
|
@@ -38,12 +39,12 @@
|
|
|
38
39
|
"zod": "^3.23.8"
|
|
39
40
|
},
|
|
40
41
|
"optionalDependencies": {
|
|
41
|
-
"@testspectra/cli-darwin-arm64": "1.1.
|
|
42
|
-
"@testspectra/cli-linux-x64": "1.1.
|
|
43
|
-
"@testspectra/cli-win32-
|
|
44
|
-
"@testspectra/cli-win32-x64": "1.1.8-rc.9"
|
|
42
|
+
"@testspectra/cli-darwin-arm64": "1.1.11-rc.3",
|
|
43
|
+
"@testspectra/cli-linux-x64": "1.1.11-rc.3",
|
|
44
|
+
"@testspectra/cli-win32-x64": "1.1.11-rc.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@types/cross-spawn": "^6.0.6",
|
|
47
48
|
"@types/node": "^20.14.0",
|
|
48
49
|
"@wdio/globals": "^9.24.0",
|
|
49
50
|
"@wdio/mocha-framework": "^9.24.0",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"postinstall": "spectra sync-types"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@testspectra/cli": "^1.1.
|
|
21
|
-
"@testspectra/matchers": "^1.1.
|
|
20
|
+
"@testspectra/cli": "^1.1.11-rc.3",
|
|
21
|
+
"@testspectra/matchers": "^1.1.11-rc.3",
|
|
22
22
|
"@types/node": "^20.14.0",
|
|
23
23
|
"@wdio/cli": "^9.2.8",
|
|
24
24
|
"@wdio/local-runner": "^9.2.8",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# TestSpectra Project Architecture
|
|
2
|
+
|
|
3
|
+
This project is built with the TestSpectra testing framework.
|
|
4
|
+
|
|
5
|
+
## Structure & File Map
|
|
6
|
+
- `./spectra.config.ts`: Typed runner configuration.
|
|
7
|
+
- `./specs/`: Layered test cases (`specs/<Suite>/<CaseId>/` with `web.test.ts`, `android.test.ts`, `ios.test.ts`).
|
|
8
|
+
- `./page-objects/`: Page Object models per platform.
|
|
9
|
+
- `./support/steps/`: Business flows accessible globally via `Step.*`.
|
|
10
|
+
- `./support/actions/`: Atomic actions accessible globally via `Spectra.*`.
|
|
11
|
+
- `./fixtures/`: JSON fixtures accessible globally via `Fixture.*`.
|
|
12
|
+
- `./global-hooks/`: Global suite lifecycle hooks.
|
|
13
|
+
- `./.testspectra/`: Auto-generated ambient typings and local runtime cache.
|
|
14
|
+
|
|
15
|
+
## Execution
|
|
16
|
+
```bash
|
|
17
|
+
# Run web test suite
|
|
18
|
+
pnpm test
|
|
19
|
+
|
|
20
|
+
# Run Android / iOS
|
|
21
|
+
pnpm spectra run --target android
|
|
22
|
+
pnpm spectra run --target ios
|
|
23
|
+
|
|
24
|
+
# Type check
|
|
25
|
+
pnpm type-check
|
|
26
|
+
```
|