@testspectra/cli 1.1.8-rc.9 → 1.1.11-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +11 -8
  2. package/bin/.testspectra-runner.hash +1 -0
  3. package/dist/commands/__tests__/doctor-filter.test.js +37 -0
  4. package/dist/commands/__tests__/doctor-scope.test.d.ts +1 -0
  5. package/dist/commands/__tests__/doctor-scope.test.js +24 -0
  6. package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
  7. package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
  8. package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
  9. package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
  10. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  11. package/dist/commands/__tests__/upgrade.test.js +95 -0
  12. package/dist/commands/add.js +3 -3
  13. package/dist/commands/devices.js +1 -0
  14. package/dist/commands/docs.d.ts +4 -0
  15. package/dist/commands/docs.js +17 -0
  16. package/dist/commands/doctor.d.ts +9 -1
  17. package/dist/commands/doctor.js +17 -3
  18. package/dist/commands/init.js +5 -159
  19. package/dist/commands/install.js +5 -0
  20. package/dist/commands/license.js +1 -0
  21. package/dist/commands/run.d.ts +2 -0
  22. package/dist/commands/run.js +22 -0
  23. package/dist/commands/test.d.ts +13 -0
  24. package/dist/commands/test.js +115 -21
  25. package/dist/commands/upgrade.d.ts +12 -0
  26. package/dist/commands/upgrade.js +197 -0
  27. package/dist/config/schema.d.ts +53 -0
  28. package/dist/config/schema.js +2 -0
  29. package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
  30. package/dist/generator/architecture-doc-generator.d.ts +14 -0
  31. package/dist/generator/architecture-doc-generator.js +80 -0
  32. package/dist/generator/index.d.ts +1 -0
  33. package/dist/generator/index.js +1 -0
  34. package/dist/generator/tsconfig-generator.js +1 -1
  35. package/dist/index.js +59 -16
  36. package/dist/linter/discovery.js +2 -2
  37. package/dist/linter/schemas/spec.schema.d.ts +6 -6
  38. package/dist/runner/bridge.d.ts +1 -1
  39. package/dist/runner/bridge.js +58 -16
  40. package/dist/runner/reporter.d.ts +1 -0
  41. package/dist/runner/reporter.js +12 -6
  42. package/dist/utils/__tests__/dev-server-manager.test.d.ts +1 -0
  43. package/dist/utils/__tests__/dev-server-manager.test.js +72 -0
  44. package/dist/utils/background-update-check.d.ts +1 -0
  45. package/dist/utils/background-update-check.js +43 -0
  46. package/dist/utils/dependency-updates.d.ts +15 -0
  47. package/dist/utils/dependency-updates.js +107 -0
  48. package/dist/utils/dev-server-manager.d.ts +40 -0
  49. package/dist/utils/dev-server-manager.js +257 -0
  50. package/dist/utils/update-notifier.d.ts +2 -0
  51. package/dist/utils/update-notifier.js +67 -0
  52. package/package.json +10 -9
  53. package/templates/default/package.json +2 -2
  54. package/templates/docs/ARCHITECTURE.default.md +26 -0
  55. package/templates/docs/ARCHITECTURE.nx.md +147 -0
  56. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  57. package/templates/nx/.nx/workspace-data/d/daemon.log +1012 -5748
  58. package/templates/nx/.nx/workspace-data/file-map.json +171 -185
  59. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  60. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  61. package/templates/nx/.nx/workspace-data/project-graph.json +3 -1
  62. package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
  63. package/templates/nx/package.json +3 -3
  64. package/templates/nx/pnpm-lock.yaml +191 -214
  65. package/templates/nx/spectra.config.ts +13 -0
  66. package/templates/react-component/package.json +3 -3
  67. package/templates/react-component/page-objects/BadgeComponent/web.ts +4 -4
  68. package/templates/react-component/page-objects/ButtonComponent/web.ts +3 -3
  69. package/templates/react-component/page-objects/InputComponent/web.ts +1 -1
  70. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +0 -3
  71. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +0 -9
  72. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +1 -1
  73. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +4 -8
  74. package/templates/react-component/spectra.config.ts +0 -10
  75. package/templates/react-component/src/components/Badge/Badge.tsx +3 -3
  76. package/templates/react-component/src/components/Button/Button.tsx +5 -3
  77. package/templates/react-component/src/components/Input/Input.tsx +2 -3
  78. package/dist/.tsbuildinfo +0 -1
  79. package/dist/commands/update.d.ts +0 -3
  80. package/dist/commands/update.js +0 -109
  81. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +0 -290
  82. package/dist/lifecycle/hook-discovery.d.ts +0 -24
  83. package/dist/lifecycle/hook-discovery.js +0 -60
  84. package/dist/lifecycle/hook-dispatcher.d.ts +0 -34
  85. package/dist/lifecycle/hook-dispatcher.js +0 -190
  86. package/dist/lifecycle/index.d.ts +0 -3
  87. package/dist/lifecycle/index.js +0 -3
  88. package/dist/lifecycle/parallel-grouping.d.ts +0 -27
  89. package/dist/lifecycle/parallel-grouping.js +0 -151
  90. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  91. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  92. package/templates/nx/.nx/workspace-data/d/server-process.json +0 -3
  93. /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 four separate, platform-locked packages under `cli/npm/`:
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
- - `@testspectra/cli` lists all four as `optionalDependencies`, pinned to an exact version (not
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 four above, install succeeds (optional deps just
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 four platforms from a
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 / arm64** — cross-compiled via [`cargo-xwin`](https://github.com/rust-cross/cargo-xwin)
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 five tarballs,
83
- and (with `--publish`) publishes the four platform packages before `@testspectra/cli` itself.
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,85 @@
1
+ import { describe, expect, it, vi } 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('prints error message below test title and steps on failure', () => {
44
+ const reporter = new Reporter();
45
+ const consoleLogs = [];
46
+ const spy = vi.spyOn(console, 'log').mockImplementation((msg) => {
47
+ if (typeof msg === 'string')
48
+ consoleLogs.push(msg);
49
+ });
50
+ reporter.addLog({
51
+ timestamp: '12:00:00',
52
+ level: 'INFO',
53
+ message: '[TESTSPECTRA_TEST_START] {"title":"ApiInterception / TC-0009-post-mocking"}',
54
+ });
55
+ reporter.addLog({
56
+ timestamp: '12:00:00',
57
+ level: 'INFO',
58
+ message: '[TESTSPECTRA_STEP_PASS] {"type":"action","key":"navigate","args":["/#api-post"],"test":"ApiInterception / TC-0009-post-mocking"} (8ms)',
59
+ });
60
+ reporter.addLog({
61
+ timestamp: '12:00:00',
62
+ level: 'INFO',
63
+ message: '[TESTSPECTRA_STEP_FAIL] {"type":"assertion","target":"#post-status-badge","args":["201 Created"],"test":"ApiInterception / TC-0009-post-mocking"} (2005ms)',
64
+ });
65
+ reporter.addLog({
66
+ timestamp: '12:00:00',
67
+ level: 'INFO',
68
+ message: '[TESTSPECTRA_TEST_ERROR] Expected element "#post-status-badge" to have text "201 Created", but got ""',
69
+ });
70
+ reporter.addLog({
71
+ timestamp: '12:00:00',
72
+ level: 'INFO',
73
+ message: '[TESTSPECTRA_TEST_FAIL] ApiInterception / TC-0009-post-mocking (2066ms)',
74
+ });
75
+ spy.mockRestore();
76
+ const titleIndex = consoleLogs.findIndex((log) => log.includes('ApiInterception / TC-0009-post-mocking'));
77
+ const stepPassIndex = consoleLogs.findIndex((log) => log.includes('Navigate to'));
78
+ const stepFailIndex = consoleLogs.findIndex((log) => log.includes('#post-status-badge'));
79
+ const errorIndex = consoleLogs.findIndex((log) => log.includes('↳ Error:'));
80
+ expect(titleIndex).toBeGreaterThanOrEqual(0);
81
+ expect(stepPassIndex).toBeGreaterThan(titleIndex);
82
+ expect(stepFailIndex).toBeGreaterThan(stepPassIndex);
83
+ expect(errorIndex).toBeGreaterThan(stepFailIndex);
84
+ });
85
+ });
@@ -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
+ });
@@ -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
- if (fs.existsSync(path.join(cur, 'spectra.config.ts')) ||
111
- fs.existsSync(path.join(cur, 'pnpm-workspace.yaml')) ||
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
  }
