@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
|
@@ -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.10",
|
|
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.10",
|
|
30
|
+
"@testspectra/react": "^1.1.10",
|
|
31
|
+
"@testspectra/skills": "^1.1.10",
|
|
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.10",
|
|
43
|
+
"@testspectra/cli-linux-x64": "1.1.10",
|
|
44
|
+
"@testspectra/cli-win32-x64": "1.1.10"
|
|
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.10",
|
|
21
|
+
"@testspectra/matchers": "^1.1.10",
|
|
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
|
+
```
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# TestSpectra Enterprise Monorepo Architecture
|
|
2
|
+
|
|
3
|
+
This workspace uses TestSpectra's **"Centralized Configuration, Distributed Implementation"** model for automated cross-platform testing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Directory & File Overview
|
|
8
|
+
|
|
9
|
+
### Centralized Root Elements
|
|
10
|
+
- `./spectra.config.ts`: Single source of truth for runtime configurations (Base URL, Appium devices, browser targets, timeouts).
|
|
11
|
+
- `./.testspectra/`: Local test project cache, runtime metadata, auto-generated master TSConfig (`.testspectra/tsconfig.json`), platform sub-configs (`.testspectra/tsconfig/`), and universal ambient types (`.testspectra/types/`). Global binaries and drivers are managed at `~/.testspectra/drivers/`.
|
|
12
|
+
- `./nx.json`: Nx target defaults with execution caching and inputs.
|
|
13
|
+
- `./pnpm-workspace.yaml`: Monorepo package glob definitions.
|
|
14
|
+
- `./tsconfig.json`: Solution-style TypeScript orchestrator referencing `./.testspectra/tsconfig.json`.
|
|
15
|
+
|
|
16
|
+
### Centralized Shared Testing Library (`./__SHARED_TESTING_REL_PATH__`)
|
|
17
|
+
Provides cross-feature reusable test entities with **100% zero-import global resolution**:
|
|
18
|
+
- `page-objects/`: Shared Page Object models (e.g. NavigationBar, AppHeader).
|
|
19
|
+
- `support/steps/`: Cross-feature business step flows (e.g. `Step.loginAsAdmin()`).
|
|
20
|
+
- `support/actions/`: Custom atomic actions (e.g. `Spectra.dismissBanner()`).
|
|
21
|
+
- `fixtures/`: Common test data and environment configurations (e.g. `Fixture.appConfig`).
|
|
22
|
+
|
|
23
|
+
### Distributed Feature E2E Modules (__FEATURE_COUNT__ Features)
|
|
24
|
+
__FEATURE_MODULES_SECTION__
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Zero-Import Consumption Flow & Mechanics
|
|
29
|
+
|
|
30
|
+
TestSpectra completely eliminates boilerplate `import` statements across all test specs, steps, and page objects.
|
|
31
|
+
|
|
32
|
+
### How It Works:
|
|
33
|
+
1. **Centralized Ambient Aggregation**:
|
|
34
|
+
TestSpectra Language Service Plugin & CLI automatically scan:
|
|
35
|
+
- **Local Feature Entities**: `<feature>/__E2E_FOLDER_NAME__/page-objects/`, `<feature>/__E2E_FOLDER_NAME__/support/steps/`, `<feature>/__E2E_FOLDER_NAME__/support/actions/`
|
|
36
|
+
- **Shared Library Entities**: `./__SHARED_TESTING_REL_PATH__/page-objects/`, `./__SHARED_TESTING_REL_PATH__/support/steps/`, `./__SHARED_TESTING_REL_PATH__/support/actions/`, `./__SHARED_TESTING_REL_PATH__/fixtures/`
|
|
37
|
+
2. **Ambient Typing Generation**:
|
|
38
|
+
These are compiled into `.testspectra/types/web.d.ts`, `android.d.ts`, `ios.d.ts`, `fixtures.d.ts`.
|
|
39
|
+
3. **Instant IDE Autocomplete & Zero Clutter**:
|
|
40
|
+
Specs consume both local feature objects and shared library utilities globally.
|
|
41
|
+
|
|
42
|
+
### Code Example:
|
|
43
|
+
```typescript
|
|
44
|
+
// packages/features/auth/e2e/specs/Auth/TC-AUTH-01/web.test.ts
|
|
45
|
+
// 🌟 NOTICE: 100% Zero-Import & Flat Execution! (No import statement, no describe wrapper)
|
|
46
|
+
|
|
47
|
+
it("should navigate via shared header and login with local POM", async () => {
|
|
48
|
+
// 1. Consumed from Shared Testing Library (__SHARED_TESTING_REL_PATH__/page-objects/NavigationBar)
|
|
49
|
+
await NavigationBar.goToLogin();
|
|
50
|
+
|
|
51
|
+
// 2. Consumed from Shared Testing Library (__SHARED_TESTING_REL_PATH__/support/steps/loginAsAdmin)
|
|
52
|
+
await Step.loginAsAdmin();
|
|
53
|
+
|
|
54
|
+
// 3. Consumed from Local Feature POM (page-objects/LoginPage)
|
|
55
|
+
await LoginPage.flashAlert.shouldBeVisible();
|
|
56
|
+
await LoginPage.flashAlert.shouldContainText("Welcome");
|
|
57
|
+
|
|
58
|
+
// 4. Consumed from Shared Testing Library Custom Action (__SHARED_TESTING_REL_PATH__/support/actions/dismissBanner)
|
|
59
|
+
await Spectra.dismissBanner();
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 3. How to Run Tests
|
|
66
|
+
|
|
67
|
+
You can execute tests either from the **Monorepo Root via Nx** or directly inside each **Feature Directory via CLI**:
|
|
68
|
+
|
|
69
|
+
### Option A: From Root Monorepo via Nx (Recommended for CI & Team Workflows)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 1. Run E2E for a SPECIFIC feature module (e.g. auth)
|
|
73
|
+
pnpm nx run auth-__E2E_FOLDER_NAME__:e2e
|
|
74
|
+
|
|
75
|
+
# 2. Run E2E for a specific feature on ANDROID or IOS
|
|
76
|
+
pnpm nx run auth-__E2E_FOLDER_NAME__:e2e --configuration=android
|
|
77
|
+
pnpm nx run auth-__E2E_FOLDER_NAME__:e2e --configuration=ios
|
|
78
|
+
|
|
79
|
+
# 3. Run E2E for a specific feature in HEADLESS browser mode
|
|
80
|
+
pnpm nx run auth-__E2E_FOLDER_NAME__:e2e --configuration=headless
|
|
81
|
+
|
|
82
|
+
# 4. Run ALL feature E2E suites across the entire monorepo in parallel
|
|
83
|
+
pnpm nx run-many -t e2e
|
|
84
|
+
|
|
85
|
+
# 5. Run only E2E tests for features modified in current Git branch / PR
|
|
86
|
+
pnpm nx affected -t e2e
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Option B: Directly via TestSpectra CLI (`spectra run`)
|
|
90
|
+
|
|
91
|
+
You can execute tests directly via CLI from either the **Monorepo Root** or inside a **Feature Directory**.
|
|
92
|
+
*Zero file-path overhead: You do NOT need to provide long file globs like `"packages/features/auth/**/*.spec.web.ts"` — just specify the TC ID or Suite name directly.*
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# === From Monorepo Root ===
|
|
96
|
+
|
|
97
|
+
# 1. Run ALL tests across the entire monorepo
|
|
98
|
+
# (Spawns configured dev servers in parallel, checks readiness, runs tests, tears down)
|
|
99
|
+
pnpm spectra run
|
|
100
|
+
|
|
101
|
+
# 2. Run all tests for a specific host / dev server
|
|
102
|
+
pnpm spectra run --host sso
|
|
103
|
+
|
|
104
|
+
# 3. Run a specific test case by ID (simple TC ID, no long file path needed)
|
|
105
|
+
pnpm spectra run --host sso --spec TC-AUTH-01
|
|
106
|
+
|
|
107
|
+
# Or as a direct positional argument:
|
|
108
|
+
pnpm spectra run --host sso TC-AUTH-01
|
|
109
|
+
|
|
110
|
+
# 4. Run an entire suite by suite name
|
|
111
|
+
pnpm spectra run --host sso --spec Auth
|
|
112
|
+
# Or:
|
|
113
|
+
pnpm spectra run --host sso Auth
|
|
114
|
+
|
|
115
|
+
# 5. Run in Headed mode (visible Chrome window)
|
|
116
|
+
pnpm spectra run --host sso --headed
|
|
117
|
+
|
|
118
|
+
# === From Inside Feature Directory (cd __SAMPLE_FEATURE_E2E_PATH__) ===
|
|
119
|
+
|
|
120
|
+
# Run all specs in this feature
|
|
121
|
+
pnpm spectra run
|
|
122
|
+
|
|
123
|
+
# Run a specific test case by ID
|
|
124
|
+
pnpm spectra run TC-AUTH-01 -t web
|
|
125
|
+
|
|
126
|
+
# Run a specific suite by name
|
|
127
|
+
pnpm spectra run Auth -t web
|
|
128
|
+
|
|
129
|
+
# Target Android device or emulator
|
|
130
|
+
pnpm spectra run -t android
|
|
131
|
+
|
|
132
|
+
# Target iOS simulator or device
|
|
133
|
+
pnpm spectra run -t ios
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 4. Type Checking & Validation
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Type check all E2E projects via Nx
|
|
142
|
+
pnpm nx run-many -t type-check
|
|
143
|
+
|
|
144
|
+
# Or standard TypeScript project references build from root
|
|
145
|
+
pnpm type-check
|
|
146
|
+
# or: npx tsc -b
|
|
147
|
+
```
|
|
Binary file
|
|
Binary file
|