@testspectra/cli 1.1.8-rc.20 → 1.1.8-rc.22
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/bin/.testspectra-runner.hash +1 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
- package/dist/commands/__tests__/cloud-run.test.js +93 -0
- package/dist/commands/cloud-run.d.ts +13 -0
- package/dist/commands/cloud-run.js +120 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.js +17 -0
- package/dist/commands/init.js +5 -159
- package/dist/generator/architecture-doc-generator.d.ts +14 -0
- package/dist/generator/architecture-doc-generator.js +82 -0
- package/dist/generator/index.d.ts +1 -0
- package/dist/generator/index.js +1 -0
- package/dist/index.js +7 -0
- package/package.json +8 -8
- package/templates/default/package.json +2 -2
- package/templates/docs/ARCHITECTURE.default.md +26 -0
- package/templates/docs/ARCHITECTURE.nx.md +119 -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 +1112 -6269
- package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
- package/templates/nx/.nx/workspace-data/file-map.json +101 -101
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/parsed-lock-file.json +1 -6497
- package/templates/nx/.nx/workspace-data/project-graph.json +1 -7244
- package/templates/nx/package.json +3 -3
- package/templates/react-component/package.json +3 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { cloudRunCommand, parseTimeoutMs } from '../cloud-run.js';
|
|
3
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
4
|
+
function mockFetchSequence(responses) {
|
|
5
|
+
let call = 0;
|
|
6
|
+
return vi.fn(async (_url, _init) => {
|
|
7
|
+
const res = responses[Math.min(call, responses.length - 1)];
|
|
8
|
+
call += 1;
|
|
9
|
+
return {
|
|
10
|
+
ok: res.ok,
|
|
11
|
+
status: res.status ?? (res.ok ? 200 : 500),
|
|
12
|
+
statusText: 'error',
|
|
13
|
+
json: async () => res.json,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
describe('cloudRunCommand (docs/v2/cloud/git-spec-standards-and-workflow.md)', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
process.env = { ...ORIGINAL_ENV, SPECTRA_URL: 'https://cloud.test', SPECTRA_TOKEN: 'gitea-pat-123' };
|
|
20
|
+
process.exitCode = undefined;
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.env = { ...ORIGINAL_ENV };
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
});
|
|
28
|
+
it('fails fast without SPECTRA_URL/SPECTRA_TOKEN', async () => {
|
|
29
|
+
delete process.env.SPECTRA_URL;
|
|
30
|
+
delete process.env.SPECTRA_TOKEN;
|
|
31
|
+
const fetchSpy = vi.fn();
|
|
32
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
33
|
+
await cloudRunCommand({ project: 'org/repo', branch: 'main' });
|
|
34
|
+
expect(process.exitCode).toBe(1);
|
|
35
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
it('triggers and returns immediately with --no-wait', async () => {
|
|
38
|
+
const fetchSpy = mockFetchSequence([{ ok: true, json: { job_id: 'job-1', status: 'queued' } }]);
|
|
39
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
40
|
+
await cloudRunCommand({ project: 'org/repo', branch: 'main', wait: false });
|
|
41
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
42
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
43
|
+
expect(url).toBe('https://cloud.test/api/v1/runs/trigger');
|
|
44
|
+
const headers = init.headers;
|
|
45
|
+
expect(headers.Authorization).toBe('Bearer gitea-pat-123');
|
|
46
|
+
expect(JSON.parse(init.body)).toMatchObject({ project_id: 'org/repo', branch: 'main', platform: 'web' });
|
|
47
|
+
expect(process.exitCode).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
it('exits 0 when the job reaches done', async () => {
|
|
50
|
+
const fetchSpy = mockFetchSequence([
|
|
51
|
+
{ ok: true, json: { job_id: 'job-2', status: 'queued' } },
|
|
52
|
+
{ ok: true, json: { status: 'done', run_id: 'run-2', started_at: null, completed_at: null, error_log: null } },
|
|
53
|
+
]);
|
|
54
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
55
|
+
const promise = cloudRunCommand({ project: 'org/repo', branch: 'main' });
|
|
56
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
57
|
+
await promise;
|
|
58
|
+
expect(process.exitCode).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
it('exits 1 when the job reaches failed', async () => {
|
|
61
|
+
const fetchSpy = mockFetchSequence([
|
|
62
|
+
{ ok: true, json: { job_id: 'job-3', status: 'queued' } },
|
|
63
|
+
{
|
|
64
|
+
ok: true,
|
|
65
|
+
json: { status: 'failed', run_id: 'run-3', started_at: null, completed_at: null, error_log: 'boom' },
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
69
|
+
const promise = cloudRunCommand({ project: 'org/repo', branch: 'main' });
|
|
70
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
71
|
+
await promise;
|
|
72
|
+
expect(process.exitCode).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
it('exits 1 when the trigger request itself fails', async () => {
|
|
75
|
+
const fetchSpy = mockFetchSequence([{ ok: false, status: 401, json: { error: 'Invalid PAT' } }]);
|
|
76
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
77
|
+
await cloudRunCommand({ project: 'org/repo', branch: 'main' });
|
|
78
|
+
expect(process.exitCode).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('parseTimeoutMs', () => {
|
|
82
|
+
it('parses seconds, minutes, and hours', () => {
|
|
83
|
+
expect(parseTimeoutMs('30s')).toBe(30_000);
|
|
84
|
+
expect(parseTimeoutMs('30m')).toBe(30 * 60_000);
|
|
85
|
+
expect(parseTimeoutMs('2h')).toBe(2 * 3_600_000);
|
|
86
|
+
});
|
|
87
|
+
it('defaults to minutes when no unit is given', () => {
|
|
88
|
+
expect(parseTimeoutMs('10')).toBe(10 * 60_000);
|
|
89
|
+
});
|
|
90
|
+
it('falls back to 30m for unparseable input', () => {
|
|
91
|
+
expect(parseTimeoutMs('nonsense')).toBe(30 * 60_000);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CloudRunOptions {
|
|
2
|
+
project: string;
|
|
3
|
+
projectName?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
commit?: string;
|
|
6
|
+
platform?: string;
|
|
7
|
+
wait?: boolean;
|
|
8
|
+
timeout?: string;
|
|
9
|
+
demo?: boolean;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseTimeoutMs(timeout: string): number;
|
|
13
|
+
export declare function cloudRunCommand(options: CloudRunOptions): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
const POLL_INTERVAL_MS = 5000;
|
|
5
|
+
const DEFAULT_TIMEOUT = '30m';
|
|
6
|
+
function detectGitRef(...args) {
|
|
7
|
+
try {
|
|
8
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseTimeoutMs(timeout) {
|
|
15
|
+
const match = /^(\d+)(s|m|h)?$/.exec(timeout.trim());
|
|
16
|
+
if (!match)
|
|
17
|
+
return 30 * 60 * 1000;
|
|
18
|
+
const value = Number(match[1]);
|
|
19
|
+
const unit = match[2] ?? 'm';
|
|
20
|
+
const multiplier = unit === 's' ? 1000 : unit === 'h' ? 3_600_000 : 60_000;
|
|
21
|
+
return value * multiplier;
|
|
22
|
+
}
|
|
23
|
+
// Same credential path as the trigger endpoint expects: a Gitea PAT, not the
|
|
24
|
+
// dashboard's OAuth2 session — a CI runner can't complete a browser redirect.
|
|
25
|
+
// See docs/v2/cloud/git-spec-standards-and-workflow.md.
|
|
26
|
+
function requireEnv(name) {
|
|
27
|
+
const value = process.env[name];
|
|
28
|
+
if (!value) {
|
|
29
|
+
console.error(chalk.red(`Missing required environment variable: ${name}`));
|
|
30
|
+
console.error(chalk.gray('Set it as a CI secret — see docs/v2/cloud/git-spec-standards-and-workflow.md'));
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export async function cloudRunCommand(options) {
|
|
35
|
+
const spectraUrl = requireEnv('SPECTRA_URL')?.replace(/\/$/, '');
|
|
36
|
+
const spectraToken = requireEnv('SPECTRA_TOKEN');
|
|
37
|
+
if (!spectraUrl || !spectraToken) {
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const branch = options.branch ?? detectGitRef('rev-parse', '--abbrev-ref', 'HEAD');
|
|
42
|
+
if (!branch) {
|
|
43
|
+
console.error(chalk.red('Could not determine branch: pass --branch or run inside a git repository.'));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const commit = options.commit ?? detectGitRef('rev-parse', 'HEAD');
|
|
48
|
+
const platform = options.platform ?? 'web';
|
|
49
|
+
const wait = options.wait !== false;
|
|
50
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
51
|
+
const spinner = ora(`Triggering cloud run for ${chalk.bold(options.project)}@${branch}...`).start();
|
|
52
|
+
let trigger;
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`${spectraUrl}/api/v1/runs/trigger`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
Authorization: `Bearer ${spectraToken}`,
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
project_id: options.project,
|
|
62
|
+
project_name: options.projectName ?? options.project,
|
|
63
|
+
branch,
|
|
64
|
+
commit_hash: commit,
|
|
65
|
+
platform,
|
|
66
|
+
demo: options.demo ?? false,
|
|
67
|
+
base_url: options.baseUrl,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
72
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
73
|
+
}
|
|
74
|
+
trigger = (await res.json());
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
spinner.fail(chalk.red(`Failed to trigger cloud run: ${err.message}`));
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
spinner.succeed(`Cloud run queued: ${chalk.cyan(trigger.job_id)}`);
|
|
82
|
+
if (!wait) {
|
|
83
|
+
console.log(chalk.gray(`Not waiting for result. Poll ${spectraUrl}/api/v1/jobs/${trigger.job_id}/status yourself.`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await waitForJob(spectraUrl, spectraToken, trigger.job_id, timeout);
|
|
87
|
+
}
|
|
88
|
+
async function waitForJob(spectraUrl, spectraToken, jobId, timeout) {
|
|
89
|
+
const spinner = ora('Waiting for run to finish...').start();
|
|
90
|
+
const deadline = Date.now() + parseTimeoutMs(timeout);
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
93
|
+
let job;
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${spectraUrl}/api/v1/jobs/${jobId}/status`, {
|
|
96
|
+
headers: { Authorization: `Bearer ${spectraToken}` },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
throw new Error(`HTTP ${res.status}`);
|
|
100
|
+
job = (await res.json());
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
spinner.text = chalk.yellow(`Poll failed, retrying: ${err.message}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (job.status === 'done') {
|
|
107
|
+
spinner.succeed(chalk.green(`Run passed (run_id: ${job.run_id})`));
|
|
108
|
+
process.exitCode = 0;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (job.status === 'failed') {
|
|
112
|
+
spinner.fail(chalk.red(`Run failed (run_id: ${job.run_id})${job.error_log ? `: ${job.error_log}` : ''}`));
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
spinner.text = `Run status: ${job.status}`;
|
|
117
|
+
}
|
|
118
|
+
spinner.fail(chalk.red(`Timed out after ${timeout} waiting for the run to finish.`));
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
@@ -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
|
+
}
|
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`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ArchitectureDocResult {
|
|
2
|
+
mode: 'nx' | 'default';
|
|
3
|
+
path: string;
|
|
4
|
+
sharedTestingRelPath?: string;
|
|
5
|
+
featureCount?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildFeatureModulesSection(cwd: string, featureE2eDirs: string[]): string;
|
|
8
|
+
/**
|
|
9
|
+
* (Re)generates `ARCHITECTURE.md` from the current on-disk workspace state — no prompts, no other
|
|
10
|
+
* scaffolding side effects. Nx/monorepo mode is auto-detected via `TsConfigGenerator.getEntityScopes`
|
|
11
|
+
* (the same shared/feature discovery `sync-types` already relies on), so this stays accurate after
|
|
12
|
+
* features are added/removed/renamed without needing to re-run `spectra init`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateArchitectureDoc(cwd: string): ArchitectureDocResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { TsConfigGenerator } from './tsconfig-generator.js';
|
|
5
|
+
function resolveDocsTemplateDir() {
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const candidates = [
|
|
9
|
+
path.resolve(__dirname, '../templates/docs'),
|
|
10
|
+
path.resolve(__dirname, '../../templates/docs'),
|
|
11
|
+
path.resolve(__dirname, '../../../templates/docs'),
|
|
12
|
+
];
|
|
13
|
+
return candidates.find((d) => fs.existsSync(d)) ?? candidates[candidates.length - 1];
|
|
14
|
+
}
|
|
15
|
+
function renderArchitectureDoc(templatePath, replacements) {
|
|
16
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
17
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
18
|
+
content = content.replaceAll(token, value);
|
|
19
|
+
}
|
|
20
|
+
return content;
|
|
21
|
+
}
|
|
22
|
+
/** Reads a feature e2e dir's project.json for its Nx project name, falling back to `<parentDirName>-<e2eDirName>`. */
|
|
23
|
+
function resolveNxProjectName(featureDir) {
|
|
24
|
+
const parentName = path.basename(path.dirname(featureDir));
|
|
25
|
+
const fallback = `${parentName}-${path.basename(featureDir)}`;
|
|
26
|
+
const projJsonPath = path.join(featureDir, 'project.json');
|
|
27
|
+
if (!fs.existsSync(projJsonPath))
|
|
28
|
+
return fallback;
|
|
29
|
+
try {
|
|
30
|
+
const pObj = JSON.parse(fs.readFileSync(projJsonPath, 'utf-8'));
|
|
31
|
+
return typeof pObj.name === 'string' && pObj.name ? pObj.name : fallback;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function buildFeatureModulesSection(cwd, featureE2eDirs) {
|
|
38
|
+
return featureE2eDirs
|
|
39
|
+
.map((d) => {
|
|
40
|
+
const rel = path.relative(cwd, d).replace(/\\/g, '/');
|
|
41
|
+
const featureName = path.basename(path.dirname(d));
|
|
42
|
+
const e2eName = resolveNxProjectName(d);
|
|
43
|
+
return `- \`./${rel}\` (**${featureName}**, Nx Project: \`${e2eName}\`):
|
|
44
|
+
- \`specs/\`: Feature test cases (\`specs/<SuiteName>/<CaseId>/\` with \`web.test.ts\`, \`android.test.ts\`, \`ios.test.ts\`).
|
|
45
|
+
- \`page-objects/\`: Feature-specific Page Objects (e.g. \`LoginPage\`, \`ProductPage\`).
|
|
46
|
+
- \`project.json\`: Nx targets (\`e2e\`, \`type-check\`) supporting configurations (\`--configuration=android\`, \`--configuration=ios\`, \`--configuration=headless\`).`;
|
|
47
|
+
})
|
|
48
|
+
.join('\n');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* (Re)generates `ARCHITECTURE.md` from the current on-disk workspace state — no prompts, no other
|
|
52
|
+
* scaffolding side effects. Nx/monorepo mode is auto-detected via `TsConfigGenerator.getEntityScopes`
|
|
53
|
+
* (the same shared/feature discovery `sync-types` already relies on), so this stays accurate after
|
|
54
|
+
* features are added/removed/renamed without needing to re-run `spectra init`.
|
|
55
|
+
*/
|
|
56
|
+
export function generateArchitectureDoc(cwd) {
|
|
57
|
+
const docsTemplateDir = resolveDocsTemplateDir();
|
|
58
|
+
const destPath = path.join(cwd, 'ARCHITECTURE.md');
|
|
59
|
+
const scopes = TsConfigGenerator.getEntityScopes(cwd);
|
|
60
|
+
const isNxMode = scopes.length > 0 || fs.existsSync(path.join(cwd, 'nx.json'));
|
|
61
|
+
if (!isNxMode) {
|
|
62
|
+
fs.copyFileSync(path.join(docsTemplateDir, 'ARCHITECTURE.default.md'), destPath);
|
|
63
|
+
return { mode: 'default', path: destPath };
|
|
64
|
+
}
|
|
65
|
+
const sharedScope = scopes.find((s) => s.isShared);
|
|
66
|
+
const featureE2eDirs = scopes.filter((s) => !s.isShared).map((s) => s.dir);
|
|
67
|
+
const sharedTestingRelPath = sharedScope
|
|
68
|
+
? path.relative(cwd, sharedScope.dir).replace(/\\/g, '/')
|
|
69
|
+
: 'shared/testing';
|
|
70
|
+
const e2eFolderName = featureE2eDirs.length > 0 ? path.basename(featureE2eDirs[0]) : 'e2e';
|
|
71
|
+
const content = renderArchitectureDoc(path.join(docsTemplateDir, 'ARCHITECTURE.nx.md'), {
|
|
72
|
+
__SHARED_TESTING_REL_PATH__: sharedTestingRelPath,
|
|
73
|
+
__FEATURE_COUNT__: String(featureE2eDirs.length),
|
|
74
|
+
__FEATURE_MODULES_SECTION__: buildFeatureModulesSection(cwd, featureE2eDirs),
|
|
75
|
+
__E2E_FOLDER_NAME__: e2eFolderName,
|
|
76
|
+
__SAMPLE_FEATURE_E2E_PATH__: featureE2eDirs.length > 0
|
|
77
|
+
? path.relative(cwd, featureE2eDirs[0]).replace(/\\/g, '/')
|
|
78
|
+
: `modules/auth/${e2eFolderName}`,
|
|
79
|
+
});
|
|
80
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
81
|
+
return { mode: 'nx', path: destPath, sharedTestingRelPath, featureCount: featureE2eDirs.length };
|
|
82
|
+
}
|
package/dist/generator/index.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { syncTypesCommand } from './commands/sync-types.js';
|
|
|
13
13
|
import { refactorCommand } from './commands/refactor.js';
|
|
14
14
|
import { lintCommand } from './commands/lint.js';
|
|
15
15
|
import { licenseCommand } from './commands/license.js';
|
|
16
|
+
import { docsUpdateCommand } from './commands/docs.js';
|
|
16
17
|
import { skillsInitCommand, skillsListCommand, skillsAddCommand, skillsUpdateCommand, skillsRemoveCommand, } from './commands/skills.js';
|
|
17
18
|
export * from './config/schema.js';
|
|
18
19
|
export * from './config/loader.js';
|
|
@@ -147,6 +148,12 @@ Examples:
|
|
|
147
148
|
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
148
149
|
.option('-a, --agent <ids>', 'One-off override: comma-separated agent(s) to target, bypassing the saved "init" selection (claude, opencode, kilo, agents, gemini)')
|
|
149
150
|
.action(skillsRemoveCommand);
|
|
151
|
+
const docs = program.command('docs').description('Manage generated TestSpectra workspace documentation');
|
|
152
|
+
docs
|
|
153
|
+
.command('update')
|
|
154
|
+
.description('Regenerate ARCHITECTURE.md from the current workspace state, without re-running init')
|
|
155
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
156
|
+
.action(docsUpdateCommand);
|
|
150
157
|
program
|
|
151
158
|
.command('run [paths...]')
|
|
152
159
|
.description('Execute test suite or individual test case')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testspectra/cli",
|
|
3
|
-
"version": "1.1.8-rc.
|
|
3
|
+
"version": "1.1.8-rc.22",
|
|
4
4
|
"description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@testspectra/matchers": "^1.1.8-rc.
|
|
30
|
-
"@testspectra/react": "^1.1.8-rc.
|
|
31
|
-
"@testspectra/skills": "^1.1.8-rc.
|
|
29
|
+
"@testspectra/matchers": "^1.1.8-rc.22",
|
|
30
|
+
"@testspectra/react": "^1.1.8-rc.22",
|
|
31
|
+
"@testspectra/skills": "^1.1.8-rc.22",
|
|
32
32
|
"chalk": "^5.3.0",
|
|
33
33
|
"commander": "^12.1.0",
|
|
34
34
|
"dotenv": "^16.4.5",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"zod": "^3.23.8"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@testspectra/cli-darwin-arm64": "1.1.8-rc.
|
|
42
|
-
"@testspectra/cli-linux-x64": "1.1.8-rc.
|
|
43
|
-
"@testspectra/cli-win32-arm64": "1.1.8-rc.
|
|
44
|
-
"@testspectra/cli-win32-x64": "1.1.8-rc.
|
|
41
|
+
"@testspectra/cli-darwin-arm64": "1.1.8-rc.22",
|
|
42
|
+
"@testspectra/cli-linux-x64": "1.1.8-rc.22",
|
|
43
|
+
"@testspectra/cli-win32-arm64": "1.1.8-rc.22",
|
|
44
|
+
"@testspectra/cli-win32-x64": "1.1.8-rc.22"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^20.14.0",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"postinstall": "spectra sync-types"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@testspectra/cli": "^1.1.8-rc.
|
|
21
|
-
"@testspectra/matchers": "^1.1.8-rc.
|
|
20
|
+
"@testspectra/cli": "^1.1.8-rc.22",
|
|
21
|
+
"@testspectra/matchers": "^1.1.8-rc.22",
|
|
22
22
|
"@types/node": "^20.14.0",
|
|
23
23
|
"@wdio/cli": "^9.2.8",
|
|
24
24
|
"@wdio/local-runner": "^9.2.8",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# TestSpectra Project Architecture
|
|
2
|
+
|
|
3
|
+
This project is built with the TestSpectra testing framework.
|
|
4
|
+
|
|
5
|
+
## Structure & File Map
|
|
6
|
+
- `./spectra.config.ts`: Typed runner configuration.
|
|
7
|
+
- `./specs/`: Layered test cases (`specs/<Suite>/<CaseId>/` with `web.test.ts`, `android.test.ts`, `ios.test.ts`).
|
|
8
|
+
- `./page-objects/`: Page Object models per platform.
|
|
9
|
+
- `./support/steps/`: Business flows accessible globally via `Step.*`.
|
|
10
|
+
- `./support/actions/`: Atomic actions accessible globally via `Spectra.*`.
|
|
11
|
+
- `./fixtures/`: JSON fixtures accessible globally via `Fixture.*`.
|
|
12
|
+
- `./global-hooks/`: Global suite lifecycle hooks.
|
|
13
|
+
- `./.testspectra/`: Auto-generated ambient typings and local runtime cache.
|
|
14
|
+
|
|
15
|
+
## Execution
|
|
16
|
+
```bash
|
|
17
|
+
# Run web test suite
|
|
18
|
+
pnpm test
|
|
19
|
+
|
|
20
|
+
# Run Android / iOS
|
|
21
|
+
pnpm spectra run --target android
|
|
22
|
+
pnpm spectra run --target ios
|
|
23
|
+
|
|
24
|
+
# Type check
|
|
25
|
+
pnpm type-check
|
|
26
|
+
```
|