@testspectra/cli 1.1.8-rc.24 → 1.1.8-rc.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/bin/.testspectra-runner.hash +1 -0
- package/dist/commands/__tests__/doctor-filter.test.js +37 -0
- package/dist/commands/__tests__/doctor-scope.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor-scope.test.js +24 -0
- package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
- package/dist/commands/__tests__/test-error-streaming.test.js +52 -0
- package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
- package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +95 -0
- package/dist/commands/add.js +3 -3
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +16 -3
- package/dist/commands/install.js +3 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.js +114 -22
- package/dist/commands/upgrade.d.ts +8 -1
- package/dist/commands/upgrade.js +63 -13
- package/dist/generator/architecture-doc-generator.js +1 -3
- package/dist/index.js +30 -15
- package/dist/linter/discovery.js +2 -2
- package/dist/linter/schemas/spec.schema.d.ts +6 -6
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +45 -16
- package/dist/runner/reporter.js +11 -3
- package/dist/utils/background-update-check.js +1 -8
- package/dist/utils/dependency-updates.d.ts +15 -0
- package/dist/utils/dependency-updates.js +107 -0
- package/package.json +8 -9
- package/templates/default/package.json +2 -2
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +983 -6269
- package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
- package/templates/nx/.nx/workspace-data/file-map.json +20 -34
- package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/project-graph.json +3 -1
- package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
- package/templates/nx/package.json +2 -2
- package/templates/react-component/package.json +3 -3
- package/templates/react-component/spectra.config.ts +0 -10
- package/dist/.tsbuildinfo +0 -1
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.js +0 -109
- package/dist/lifecycle/__tests__/lifecycle-engine.test.js +0 -290
- package/dist/lifecycle/hook-discovery.d.ts +0 -24
- package/dist/lifecycle/hook-discovery.js +0 -60
- package/dist/lifecycle/hook-dispatcher.d.ts +0 -34
- package/dist/lifecycle/hook-dispatcher.js +0 -190
- package/dist/lifecycle/index.d.ts +0 -3
- package/dist/lifecycle/index.js +0 -3
- package/dist/lifecycle/parallel-grouping.d.ts +0 -27
- package/dist/lifecycle/parallel-grouping.js +0 -151
- /package/dist/{lifecycle/__tests__/lifecycle-engine.test.d.ts → commands/__tests__/doctor-filter.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -44,31 +44,34 @@ portable JS, so one npm package can't ship a single binary that works on every O
|
|
|
44
44
|
`@testspectra/cli` follows the same pattern as esbuild/swc/turbo:
|
|
45
45
|
|
|
46
46
|
- `@testspectra/cli` itself ships **no native binary** — only the JS launcher (`bin/spectra.js`).
|
|
47
|
-
- The binary is published as
|
|
47
|
+
- The binary is published as three separate, platform-locked packages under `cli/npm/`:
|
|
48
48
|
- `@testspectra/cli-darwin-arm64`
|
|
49
49
|
- `@testspectra/cli-win32-x64`
|
|
50
|
-
- `@testspectra/cli-win32-arm64`
|
|
51
50
|
- `@testspectra/cli-linux-x64`
|
|
52
51
|
|
|
52
|
+
(Windows arm64 has no native build — `resolveBinaryPath()` throws a clear "not supported"
|
|
53
|
+
error there instead of pointing at a package that was never published.)
|
|
54
|
+
|
|
53
55
|
Each has `os`/`cpu` fields in its `package.json`, so npm/pnpm/yarn only ever download the one
|
|
54
56
|
matching the installing machine.
|
|
55
|
-
|
|
57
|
+
|
|
58
|
+
- `@testspectra/cli` lists all three as `optionalDependencies`, pinned to an exact version (not
|
|
56
59
|
`^`) that always matches the `@testspectra/cli` release it ships with.
|
|
57
60
|
- At runtime, `RustCoreBridge.resolveBinaryPath()` (`cli/src/runner/bridge.ts`) resolves the
|
|
58
61
|
binary from whichever platform package got installed (`require.resolve('@testspectra/cli-<platform>/package.json')`),
|
|
59
62
|
falling back to the monorepo's local build output (`core/target/release/...`) for local dev
|
|
60
63
|
where no optional package is installed.
|
|
61
|
-
- If a user's platform/arch isn't one of the
|
|
64
|
+
- If a user's platform/arch isn't one of the three above, install succeeds (optional deps just
|
|
62
65
|
get skipped) but `resolveBinaryPath()` throws a clear "not supported" error at run time.
|
|
63
66
|
|
|
64
67
|
### Building & releasing all platforms
|
|
65
68
|
|
|
66
|
-
`scripts/build-cross-platform-binaries.js` builds the binary for all
|
|
69
|
+
`scripts/build-cross-platform-binaries.js` builds the binary for all three platforms from a
|
|
67
70
|
single macOS machine and copies each into `cli/npm/<platform>/bin/`:
|
|
68
71
|
|
|
69
72
|
- **macOS arm64** — native `cargo build` (this machine only builds arm64; there's no x64 macOS
|
|
70
73
|
package).
|
|
71
|
-
- **Windows x64
|
|
74
|
+
- **Windows x64** — cross-compiled via [`cargo-xwin`](https://github.com/rust-cross/cargo-xwin)
|
|
72
75
|
(`cargo install cargo-xwin`), which downloads the MSVC CRT/Windows SDK bits needed for linking.
|
|
73
76
|
reqwest's default `native-tls` backend uses Schannel on Windows, so no OpenSSL cross-compile
|
|
74
77
|
headaches here.
|
|
@@ -79,8 +82,8 @@ single macOS machine and copies each into `cli/npm/<platform>/bin/`:
|
|
|
79
82
|
Docker must be running for this step.
|
|
80
83
|
|
|
81
84
|
`scripts/release-cli-local.sh` runs this automatically, then stamps every `cli/npm/*/package.json`
|
|
82
|
-
and `cli/package.json`'s `optionalDependencies` with the target version, packs all
|
|
83
|
-
and (with `--publish`) publishes the
|
|
85
|
+
and `cli/package.json`'s `optionalDependencies` with the target version, packs all four tarballs,
|
|
86
|
+
and (with `--publish`) publishes the three platform packages before `@testspectra/cli` itself.
|
|
84
87
|
|
|
85
88
|
---
|
|
86
89
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2db95c618fc201b3c5e4b2f56e93c5e5fca36ba0d31250c65c98aabed5385361
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
const { execFileSyncMock } = vi.hoisted(() => ({ execFileSyncMock: vi.fn() }));
|
|
3
|
+
vi.mock('child_process', () => ({
|
|
4
|
+
execFileSync: execFileSyncMock,
|
|
5
|
+
spawn: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../../runner/bridge.js', () => ({
|
|
8
|
+
RustCoreBridge: { resolveBinaryPath: () => '/fake/testspectra-runner' },
|
|
9
|
+
}));
|
|
10
|
+
import { runSystemChecks } from '../doctor.js';
|
|
11
|
+
const EMPTY = JSON.stringify({ all_ready: true, missing_count: 0, dependencies: [] });
|
|
12
|
+
describe('runSystemChecks filter forwarding', () => {
|
|
13
|
+
it('forwards a category scope to the runner', async () => {
|
|
14
|
+
execFileSyncMock.mockReturnValueOnce(EMPTY);
|
|
15
|
+
await runSystemChecks('/tmp/ws', { scope: 'web' });
|
|
16
|
+
const [, args] = execFileSyncMock.mock.calls.at(-1);
|
|
17
|
+
expect(args).toEqual(['doctor', '--json', '--scope', 'web']);
|
|
18
|
+
});
|
|
19
|
+
it('forwards a fine-grained --only filter (single dependency)', async () => {
|
|
20
|
+
execFileSyncMock.mockReturnValueOnce(EMPTY);
|
|
21
|
+
await runSystemChecks('/tmp/ws', { only: 'chrome' });
|
|
22
|
+
const [, args] = execFileSyncMock.mock.calls.at(-1);
|
|
23
|
+
expect(args).toEqual(['doctor', '--json', '--only', 'chrome']);
|
|
24
|
+
});
|
|
25
|
+
it('forwards both scope and only when provided', async () => {
|
|
26
|
+
execFileSyncMock.mockReturnValueOnce(EMPTY);
|
|
27
|
+
await runSystemChecks('/tmp/ws', { scope: 'web', only: 'chrome,adb' });
|
|
28
|
+
const [, args] = execFileSyncMock.mock.calls.at(-1);
|
|
29
|
+
expect(args).toEqual(['doctor', '--json', '--scope', 'web', '--only', 'chrome,adb']);
|
|
30
|
+
});
|
|
31
|
+
it('omits filters entirely when none are requested (full doctor)', async () => {
|
|
32
|
+
execFileSyncMock.mockReturnValueOnce(EMPTY);
|
|
33
|
+
await runSystemChecks('/tmp/ws');
|
|
34
|
+
const [, args] = execFileSyncMock.mock.calls.at(-1);
|
|
35
|
+
expect(args).toEqual(['doctor', '--json']);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
const { execFileSyncMock } = vi.hoisted(() => ({ execFileSyncMock: vi.fn() }));
|
|
3
|
+
vi.mock('child_process', () => ({
|
|
4
|
+
execFileSync: execFileSyncMock,
|
|
5
|
+
spawn: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../../runner/bridge.js', () => ({
|
|
8
|
+
RustCoreBridge: { resolveBinaryPath: () => '/fake/testspectra-runner' },
|
|
9
|
+
}));
|
|
10
|
+
import { runSystemChecks } from '../doctor.js';
|
|
11
|
+
describe('runSystemChecks scope forwarding', () => {
|
|
12
|
+
it('forwards a category scope to the runner (efficient partial doctor)', async () => {
|
|
13
|
+
execFileSyncMock.mockReturnValueOnce(JSON.stringify({ all_ready: true, missing_count: 0, dependencies: [] }));
|
|
14
|
+
await runSystemChecks('/tmp/ws', 'web');
|
|
15
|
+
const [, args] = execFileSyncMock.mock.calls.at(-1);
|
|
16
|
+
expect(args).toEqual(['doctor', '--json', '--scope', 'web']);
|
|
17
|
+
});
|
|
18
|
+
it('omits --scope entirely when no scope is requested (full doctor)', async () => {
|
|
19
|
+
execFileSyncMock.mockReturnValueOnce(JSON.stringify({ all_ready: true, missing_count: 0, dependencies: [] }));
|
|
20
|
+
await runSystemChecks('/tmp/ws');
|
|
21
|
+
const [, args] = execFileSyncMock.mock.calls.at(-1);
|
|
22
|
+
expect(args).toEqual(['doctor', '--json']);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { isVitestNoise, sanitizeErrorMessage } from '@testspectra/react';
|
|
3
|
+
import { Reporter } from '../../runner/reporter.js';
|
|
4
|
+
describe('spectra test error streaming and formatting', () => {
|
|
5
|
+
it('sanitizes Vitest/Vite error messages without leaking behind-the-scenes tooling', () => {
|
|
6
|
+
const rawError = 'Failed to fetch dynamically imported module: http://localhost:63315/@fs/D:/wangs-ui-react/packages/testing-library/page-objects/AccordionComponent/web.ts?import';
|
|
7
|
+
const sanitized = sanitizeErrorMessage(rawError);
|
|
8
|
+
expect(sanitized).toBe('Failed to load module: D:/wangs-ui-react/packages/testing-library/page-objects/AccordionComponent/web.ts');
|
|
9
|
+
expect(sanitized).not.toContain('localhost');
|
|
10
|
+
expect(sanitized).not.toContain('@fs');
|
|
11
|
+
expect(sanitized).not.toContain('?import');
|
|
12
|
+
});
|
|
13
|
+
it('filters Vitest CLI runner noise while allowing actual errors through', () => {
|
|
14
|
+
expect(isVitestNoise('RUN v2.1.8 /Volumes/Home/TestSpectra')).toBe(true);
|
|
15
|
+
expect(isVitestNoise('Test Files 1 failed (1)')).toBe(true);
|
|
16
|
+
expect(isVitestNoise('⎯⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯')).toBe(true);
|
|
17
|
+
expect(isVitestNoise('Re-optimizing dependencies because vite config has changed')).toBe(true);
|
|
18
|
+
expect(isVitestNoise('Error: Cannot find module "./Button"')).toBe(false);
|
|
19
|
+
expect(isVitestNoise('SyntaxError: Unexpected token (10:15)')).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('formats multi-line errors with clean indentation in Reporter', () => {
|
|
22
|
+
const reporter = new Reporter();
|
|
23
|
+
reporter.addLog({
|
|
24
|
+
timestamp: '12:00:00',
|
|
25
|
+
level: 'INFO',
|
|
26
|
+
message: '[TESTSPECTRA_TEST_START] {"title":"specs/Accordion/TC-0009/web.test.tsx"}',
|
|
27
|
+
});
|
|
28
|
+
reporter.addLog({
|
|
29
|
+
timestamp: '12:00:00',
|
|
30
|
+
level: 'INFO',
|
|
31
|
+
message: '[TESTSPECTRA_TEST_ERROR] Failed to load module: D:/wangs-ui-react/page-objects/Accordion/web.ts',
|
|
32
|
+
});
|
|
33
|
+
reporter.addLog({
|
|
34
|
+
timestamp: '12:00:00',
|
|
35
|
+
level: 'INFO',
|
|
36
|
+
message: '[TESTSPECTRA_TEST_FAIL] specs/Accordion/TC-0009/web.test.tsx (0ms)',
|
|
37
|
+
});
|
|
38
|
+
const result = reporter.generateResult('failed', '0.1s');
|
|
39
|
+
expect(result.failedCount).toBe(1);
|
|
40
|
+
expect(result.passedCount).toBe(0);
|
|
41
|
+
expect(result.status).toBe('failed');
|
|
42
|
+
});
|
|
43
|
+
it('guarantees failedCount is at least 1 when test run fails without test events', () => {
|
|
44
|
+
const reporter = new Reporter();
|
|
45
|
+
const ok = false;
|
|
46
|
+
const result = reporter.generateResult(ok ? 'passed' : 'failed', '0.1s');
|
|
47
|
+
const failedCount = !ok && result.failedCount === 0 ? 1 : result.failedCount;
|
|
48
|
+
const totalTests = result.passedCount + failedCount;
|
|
49
|
+
expect(failedCount).toBe(1);
|
|
50
|
+
expect(totalTests).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { ensureTestSpectraScaffolding } from '../test.js';
|
|
6
|
+
describe('spectra test pre-flight scaffolding (docs/v2/features/component-testing/react/README.md)', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-test-scaffold-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
try {
|
|
13
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
});
|
|
17
|
+
it('generates the .testspectra solution tsconfig when it is missing (clean clone / CI)', () => {
|
|
18
|
+
const masterConfig = path.join(tmpDir, '.testspectra', 'tsconfig.json');
|
|
19
|
+
expect(fs.existsSync(masterConfig)).toBe(false);
|
|
20
|
+
ensureTestSpectraScaffolding(tmpDir);
|
|
21
|
+
// The exact file `vite:esbuild` tries to read via the root tsconfig's `references` entry.
|
|
22
|
+
expect(fs.existsSync(masterConfig)).toBe(true);
|
|
23
|
+
expect(fs.existsSync(path.join(tmpDir, '.testspectra', 'tsconfig', 'web.json'))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('leaves an existing scaffolding untouched', () => {
|
|
26
|
+
const masterConfig = path.join(tmpDir, '.testspectra', 'tsconfig.json');
|
|
27
|
+
fs.mkdirSync(path.dirname(masterConfig), { recursive: true });
|
|
28
|
+
fs.writeFileSync(masterConfig, '{"custom":true}\n', 'utf-8');
|
|
29
|
+
ensureTestSpectraScaffolding(tmpDir);
|
|
30
|
+
expect(fs.readFileSync(masterConfig, 'utf-8')).toBe('{"custom":true}\n');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execFileSync, execSync } from 'child_process';
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { upgradeCommand } from '../upgrade.js';
|
|
7
|
+
const DOCTOR_JSON = JSON.stringify({
|
|
8
|
+
all_ready: true,
|
|
9
|
+
missing_count: 0,
|
|
10
|
+
dependencies: [
|
|
11
|
+
{
|
|
12
|
+
name: 'Google Chrome',
|
|
13
|
+
category: 'web',
|
|
14
|
+
installed: true,
|
|
15
|
+
version: 'Google Chrome 123.0.0.0 (Chrome for Testing)',
|
|
16
|
+
required: true,
|
|
17
|
+
required_version: 'Stable / CfT',
|
|
18
|
+
depends_on: [],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'Bun',
|
|
22
|
+
category: 'core',
|
|
23
|
+
installed: true,
|
|
24
|
+
version: 'v1.0.0',
|
|
25
|
+
required: true,
|
|
26
|
+
required_version: '>=1.0.0',
|
|
27
|
+
depends_on: [],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
vi.mock('child_process', () => ({
|
|
32
|
+
execFileSync: vi.fn((_cmd, args) => Array.isArray(args) && args[0] === 'doctor' ? DOCTOR_JSON : '"9.9.9"'),
|
|
33
|
+
execSync: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
describe('upgradeCommand self-upgrade mechanism (docs/v2/features/upgrade-mechanism-and-notifications.md)', () => {
|
|
36
|
+
let tmpDir;
|
|
37
|
+
const originalCwd = process.cwd();
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-upgrade-test-'));
|
|
40
|
+
process.chdir(tmpDir);
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
vi.stubGlobal('fetch', vi.fn(async (url) => {
|
|
43
|
+
const href = String(url);
|
|
44
|
+
if (href.includes('chrome-for-testing')) {
|
|
45
|
+
return { ok: true, json: async () => ({ channels: { Stable: { version: '999.0.0.0' } } }) };
|
|
46
|
+
}
|
|
47
|
+
if (href.includes('github')) {
|
|
48
|
+
return { ok: true, json: async () => ({ tag_name: 'bun-v2.0.0' }) };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
text: async () => '<remotePackage path="platform-tools"><revision><major>99</major><minor>0</minor><micro>0</micro></revision></remotePackage>',
|
|
53
|
+
};
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
process.chdir(originalCwd);
|
|
59
|
+
try {
|
|
60
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
});
|
|
64
|
+
it('emits a pure JSON payload covering packages and drivers in --json mode', async () => {
|
|
65
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
66
|
+
await upgradeCommand([], { json: true });
|
|
67
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
68
|
+
const payload = JSON.parse(logSpy.mock.calls[0][0]);
|
|
69
|
+
expect(payload.packages).toHaveLength(3);
|
|
70
|
+
expect(payload.packages.every((p) => p.latest === '9.9.9')).toBe(true);
|
|
71
|
+
expect(payload.drivers).toHaveLength(2);
|
|
72
|
+
expect(payload.drivers.find((d) => d.name === 'Google Chrome')?.latest).toBe('999.0.0.0');
|
|
73
|
+
expect(payload.updatesAvailable).toBeGreaterThan(0);
|
|
74
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
75
|
+
logSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
it('restricts to a single package (no drivers) when package names are passed', async () => {
|
|
78
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
79
|
+
await upgradeCommand(['@testspectra/cli'], { json: true });
|
|
80
|
+
const payload = JSON.parse(logSpy.mock.calls[0][0]);
|
|
81
|
+
expect(payload.packages).toHaveLength(1);
|
|
82
|
+
expect(payload.packages[0].pkg).toBe('@testspectra/cli');
|
|
83
|
+
expect(payload.drivers).toHaveLength(0);
|
|
84
|
+
logSpy.mockRestore();
|
|
85
|
+
});
|
|
86
|
+
it('reports available updates in --check mode without running the package manager', async () => {
|
|
87
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
88
|
+
await upgradeCommand([], { check: true });
|
|
89
|
+
expect(execFileSync).toHaveBeenCalled();
|
|
90
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
91
|
+
const output = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
92
|
+
expect(output).toContain('package update(s) available');
|
|
93
|
+
logSpy.mockRestore();
|
|
94
|
+
});
|
|
95
|
+
});
|
package/dist/commands/add.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { TypeGenerator } from '../types/generator.js';
|
|
7
|
+
import { ConfigLoader } from '../config/loader.js';
|
|
7
8
|
function parsePnpmWorkspaceGlobs(content) {
|
|
8
9
|
const lines = content.split('\n');
|
|
9
10
|
const globs = [];
|
|
@@ -107,9 +108,8 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
107
108
|
let rootDir = cwd;
|
|
108
109
|
let cur = cwd;
|
|
109
110
|
while (cur !== path.dirname(cur)) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
fs.existsSync(path.join(cur, 'nx.json'))) {
|
|
111
|
+
const hasConfig = ConfigLoader.CONFIG_FILE_NAMES.some((name) => fs.existsSync(path.join(cur, name)));
|
|
112
|
+
if (hasConfig || fs.existsSync(path.join(cur, 'pnpm-workspace.yaml')) || fs.existsSync(path.join(cur, 'nx.json'))) {
|
|
113
113
|
rootDir = cur;
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
@@ -13,9 +13,17 @@ export interface SystemCheckResult {
|
|
|
13
13
|
missing_count: number;
|
|
14
14
|
dependencies: DependencyStatus[];
|
|
15
15
|
}
|
|
16
|
-
export
|
|
16
|
+
export interface SystemCheckOptions {
|
|
17
|
+
/** Category filter: `core`, `web`, or `mobile` (default: all). */
|
|
18
|
+
scope?: string;
|
|
19
|
+
/** Comma-separated specific dependency keys, e.g. `chrome` or `chrome,adb` (takes precedence over `scope`). */
|
|
20
|
+
only?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function runSystemChecks(cwd?: string, options?: SystemCheckOptions): Promise<SystemCheckResult>;
|
|
17
23
|
export declare function doctorCommand(options?: {
|
|
18
24
|
fix?: boolean;
|
|
19
25
|
json?: boolean;
|
|
20
26
|
verbose?: boolean;
|
|
27
|
+
scope?: string;
|
|
28
|
+
only?: string;
|
|
21
29
|
}): Promise<void>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -9,9 +9,16 @@ function getRunnerEnv() {
|
|
|
9
9
|
TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
export async function runSystemChecks(cwd = process.cwd()) {
|
|
12
|
+
export async function runSystemChecks(cwd = process.cwd(), options = {}) {
|
|
13
13
|
const binPath = RustCoreBridge.resolveBinaryPath();
|
|
14
|
-
const
|
|
14
|
+
const args = ['doctor', '--json'];
|
|
15
|
+
if (options.scope) {
|
|
16
|
+
args.push('--scope', options.scope);
|
|
17
|
+
}
|
|
18
|
+
if (options.only) {
|
|
19
|
+
args.push('--only', options.only);
|
|
20
|
+
}
|
|
21
|
+
const stdout = execFileSync(binPath, args, {
|
|
15
22
|
cwd,
|
|
16
23
|
env: getRunnerEnv(),
|
|
17
24
|
encoding: 'utf8',
|
|
@@ -29,11 +36,17 @@ export async function doctorCommand(options = {}) {
|
|
|
29
36
|
if (options.verbose) {
|
|
30
37
|
args.push('--verbose');
|
|
31
38
|
}
|
|
39
|
+
if (options.scope) {
|
|
40
|
+
args.push('--scope', options.scope);
|
|
41
|
+
}
|
|
42
|
+
if (options.only) {
|
|
43
|
+
args.push('--only', options.only);
|
|
44
|
+
}
|
|
32
45
|
if (options.fix) {
|
|
33
46
|
args.push('--fix');
|
|
34
47
|
}
|
|
35
48
|
if (options.json) {
|
|
36
|
-
const result = await runSystemChecks(cwd);
|
|
49
|
+
const result = await runSystemChecks(cwd, { scope: options.scope, only: options.only });
|
|
37
50
|
console.log(JSON.stringify(result, null, 2));
|
|
38
51
|
return;
|
|
39
52
|
}
|
package/dist/commands/install.js
CHANGED
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,7 +35,9 @@ import { runSystemChecks } from './doctor.js';
|
|
|
14
35
|
*/
|
|
15
36
|
async function resolveBrowserExecutable(cwd) {
|
|
16
37
|
try {
|
|
17
|
-
|
|
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' });
|
|
18
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
|
}
|
|
@@ -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,33 +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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
185
|
const durationMs = Date.now() - runStartedAt;
|
|
95
186
|
const result = reporter.generateResult(ok ? 'passed' : 'failed', `${(durationMs / 1000).toFixed(1)}s`);
|
|
96
|
-
const
|
|
187
|
+
const failedCount = !ok && result.failedCount === 0 ? 1 : result.failedCount;
|
|
188
|
+
const totalTests = result.passedCount + failedCount;
|
|
97
189
|
renderSummaryView({
|
|
98
190
|
totalSuites: totalTests,
|
|
99
191
|
totalTests,
|
|
100
192
|
passedCount: result.passedCount,
|
|
101
|
-
failedCount
|
|
193
|
+
failedCount,
|
|
102
194
|
durationMs,
|
|
103
195
|
reportLocation: outputFile ? path.relative(cwd, outputFile) : '(use -o/--output to save a report)',
|
|
104
196
|
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
export
|
|
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?: {
|
|
2
8
|
check?: boolean;
|
|
3
9
|
packagesOnly?: boolean;
|
|
4
10
|
driversOnly?: boolean;
|
|
11
|
+
json?: boolean;
|
|
5
12
|
}): Promise<void>;
|