@testspectra/cli 1.1.8-rc.8 → 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 +5 -4
- 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 +7 -160
- 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/__tests__/type-generator.test.js +38 -0
- 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/generator/type-generator.d.ts +5 -2
- package/dist/generator/type-generator.js +14 -17
- package/dist/index.js +59 -16
- package/dist/linter/__tests__/shared-lib-boundary.test.js +15 -15
- package/dist/linter/discovery.d.ts +3 -3
- package/dist/linter/discovery.js +7 -7
- package/dist/linter/schemas/spec.schema.d.ts +6 -6
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +67 -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 +947 -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 +90 -144
- package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
- package/templates/nx/package.json +3 -3
- package/templates/nx/packages/matchers/e2e/project.json +2 -2
- package/templates/nx/packages/playground/e2e/project.json +2 -2
- package/templates/nx/pnpm-lock.yaml +191 -214
- package/templates/nx/shared/testing/project.json +1 -2
- 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/commands/run.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Reporter } from '../runner/reporter.js';
|
|
|
6
6
|
import { TypeGenerator } from '../types/generator.js';
|
|
7
7
|
import { DemoServer } from '../utils/demo-server.js';
|
|
8
8
|
import { DemoApiServer } from '../utils/api-server.js';
|
|
9
|
+
import { DevServerManager } from '../utils/dev-server-manager.js';
|
|
9
10
|
function renderSummaryCard(options) {
|
|
10
11
|
const width = 64;
|
|
11
12
|
const borderColor = options.isPassed ? '\x1b[38;2;52;211;153m' : '\x1b[38;2;248;113;113m';
|
|
@@ -157,9 +158,29 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
const config = await ConfigLoader.loadConfig(cwd);
|
|
161
|
+
if (options.baseUrl) {
|
|
162
|
+
config.webConfig.baseUrl = options.baseUrl;
|
|
163
|
+
}
|
|
164
|
+
else if (options.host) {
|
|
165
|
+
if (config.webConfig.hosts && config.webConfig.hosts[options.host]) {
|
|
166
|
+
config.webConfig.baseUrl = config.webConfig.hosts[options.host];
|
|
167
|
+
console.log(`\x1b[36m[TestSpectra]\x1b[0m 🎯 Active Host target: \x1b[33m${options.host}\x1b[0m (\x1b[32m${config.webConfig.baseUrl}\x1b[0m)`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.warn(`\x1b[33m[TestSpectra] Warning: Target host "${options.host}" is not defined in spectra.config.ts (webConfig.hosts). Falling back to ${config.webConfig.baseUrl}\x1b[0m`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
160
173
|
let demoServer = null;
|
|
161
174
|
let apiServer = null;
|
|
175
|
+
const devServerManager = new DevServerManager();
|
|
162
176
|
try {
|
|
177
|
+
const configuredDevServers = [
|
|
178
|
+
...(config.webConfig.devServers || []),
|
|
179
|
+
...(config.webConfig.devServer ? [config.webConfig.devServer] : []),
|
|
180
|
+
];
|
|
181
|
+
if (configuredDevServers.length > 0 && !options.demo) {
|
|
182
|
+
await devServerManager.startAll(configuredDevServers, options.host, cwd);
|
|
183
|
+
}
|
|
163
184
|
if (options.demo) {
|
|
164
185
|
demoServer = new DemoServer(5173);
|
|
165
186
|
try {
|
|
@@ -343,6 +364,7 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
343
364
|
process.exit(1);
|
|
344
365
|
}
|
|
345
366
|
finally {
|
|
367
|
+
await devServerManager.stopAll();
|
|
346
368
|
if (demoServer) {
|
|
347
369
|
await demoServer.stop();
|
|
348
370
|
}
|
package/dist/commands/test.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export interface TestCommandOptions {
|
|
2
2
|
cwd?: string;
|
|
3
|
+
runner?: 'spectra' | 'vitest';
|
|
3
4
|
headed?: boolean;
|
|
4
5
|
headless?: boolean;
|
|
5
6
|
stepDelay?: string | number;
|
|
@@ -8,6 +9,18 @@ export interface TestCommandOptions {
|
|
|
8
9
|
output?: string;
|
|
9
10
|
concurrency?: string | number;
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Guarantees the `.testspectra` solution-tsconfig scaffolding exists before Vitest starts.
|
|
14
|
+
*
|
|
15
|
+
* The bundled Vitest browser runner transpiles raw TypeScript specs/page-objects Just-In-Time.
|
|
16
|
+
* When it does, `vite:esbuild` walks the nearest `tsconfig.json` and tries to read every entry in
|
|
17
|
+
* its `references` — including `./.testspectra/tsconfig.json`. That directory is gitignored, so on
|
|
18
|
+
* a clean clone / fresh CI checkout the reference points at a file that does not exist, esbuild
|
|
19
|
+
* throws `ENOENT`, Vite answers HTTP 500, and every suite dies with "Failed to fetch dynamically
|
|
20
|
+
* imported module" before a single assertion runs. Regenerating the scaffolding up front (same
|
|
21
|
+
* output as `spectra sync-types`) keeps the reference resolvable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ensureTestSpectraScaffolding(cwd: string): void;
|
|
11
24
|
/**
|
|
12
25
|
* `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
|
|
13
26
|
* component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
|
package/dist/commands/test.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import * as p from '@clack/prompts';
|
|
5
|
+
import { ConfigLoader } from '../config/loader.js';
|
|
4
6
|
import { renderSummaryView } from '../reporter/summary-view.js';
|
|
5
7
|
import { Reporter } from '../runner/reporter.js';
|
|
8
|
+
import { RustCoreBridge } from '../runner/bridge.js';
|
|
9
|
+
import { TypeGenerator } from '../generator/type-generator.js';
|
|
6
10
|
import { runSystemChecks } from './doctor.js';
|
|
11
|
+
/**
|
|
12
|
+
* Guarantees the `.testspectra` solution-tsconfig scaffolding exists before Vitest starts.
|
|
13
|
+
*
|
|
14
|
+
* The bundled Vitest browser runner transpiles raw TypeScript specs/page-objects Just-In-Time.
|
|
15
|
+
* When it does, `vite:esbuild` walks the nearest `tsconfig.json` and tries to read every entry in
|
|
16
|
+
* its `references` — including `./.testspectra/tsconfig.json`. That directory is gitignored, so on
|
|
17
|
+
* a clean clone / fresh CI checkout the reference points at a file that does not exist, esbuild
|
|
18
|
+
* throws `ENOENT`, Vite answers HTTP 500, and every suite dies with "Failed to fetch dynamically
|
|
19
|
+
* imported module" before a single assertion runs. Regenerating the scaffolding up front (same
|
|
20
|
+
* output as `spectra sync-types`) keeps the reference resolvable.
|
|
21
|
+
*/
|
|
22
|
+
export function ensureTestSpectraScaffolding(cwd) {
|
|
23
|
+
const masterConfigPath = path.join(cwd, '.testspectra', 'tsconfig.json');
|
|
24
|
+
if (fs.existsSync(masterConfigPath))
|
|
25
|
+
return;
|
|
26
|
+
TypeGenerator.generateAll(cwd);
|
|
27
|
+
}
|
|
7
28
|
/**
|
|
8
29
|
* Finds the Chrome-for-Testing binary that `spectra doctor` already installs/manages for the
|
|
9
30
|
* E2E CDP driver (`core/driver-cdp`) — same install, same binary. `@testspectra/react` must not
|
|
@@ -14,8 +35,10 @@ import { runSystemChecks } from './doctor.js';
|
|
|
14
35
|
*/
|
|
15
36
|
async function resolveBrowserExecutable(cwd) {
|
|
16
37
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
// `--only chrome` runs exactly one check — no adb/bun/git spawns, no Matchers lookup — since
|
|
39
|
+
// this call exists solely to locate the managed browser binary.
|
|
40
|
+
const { dependencies } = await runSystemChecks(cwd, { only: 'chrome' });
|
|
41
|
+
const chrome = dependencies.find((d) => d.category === 'web' && d.name.includes('Chrome'));
|
|
19
42
|
return chrome?.installed && chrome.location ? chrome.location : null;
|
|
20
43
|
}
|
|
21
44
|
catch {
|
|
@@ -30,6 +53,11 @@ async function resolveBrowserExecutable(cwd) {
|
|
|
30
53
|
*/
|
|
31
54
|
export async function testCommand(options = {}) {
|
|
32
55
|
const cwd = options.cwd ?? process.cwd();
|
|
56
|
+
// User-facing phase log. Deliberately describes TestSpectra's own stages only — never the
|
|
57
|
+
// bundled runtime/toolchain underneath — so the CLI surface stays implementation-agnostic.
|
|
58
|
+
const logPhase = (message) => p.log.step(chalk.dim(message));
|
|
59
|
+
logPhase('Preparing component test workspace…');
|
|
60
|
+
ensureTestSpectraScaffolding(cwd);
|
|
33
61
|
let reactPkg;
|
|
34
62
|
try {
|
|
35
63
|
reactPkg = await import('@testspectra/react');
|
|
@@ -41,6 +69,7 @@ export async function testCommand(options = {}) {
|
|
|
41
69
|
process.exit(1);
|
|
42
70
|
return;
|
|
43
71
|
}
|
|
72
|
+
logPhase('Resolving managed test browser…');
|
|
44
73
|
const browserExecutablePath = await resolveBrowserExecutable(cwd);
|
|
45
74
|
if (!browserExecutablePath) {
|
|
46
75
|
p.log.error(chalk.red('✖ TestSpectra-managed Chrome for Testing was not found.'));
|
|
@@ -64,6 +93,21 @@ export async function testCommand(options = {}) {
|
|
|
64
93
|
else if (options.headless !== undefined) {
|
|
65
94
|
headless = options.headless;
|
|
66
95
|
}
|
|
96
|
+
// Fallback to spectra.config.ts if options were not explicitly provided on CLI
|
|
97
|
+
try {
|
|
98
|
+
const config = await ConfigLoader.loadConfig(cwd);
|
|
99
|
+
if (stepDelayMs === undefined && config?.webConfig?.stepDelay !== undefined) {
|
|
100
|
+
const parsed = parseInt(String(config.webConfig.stepDelay), 10);
|
|
101
|
+
if (!isNaN(parsed) && parsed >= 0)
|
|
102
|
+
stepDelayMs = parsed;
|
|
103
|
+
}
|
|
104
|
+
if (!options.headed && options.headless === undefined && config?.webConfig?.headless !== undefined) {
|
|
105
|
+
headless = String(config.webConfig.headless) !== 'false' && Boolean(config.webConfig.headless);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* ignore config load failures */
|
|
110
|
+
}
|
|
67
111
|
const concurrency = options.concurrency !== undefined ? parseInt(String(options.concurrency), 10) : undefined;
|
|
68
112
|
const outputFile = options.output ? path.resolve(cwd, options.output) : undefined;
|
|
69
113
|
// Reuses the exact same semantic terminal reporter `spectra run` drives (see
|
|
@@ -72,31 +116,81 @@ export async function testCommand(options = {}) {
|
|
|
72
116
|
// / setup.ts), so the same Reporter class parses them with zero changes on either side.
|
|
73
117
|
const reporter = new Reporter();
|
|
74
118
|
const runStartedAt = Date.now();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
let ok = false;
|
|
120
|
+
if (options.runner === 'vitest') {
|
|
121
|
+
logPhase('Starting component test runner (Vitest Browser Mode)…');
|
|
122
|
+
ok = await reactPkg.runComponentTests({
|
|
123
|
+
runner: 'vitest',
|
|
124
|
+
cwd,
|
|
125
|
+
headless,
|
|
126
|
+
stepDelayMs,
|
|
127
|
+
spec: options.spec,
|
|
128
|
+
outputFile,
|
|
129
|
+
concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
|
|
130
|
+
browserExecutablePath,
|
|
131
|
+
onLine: (line) => {
|
|
132
|
+
const cleanLine = line.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '').trim();
|
|
133
|
+
if (!cleanLine)
|
|
134
|
+
return;
|
|
135
|
+
if (cleanLine.startsWith('[TESTSPECTRA_')) {
|
|
136
|
+
reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'INFO', message: cleanLine });
|
|
137
|
+
if (!reporter.isInteractiveMode()) {
|
|
138
|
+
console.log(cleanLine);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (reactPkg.isVitestNoise?.(cleanLine)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const sanitized = reactPkg.sanitizeErrorMessage?.(cleanLine) ?? cleanLine;
|
|
146
|
+
reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'ERROR', message: sanitized });
|
|
147
|
+
if (!reporter.isInteractiveMode()) {
|
|
148
|
+
console.error(chalk.red(sanitized));
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
logPhase('Starting component test runner (Spectra Native CDP Engine)…');
|
|
155
|
+
const bundleServer = await reactPkg.startBundleServer({
|
|
156
|
+
testRoot: cwd,
|
|
157
|
+
stepDelayMs: stepDelayMs ?? 0,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
161
|
+
const globalAppDataPath = path.join(home, '.testspectra');
|
|
162
|
+
const appDataPath = globalAppDataPath;
|
|
163
|
+
const runResult = await RustCoreBridge.run({
|
|
164
|
+
baseDir: cwd,
|
|
165
|
+
appDataPath,
|
|
166
|
+
platform: 'component',
|
|
167
|
+
config: {
|
|
168
|
+
bundleServerUrl: bundleServer.url,
|
|
169
|
+
webConfig: {
|
|
170
|
+
headless,
|
|
171
|
+
stepDelay: stepDelayMs,
|
|
172
|
+
browserExecutablePath,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
specs: options.spec ? [options.spec] : undefined,
|
|
176
|
+
concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
|
|
177
|
+
outputJsonPath: outputFile,
|
|
178
|
+
}, reporter);
|
|
179
|
+
ok = runResult.status.toLowerCase() === 'passed';
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
bundleServer.stop();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
92
185
|
const durationMs = Date.now() - runStartedAt;
|
|
93
186
|
const result = reporter.generateResult(ok ? 'passed' : 'failed', `${(durationMs / 1000).toFixed(1)}s`);
|
|
94
|
-
const
|
|
187
|
+
const failedCount = !ok && result.failedCount === 0 ? 1 : result.failedCount;
|
|
188
|
+
const totalTests = result.passedCount + failedCount;
|
|
95
189
|
renderSummaryView({
|
|
96
190
|
totalSuites: totalTests,
|
|
97
191
|
totalTests,
|
|
98
192
|
passedCount: result.passedCount,
|
|
99
|
-
failedCount
|
|
193
|
+
failedCount,
|
|
100
194
|
durationMs,
|
|
101
195
|
reportLocation: outputFile ? path.relative(cwd, outputFile) : '(use -o/--output to save a report)',
|
|
102
196
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PackageUpdate {
|
|
2
|
+
pkg: string;
|
|
3
|
+
current: string | null;
|
|
4
|
+
latest: string | null;
|
|
5
|
+
needsUpdate: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function upgradeCommand(requestedPackages?: string[], options?: {
|
|
8
|
+
check?: boolean;
|
|
9
|
+
packagesOnly?: boolean;
|
|
10
|
+
driversOnly?: boolean;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { execSync, execFileSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { RustCoreBridge } from '../runner/bridge.js';
|
|
6
|
+
import { checkDriverUpdates } from '../utils/dependency-updates.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
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
|
+
}
|
|
21
|
+
function getLatestVersion(pkg) {
|
|
22
|
+
try {
|
|
23
|
+
const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
|
|
24
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
25
|
+
timeout: 10000,
|
|
26
|
+
})
|
|
27
|
+
.toString()
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/"/g, '');
|
|
30
|
+
return out || null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function getCurrentVersion(pkg, cwd) {
|
|
37
|
+
let cur = path.resolve(cwd);
|
|
38
|
+
while (cur && cur !== path.dirname(cur)) {
|
|
39
|
+
const pkgJson = path.join(cur, 'node_modules', pkg, 'package.json');
|
|
40
|
+
if (fs.existsSync(pkgJson)) {
|
|
41
|
+
try {
|
|
42
|
+
const p = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
|
|
43
|
+
return p.version ?? null;
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
cur = path.dirname(cur);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function detectPackageManager(cwd) {
|
|
52
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
|
|
53
|
+
return 'pnpm';
|
|
54
|
+
if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
|
|
55
|
+
return 'bun';
|
|
56
|
+
return 'npm';
|
|
57
|
+
}
|
|
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) {
|
|
73
|
+
const list = pkgSpecs.join(' ');
|
|
74
|
+
switch (pm) {
|
|
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
|
+
}
|
|
79
|
+
case 'bun':
|
|
80
|
+
return `bun add -d ${list}`;
|
|
81
|
+
case 'npm':
|
|
82
|
+
default:
|
|
83
|
+
return `npm install -D ${list}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function upgradeCommand(requestedPackages = [], options = {}) {
|
|
87
|
+
const cwd = process.cwd();
|
|
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
|
+
}
|
|
104
|
+
console.log('\x1b[36m┌ 🚀 TestSpectra Self-Upgrade Mechanism\x1b[0m');
|
|
105
|
+
console.log('\x1b[36m│\x1b[0m');
|
|
106
|
+
const updates = [];
|
|
107
|
+
let packagesUpdated = false;
|
|
108
|
+
// Phase 1: NPM Packages
|
|
109
|
+
if (!options.driversOnly) {
|
|
110
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 1: Workspace Packages\x1b[0m (Manager: \x1b[33m${pm}\x1b[0m)`);
|
|
111
|
+
console.log('\x1b[36m│\x1b[0m Fetching latest versions from npm registry...');
|
|
112
|
+
updates.push(...collectPackageUpdates(cwd, selected));
|
|
113
|
+
for (const { pkg, current, latest, needsUpdate } of updates) {
|
|
114
|
+
const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
|
|
115
|
+
const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[31m(unavailable)\x1b[0m';
|
|
116
|
+
const arrow = needsUpdate ? ` → ${latestStr}` : '';
|
|
117
|
+
const status = !latest ? '\x1b[31m✗\x1b[0m' : needsUpdate ? '\x1b[33m↑\x1b[0m' : '\x1b[32m✓\x1b[0m';
|
|
118
|
+
console.log(`\x1b[36m│\x1b[0m ${status} ${pkg.padEnd(28)} ${currentStr}${arrow}`);
|
|
119
|
+
}
|
|
120
|
+
const toUpdate = updates.filter((u) => u.needsUpdate && u.latest);
|
|
121
|
+
console.log('\x1b[36m│\x1b[0m');
|
|
122
|
+
if (options.check) {
|
|
123
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[33m${toUpdate.length} package update(s) available. Run \x1b[36mspectra upgrade\x1b[33m to install.\x1b[0m`);
|
|
124
|
+
}
|
|
125
|
+
else if (toUpdate.length > 0) {
|
|
126
|
+
const pkgSpecs = toUpdate.map((u) => `${u.pkg}@${u.latest}`);
|
|
127
|
+
const installCmd = buildInstallCmd(pm, pkgSpecs, cwd);
|
|
128
|
+
console.log(`\x1b[36m│\x1b[0m Running: \x1b[90m${installCmd}\x1b[0m`);
|
|
129
|
+
try {
|
|
130
|
+
execSync(installCmd, { cwd, stdio: 'inherit' });
|
|
131
|
+
packagesUpdated = true;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(`\x1b[31m│ ✗ Package upgrade failed: ${err.message}\x1b[0m`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log('\x1b[36m│\x1b[0m \x1b[32m✓ All TestSpectra packages are already up-to-date!\x1b[0m');
|
|
140
|
+
}
|
|
141
|
+
console.log('\x1b[36m│\x1b[0m');
|
|
142
|
+
}
|
|
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
|
+
}
|
|
154
|
+
console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m\n');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Phase 2: Drivers and Binaries
|
|
158
|
+
if (!options.packagesOnly && !targeted) {
|
|
159
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 2: System Binaries & Drivers\x1b[0m`);
|
|
160
|
+
try {
|
|
161
|
+
const binPath = RustCoreBridge.resolveBinaryPath();
|
|
162
|
+
// Using doctor --fix as the underlying mechanism for pulling drivers
|
|
163
|
+
// In the future, this could be passed a specific --force-latest flag
|
|
164
|
+
console.log(`\x1b[36m│\x1b[0m Forcing update for ADB and Spectra Android Agent...`);
|
|
165
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
166
|
+
const globalDriversDir = path.join(home, '.testspectra', 'drivers');
|
|
167
|
+
execSync(`"${binPath}" doctor --fix`, {
|
|
168
|
+
cwd,
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
env: {
|
|
171
|
+
...process.env,
|
|
172
|
+
TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
|
|
173
|
+
TEST_SPECTRA_FORCE_LATEST: '1',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
console.log('\x1b[36m│\x1b[0m \x1b[32m✓ Drivers updated successfully.\x1b[0m');
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.error(`\x1b[31m│ ✗ Driver upgrade failed: ${err.message}\x1b[0m`);
|
|
180
|
+
// We don't exit 1 here so we can still print the post-upgrade report
|
|
181
|
+
}
|
|
182
|
+
console.log('\x1b[36m│\x1b[0m');
|
|
183
|
+
}
|
|
184
|
+
// Phase 3: Post-Upgrade Report
|
|
185
|
+
console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
|
|
186
|
+
console.log('\x1b[32m🎉 TestSpectra upgrade complete!\x1b[0m');
|
|
187
|
+
if (!options.driversOnly && packagesUpdated) {
|
|
188
|
+
const toUpdate = updates.filter((u) => u.needsUpdate && u.latest);
|
|
189
|
+
for (const u of toUpdate) {
|
|
190
|
+
console.log(` \x1b[32m✓\x1b[0m ${u.pkg}: v${u.current ?? '?'} → v${u.latest}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!options.packagesOnly) {
|
|
194
|
+
console.log(` \x1b[32m✓\x1b[0m System drivers and agents synchronized to latest versions`);
|
|
195
|
+
}
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for an individual local development server lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
export interface DevServerConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Unique name or alias identifying this server (e.g. "sso", "adminConsole", "globalSettings").
|
|
7
|
+
*/
|
|
8
|
+
name?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Shell command to launch the development server (e.g. "pnpm --filter sso dev" or "nx serve sso").
|
|
11
|
+
*/
|
|
12
|
+
command: string;
|
|
13
|
+
/**
|
|
14
|
+
* Target URL or healthcheck endpoint (e.g. "https://sso.tagsamurai.local:5174").
|
|
15
|
+
*/
|
|
16
|
+
url: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional port number (inferred from `url` if omitted).
|
|
19
|
+
*/
|
|
20
|
+
port?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum wait timeout in milliseconds for the server to become interactive and accept HTTP connections.
|
|
23
|
+
* @default 60000
|
|
24
|
+
*/
|
|
25
|
+
timeout?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to reuse an already running development server if its port/URL is actively responding.
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
reuseExistingServer?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Working directory from which to launch `command`. Defaults to workspace root.
|
|
33
|
+
*/
|
|
34
|
+
cwd?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Custom environment variables passed to the spawned development server process.
|
|
37
|
+
*/
|
|
38
|
+
env?: Record<string, string>;
|
|
39
|
+
}
|
|
1
40
|
/**
|
|
2
41
|
* Configuration for web automation and browser test runners.
|
|
3
42
|
*/
|
|
@@ -7,6 +46,20 @@ export interface WebConfig {
|
|
|
7
46
|
* @default "http://localhost:3000"
|
|
8
47
|
*/
|
|
9
48
|
baseUrl: string;
|
|
49
|
+
/**
|
|
50
|
+
* Multi-host / multi-app mapping dictionary for micro-frontends and multi-entrypoint monorepos.
|
|
51
|
+
* Maps named aliases to full base URLs (e.g. `{ sso: "https://sso.tagsamurai.local:5174", fam: "https://fam.tagsamurai.local:5175" }`).
|
|
52
|
+
* Allows tests to navigate using `Spectra.navigate('sso:/login')` or CLI `--host sso`.
|
|
53
|
+
*/
|
|
54
|
+
hosts?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* Array of development servers to auto-start and healthcheck before running tests.
|
|
57
|
+
*/
|
|
58
|
+
devServers?: DevServerConfig[];
|
|
59
|
+
/**
|
|
60
|
+
* Convenience single development server configuration.
|
|
61
|
+
*/
|
|
62
|
+
devServer?: DevServerConfig;
|
|
10
63
|
/**
|
|
11
64
|
* Maximum number of parallel browser sessions/workers spawned during test execution.
|
|
12
65
|
* Can be an explicit number, numeric string, or `"auto"`.
|
package/dist/config/schema.js
CHANGED
|
@@ -41,7 +41,8 @@ describe('Platform TSConfigs & Type Isolation (docs/v2/cli/tsconfig-platform-iso
|
|
|
41
41
|
expect(rootConfig.references).toEqual([{ path: './.testspectra/tsconfig.json' }]);
|
|
42
42
|
// Verify dynamic master solution references all 5 platform solution tsconfigs in .testspectra/tsconfig/
|
|
43
43
|
const solutionConfig = JSON.parse(fs.readFileSync(path.join(tmpDir, '.testspectra', 'tsconfig.json'), 'utf-8'));
|
|
44
|
-
expect(solutionConfig.compilerOptions.
|
|
44
|
+
expect(solutionConfig.compilerOptions.composite).toBe(true);
|
|
45
|
+
expect(solutionConfig.compilerOptions.noEmit).toBeUndefined();
|
|
45
46
|
expect(solutionConfig.references).toEqual([
|
|
46
47
|
{ path: './tsconfig/web.json' },
|
|
47
48
|
{ path: './tsconfig/mobile.json' },
|
|
@@ -383,4 +383,42 @@ async function verifyZeroImport() {
|
|
|
383
383
|
expect(fs.existsSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'))).toBe(false);
|
|
384
384
|
expect(fs.existsSync(path.join(tmpDir, '.testspectra', 'types', 'common.d.ts'))).toBe(false);
|
|
385
385
|
});
|
|
386
|
+
it('nx monorepo: does NOT treat feature library as shared library and discovers nested e2e subproject', () => {
|
|
387
|
+
// 1. Shared library with testspectra:shared
|
|
388
|
+
const sharedDir = path.join(tmpDir, 'shared', 'testing');
|
|
389
|
+
fs.mkdirSync(path.join(sharedDir, 'page-objects', 'SharedNav'), { recursive: true });
|
|
390
|
+
fs.writeFileSync(path.join(sharedDir, 'project.json'), JSON.stringify({ name: 'shared-testing', projectType: 'library', tags: ['testspectra:shared'] }));
|
|
391
|
+
fs.writeFileSync(path.join(sharedDir, 'page-objects', 'SharedNav', 'common.ts'), 'export default class SharedNav {}');
|
|
392
|
+
// 2. Nx Feature library (projectType: "library", standard Nx tags, NO testspectra tags)
|
|
393
|
+
const featureLibDir = path.join(tmpDir, 'packages', 'features', 'auth');
|
|
394
|
+
fs.mkdirSync(featureLibDir, { recursive: true });
|
|
395
|
+
fs.writeFileSync(path.join(featureLibDir, 'project.json'), JSON.stringify({
|
|
396
|
+
name: '@tagsamurai/feature-auth',
|
|
397
|
+
projectType: 'library',
|
|
398
|
+
sourceRoot: 'packages/features/auth',
|
|
399
|
+
targets: {},
|
|
400
|
+
tags: ['type:feature', 'scope:feature-auth'],
|
|
401
|
+
}));
|
|
402
|
+
// 3. Nested E2E subproject inside feature library
|
|
403
|
+
const featureE2eDir = path.join(featureLibDir, 'e2e');
|
|
404
|
+
fs.mkdirSync(path.join(featureE2eDir, 'specs', 'AuthSuite', 'TC-001'), { recursive: true });
|
|
405
|
+
fs.writeFileSync(path.join(featureE2eDir, 'project.json'), JSON.stringify({
|
|
406
|
+
name: 'feature-auth-e2e',
|
|
407
|
+
projectType: 'application',
|
|
408
|
+
tags: ['testspectra:e2e', 'testspectra:scope:feature-auth'],
|
|
409
|
+
}));
|
|
410
|
+
fs.writeFileSync(path.join(featureE2eDir, 'specs', 'AuthSuite', 'TC-001', 'web.test.ts'), 'it("auth test", async () => {});');
|
|
411
|
+
const scopes = TypeGenerator.getEntityScopes(tmpDir);
|
|
412
|
+
expect(scopes).toHaveLength(2);
|
|
413
|
+
const sharedScope = scopes.find((s) => s.isShared);
|
|
414
|
+
expect(sharedScope).toBeDefined();
|
|
415
|
+
expect(sharedScope?.name).toBe('shared');
|
|
416
|
+
expect(sharedScope?.dir).toBe(path.resolve(sharedDir));
|
|
417
|
+
const featureScope = scopes.find((s) => !s.isShared);
|
|
418
|
+
expect(featureScope).toBeDefined();
|
|
419
|
+
expect(featureScope?.name).toBe('feature-auth');
|
|
420
|
+
expect(featureScope?.dir).toBe(path.resolve(featureE2eDir));
|
|
421
|
+
// Verify packages/features/auth is NOT in scopes
|
|
422
|
+
expect(scopes.some((s) => s.dir === path.resolve(featureLibDir))).toBe(false);
|
|
423
|
+
});
|
|
386
424
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ArchitectureDocResult {
|
|
2
|
+
mode: 'nx' | 'default';
|
|
3
|
+
path: string;
|
|
4
|
+
sharedTestingRelPath?: string;
|
|
5
|
+
featureCount?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildFeatureModulesSection(cwd: string, featureE2eDirs: string[]): string;
|
|
8
|
+
/**
|
|
9
|
+
* (Re)generates `ARCHITECTURE.md` from the current on-disk workspace state — no prompts, no other
|
|
10
|
+
* scaffolding side effects. Nx/monorepo mode is auto-detected via `TsConfigGenerator.getEntityScopes`
|
|
11
|
+
* (the same shared/feature discovery `sync-types` already relies on), so this stays accurate after
|
|
12
|
+
* features are added/removed/renamed without needing to re-run `spectra init`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateArchitectureDoc(cwd: string): ArchitectureDocResult;
|