@@ -32,6 +32,7 @@ export async function devicesCommand(options = {}) {
32
32
  cwd,
33
33
  env: getRunnerEnv(),
34
34
  stdio: 'inherit',
35
+ windowsHide: true,
35
36
  });
36
37
  child.on('close', (code) => {
37
38
  if (code === 0) {
@@ -0,0 +1,4 @@
1
+ export interface DocsCommandOptions {
2
+ cwd?: string;
3
+ }
4
+ export declare function docsUpdateCommand(options?: DocsCommandOptions): void;
@@ -0,0 +1,17 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import * as p from '@clack/prompts';
4
+ import { generateArchitectureDoc } from '../generator/architecture-doc-generator.js';
5
+ function resolveBaseDir(options = {}) {
6
+ return options.cwd ? path.resolve(options.cwd) : process.cwd();
7
+ }
8
+ export function docsUpdateCommand(options = {}) {
9
+ const cwd = resolveBaseDir(options);
10
+ const result = generateArchitectureDoc(cwd);
11
+ if (result.mode === 'nx') {
12
+ p.log.success(chalk.green(`ARCHITECTURE.md regenerated (Nx/monorepo mode, ${result.featureCount ?? 0} feature module(s) detected).`));
13
+ }
14
+ else {
15
+ p.log.success(chalk.green('ARCHITECTURE.md regenerated (standalone mode).'));
16
+ }
17
+ }
@@ -13,9 +13,17 @@ export interface SystemCheckResult {
13
13
  missing_count: number;
14
14
  dependencies: DependencyStatus[];
15
15
  }
16
- export declare function runSystemChecks(cwd?: string): Promise<SystemCheckResult>;
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>;
@@ -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 stdout = execFileSync(binPath, ['doctor', '--json'], {
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
  }
@@ -42,6 +55,7 @@ export async function doctorCommand(options = {}) {
42
55
  cwd,
43
56
  env: getRunnerEnv(),
44
57
  stdio: 'inherit',
58
+ windowsHide: true,
45
59
  });
46
60
  child.on('close', (code) => {
47
61
  if (code === 0 || options.fix) {
@@ -6,6 +6,7 @@ import chalk from 'chalk';
6
6
  import { ConfigLoader } from '../config/loader.js';
7
7
  import { TypeGenerator } from '../types/generator.js';
8
8
  import { TsConfigGenerator } from '../generator/tsconfig-generator.js';
9
+ import { generateArchitectureDoc } from '../generator/architecture-doc-generator.js';
9
10
  function parsePnpmWorkspaceGlobs(content) {
10
11
  const lines = content.split('\n');
11
12
  const globs = [];
@@ -460,6 +461,8 @@ export async function initCommand(options = {}) {
460
461
  (typeof rootPkg.devDependencies['@testspectra/matchers'] === 'string' &&
461
462
  rootPkg.devDependencies['@testspectra/matchers'].startsWith('link:')))
462
463
  rootPkg.devDependencies['@testspectra/matchers'] = cliDepVersion;
464
+ if (!rootPkg.devDependencies['nx'])
465
+ rootPkg.devDependencies['nx'] = '^20.8.4';
463
466
  if (!rootPkg.devDependencies['typescript'])
464
467
  rootPkg.devDependencies['typescript'] = '^5.4.5';
465
468
  if (!rootPkg.devDependencies['@wdio/cli'])
@@ -558,137 +561,7 @@ export async function initCommand(options = {}) {
558
561
  // 7. Generate Ambient Types in ROOT only
559
562
  TypeGenerator.writeDeclarationFiles(cwd);
560
563
  ensureVsCodeExtensions(cwd);
561
- const archDocContent = `# TestSpectra Enterprise Monorepo Architecture
562
-
563
- This workspace uses TestSpectra's **"Centralized Configuration, Distributed Implementation"** model for automated cross-platform testing.
564
-
565
- ---
566
-
567
- ## 1. Directory & File Overview
568
-
569
- ### Centralized Root Elements
570
- - \`./spectra.config.ts\`: Single source of truth for runtime configurations (Base URL, Appium devices, browser targets, timeouts).
571
- - \`./.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/\`.
572
- - \`./nx.json\`: Nx target defaults with execution caching and inputs.
573
- - \`./pnpm-workspace.yaml\`: Monorepo package glob definitions.
574
- - \`./tsconfig.json\`: Solution-style TypeScript orchestrator referencing \`./.testspectra/tsconfig.json\`.
575
-
576
- ### Centralized Shared Testing Library (\`./${sharedTestingRelPath}\`)
577
- Provides cross-feature reusable test entities with **100% zero-import global resolution**:
578
- - \`page-objects/\`: Shared Page Object models (e.g. NavigationBar, AppHeader).
579
- - \`steps/\`: Cross-feature business step flows (e.g. \`Step.loginAsAdmin()\`).
580
- - \`actions/\`: Custom atomic actions (e.g. \`Spectra.dismissBanner()\`).
581
- - \`fixtures/\`: Common test data and environment configurations (e.g. \`Fixture.appConfig\`).
582
-
583
- ### Distributed Feature E2E Modules (${featureE2eDirs.length} Features)
584
- ${featureE2eDirs
585
- .map((d) => {
586
- const rel = path.relative(cwd, d);
587
- const name = path.basename(path.dirname(d));
588
- const e2eName = `${name}-${e2eFolderName}`;
589
- return `- \`./${rel}\` (**${name}**, Nx Project: \`${e2eName}\`):
590
- - \`specs/\`: Feature test cases (\`specs/<SuiteName>/<CaseId>/\` with \`web.test.ts\`, \`android.test.ts\`, \`ios.test.ts\`).
591
- - \`page-objects/\`: Feature-specific Page Objects (e.g. \`LoginPage\`, \`ProductPage\`).
592
- - \`project.json\`: Nx targets (\`e2e\`, \`type-check\`) supporting configurations (\`--configuration=android\`, \`--configuration=ios\`, \`--configuration=headless\`).`;
593
- })
594
- .join('\n')}
595
-
596
- ---
597
-
598
- ## 2. Zero-Import Consumption Flow & Mechanics
599
-
600
- TestSpectra completely eliminates boilerplate \`import\` statements across all test specs, steps, and page objects.
601
-
602
- ### How It Works:
603
- 1. **Centralized Ambient Aggregation**:
604
- TestSpectra Language Service Plugin & CLI automatically scan:
605
- - **Local Feature Entities**: \`<feature>/${e2eFolderName}/page-objects/\`, \`<feature>/${e2eFolderName}/steps/\`, \`<feature>/${e2eFolderName}/actions/\`
606
- - **Shared Library Entities**: \`./${sharedTestingRelPath}/page-objects/\`, \`./${sharedTestingRelPath}/steps/\`, \`./${sharedTestingRelPath}/actions/\`, \`./${sharedTestingRelPath}/fixtures/\`
607
- 2. **Ambient Typing Generation**:
608
- These are compiled into \`.testspectra/types/web.d.ts\`, \`android.d.ts\`, \`ios.d.ts\`, \`fixtures.d.ts\`.
609
- 3. **Instant IDE Autocomplete & Zero Clutter**:
610
- Specs consume both local feature objects and shared library utilities globally.
611
-
612
- ### Code Example:
613
- \`\`\`typescript
614
- // packages/features/auth/e2e/specs/Auth/TC-AUTH-01/web.test.ts
615
- // 🌟 NOTICE: 100% Zero-Import & Flat Execution! (No import statement, no describe wrapper)
616
-
617
- it("should navigate via shared header and login with local POM", async () => {
618
- // 1. Consumed from Shared Testing Library (${sharedTestingRelPath}/page-objects/NavigationBar)
619
- await NavigationBar.goToLogin();
620
-
621
- // 2. Consumed from Shared Testing Library (${sharedTestingRelPath}/steps/loginAsAdmin)
622
- await Step.loginAsAdmin();
623
-
624
- // 3. Consumed from Local Feature POM (page-objects/LoginPage)
625
- await LoginPage.flashAlert.shouldBeVisible();
626
- await LoginPage.flashAlert.shouldContainText("Welcome");
627
-
628
- // 4. Consumed from Shared Testing Library Custom Action (${sharedTestingRelPath}/actions/dismissBanner)
629
- await Spectra.dismissBanner();
630
- });
631
- \`\`\`
632
-
633
- ---
634
-
635
- ## 3. How to Run Tests
636
-
637
- You can execute tests either from the **Monorepo Root via Nx** or directly inside each **Feature Directory via CLI**:
638
-
639
- ### Option A: From Root Monorepo via Nx (Recommended for CI & Team Workflows)
640
-
641
- \`\`\`bash
642
- # 1. Run E2E for a SPECIFIC feature module (e.g. auth)
643
- pnpm nx run auth-${e2eFolderName}:e2e
644
-
645
- # 2. Run E2E for a specific feature on ANDROID or IOS
646
- pnpm nx run auth-${e2eFolderName}:e2e --configuration=android
647
- pnpm nx run auth-${e2eFolderName}:e2e --configuration=ios
648
-
649
- # 3. Run E2E for a specific feature in HEADLESS browser mode
650
- pnpm nx run auth-${e2eFolderName}:e2e --configuration=headless
651
-
652
- # 4. Run ALL feature E2E suites across the entire monorepo in parallel
653
- pnpm nx run-many -t e2e
654
-
655
- # 5. Run only E2E tests for features modified in current Git branch / PR
656
- pnpm nx affected -t e2e
657
- \`\`\`
658
-
659
- ### Option B: From Inside the Feature Directory (Direct CLI)
660
-
661
- \`\`\`bash
662
- # Navigate to the feature e2e folder
663
- cd ${featureE2eDirs.length > 0 ? path.relative(cwd, featureE2eDirs[0]).replace(/\\/g, '/') : `modules/auth/${e2eFolderName}`}
664
-
665
- # Run all specs in this feature
666
- pnpm spectra run
667
-
668
- # Run a specific test case file
669
- pnpm spectra run specs/Auth/TC-AUTH-01/web.test.ts
670
-
671
- # Target Android device or emulator
672
- pnpm spectra run --target android
673
-
674
- # Run in headed / interactive browser mode
675
- pnpm spectra run --no-headless
676
- \`\`\`
677
-
678
- ---
679
-
680
- ## 4. Type Checking & Validation
681
-
682
- \`\`\`bash
683
- # Type check all E2E projects via Nx
684
- pnpm nx run-many -t type-check
685
-
686
- # Or standard TypeScript project references build from root
687
- pnpm type-check
688
- # or: npx tsc -b
689
- \`\`\`
690
- `;
691
- fs.writeFileSync(path.join(cwd, 'ARCHITECTURE.md'), archDocContent, 'utf-8');
564
+ generateArchitectureDoc(cwd);
692
565
  // 8. Ensure root .gitignore ignores .testspectra/ and *.tsbuildinfo
693
566
  const rootGitignorePath = path.join(cwd, '.gitignore');
694
567
  const ignoreEntries = ['.testspectra/', '*.tsbuildinfo', '.nx/cache'];
@@ -742,34 +615,7 @@ pnpm type-check
742
615
  TypeGenerator.writeDeclarationFiles(cwd);
743
616
  ensureVsCodeExtensions(cwd);
744
617
  // Standalone Architecture Documentation
745
- const standaloneArchDoc = `# TestSpectra Project Architecture
746
-
747
- This project is built with the TestSpectra testing framework.
748
-
749
- ## Structure & File Map
750
- - \`./spectra.config.ts\`: Typed runner configuration.
751
- - \`./specs/\`: Layered test cases (\`specs/<Suite>/<CaseId>/\` with \`web.test.ts\`, \`android.test.ts\`, \`ios.test.ts\`).
752
- - \`./page-objects/\`: Page Object models per platform.
753
- - \`./steps/\`: Business flows accessible globally via \`Step.*\`.
754
- - \`./actions/\`: Atomic actions accessible globally via \`Spectra.*\`.
755
- - \`./fixtures/\`: JSON fixtures accessible globally via \`Fixture.*\`.
756
- - \`./global-hooks/\`: Global suite lifecycle hooks.
757
- - \`./.testspectra/\`: Auto-generated ambient typings and local runtime cache.
758
-
759
- ## Execution
760
- \`\`\`bash
761
- # Run web test suite
762
- pnpm test
763
-
764
- # Run Android / iOS
765
- pnpm spectra run --target android
766
- pnpm spectra run --target ios
767
-
768
- # Type check
769
- pnpm type-check
770
- \`\`\`
771
- `;
772
- fs.writeFileSync(path.join(cwd, 'ARCHITECTURE.md'), standaloneArchDoc, 'utf-8');
618
+ generateArchitectureDoc(cwd);
773
619
  // Safely update root README.md (do not overwrite existing content)
774
620
  const standaloneReadmePath = path.join(cwd, 'README.md');
775
621
  const standaloneArchSection = `\n## E2E Testing Architecture (TestSpectra)\n\nDetailed test architecture and folder map are documented in: \nšŸ‘‰ **[ARCHITECTURE.md](./ARCHITECTURE.md)**\n\n- **Run tests**: \`pnpm test\`\n- **Type-check**: \`pnpm type-check\`\n`;