@testspectra/cli 1.1.8-rc.9 ā 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/doctor-filter.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor-filter.test.js +37 -0
- package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
- package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
- package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
- package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +95 -0
- package/dist/commands/add.js +3 -3
- package/dist/commands/devices.js +1 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.js +17 -0
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +17 -3
- package/dist/commands/init.js +5 -159
- package/dist/commands/install.js +5 -0
- package/dist/commands/license.js +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +22 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.js +115 -21
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +197 -0
- package/dist/config/schema.d.ts +53 -0
- package/dist/config/schema.js +2 -0
- package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
- package/dist/generator/architecture-doc-generator.d.ts +14 -0
- package/dist/generator/architecture-doc-generator.js +80 -0
- package/dist/generator/index.d.ts +1 -0
- package/dist/generator/index.js +1 -0
- package/dist/generator/tsconfig-generator.js +1 -1
- package/dist/index.js +59 -16
- package/dist/linter/discovery.js +2 -2
- package/dist/linter/schemas/spec.schema.d.ts +6 -6
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +58 -16
- package/dist/runner/reporter.d.ts +1 -0
- package/dist/runner/reporter.js +12 -6
- package/dist/utils/__tests__/dev-server-manager.test.d.ts +1 -0
- package/dist/utils/__tests__/dev-server-manager.test.js +72 -0
- package/dist/utils/background-update-check.d.ts +1 -0
- package/dist/utils/background-update-check.js +43 -0
- package/dist/utils/dependency-updates.d.ts +15 -0
- package/dist/utils/dependency-updates.js +107 -0
- package/dist/utils/dev-server-manager.d.ts +40 -0
- package/dist/utils/dev-server-manager.js +257 -0
- package/dist/utils/update-notifier.d.ts +2 -0
- package/dist/utils/update-notifier.js +67 -0
- package/package.json +10 -9
- package/templates/default/package.json +2 -2
- package/templates/docs/ARCHITECTURE.default.md +26 -0
- package/templates/docs/ARCHITECTURE.nx.md +147 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +745 -0
- package/templates/nx/.nx/workspace-data/d/server-process.json +1 -1
- package/templates/nx/.nx/workspace-data/file-map.json +172 -186
- package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/parsed-lock-file.json +81 -130
- package/templates/nx/.nx/workspace-data/project-graph.json +85 -138
- package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
- package/templates/nx/package.json +3 -3
- package/templates/nx/pnpm-lock.yaml +191 -214
- package/templates/nx/spectra.config.ts +13 -0
- package/templates/react-component/package.json +3 -3
- package/templates/react-component/page-objects/BadgeComponent/web.ts +4 -4
- package/templates/react-component/page-objects/ButtonComponent/web.ts +3 -3
- package/templates/react-component/page-objects/InputComponent/web.ts +1 -1
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +0 -3
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +0 -9
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +1 -1
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +4 -8
- package/templates/react-component/spectra.config.ts +0 -10
- package/templates/react-component/src/components/Badge/Badge.tsx +3 -3
- package/templates/react-component/src/components/Button/Button.tsx +5 -3
- package/templates/react-component/src/components/Input/Input.tsx +2 -3
|
@@ -0,0 +1,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,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
|
+
});
|
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
|
}
|
package/dist/commands/devices.js
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -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) {
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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`;
|
package/dist/commands/install.js
CHANGED
|
@@ -17,6 +17,9 @@ export async function installCommand(item, options = {}) {
|
|
|
17
17
|
if (item) {
|
|
18
18
|
args.push(item);
|
|
19
19
|
}
|
|
20
|
+
if (options.force) {
|
|
21
|
+
args.push('--force');
|
|
22
|
+
}
|
|
20
23
|
return new Promise((resolve) => {
|
|
21
24
|
let child;
|
|
22
25
|
if (binPath === 'cargo') {
|
|
@@ -34,6 +37,7 @@ export async function installCommand(item, options = {}) {
|
|
|
34
37
|
cwd: workspaceRoot,
|
|
35
38
|
env: runnerEnv,
|
|
36
39
|
stdio: 'inherit',
|
|
40
|
+
windowsHide: true,
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
else {
|
|
@@ -41,6 +45,7 @@ export async function installCommand(item, options = {}) {
|
|
|
41
45
|
cwd,
|
|
42
46
|
env: runnerEnv,
|
|
43
47
|
stdio: 'inherit',
|
|
48
|
+
windowsHide: true,
|
|
44
49
|
});
|
|
45
50
|
}
|
|
46
51
|
child.on('close', (code) => {
|
package/dist/commands/license.js
CHANGED
package/dist/commands/run.d.ts
CHANGED
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
|
}
|