@testspectra/cli 1.1.8-rc.1 → 1.1.8-rc.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 +47 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/add.js +2 -1
- package/dist/commands/init.js +2 -1
- package/dist/commands/test.js +29 -1
- package/dist/generator/__tests__/type-generator.test.js +38 -0
- package/dist/generator/type-generator.d.ts +5 -2
- package/dist/generator/type-generator.js +14 -17
- package/dist/linter/__tests__/shared-lib-boundary.test.js +15 -15
- package/dist/linter/discovery.d.ts +3 -3
- package/dist/linter/discovery.js +5 -5
- package/dist/linter/schemas/spec.schema.d.ts +10 -10
- package/dist/linter/schemas/suite.schema.d.ts +8 -8
- package/dist/reporter/semantic-formatter.js +5 -0
- package/dist/runner/bridge.js +52 -4
- package/package.json +34 -29
- package/templates/default/package.json +2 -2
- 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 +415 -0
- package/templates/nx/.nx/workspace-data/file-map.json +88 -88
- package/templates/nx/.nx/workspace-data/project-graph.json +5 -6
- package/templates/nx/package.json +2 -2
- package/templates/nx/packages/matchers/e2e/project.json +2 -2
- package/templates/nx/packages/playground/e2e/project.json +2 -2
- package/templates/nx/shared/testing/project.json +1 -2
- package/bin/.testspectra-runner.hash +0 -1
- package/bin/testspectra-runner +0 -0
package/dist/commands/add.js
CHANGED
|
@@ -289,6 +289,7 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
289
289
|
copyRecursive(sampleE2eSrc, targetE2eDir, {
|
|
290
290
|
'playground-e2e': e2eProjectName,
|
|
291
291
|
'packages/playground/e2e': path.relative(rootDir, targetE2eDir).replace(/\\/g, '/'),
|
|
292
|
+
'testspectra:scope:playground': `testspectra:scope:${featureName}`,
|
|
292
293
|
'scope:playground': `scope:${featureName}`,
|
|
293
294
|
});
|
|
294
295
|
// Adjust project.json
|
|
@@ -300,7 +301,7 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
300
301
|
pObj.sourceRoot = path.relative(rootDir, targetE2eDir).replace(/\\/g, '/');
|
|
301
302
|
pObj.targets.e2e.options.cwd = path.relative(rootDir, targetE2eDir).replace(/\\/g, '/');
|
|
302
303
|
pObj.targets['type-check'].options.cwd = '.';
|
|
303
|
-
pObj.tags = [`scope:${featureName}
|
|
304
|
+
pObj.tags = ['testspectra:e2e', `testspectra:scope:${featureName}`];
|
|
304
305
|
fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
|
|
305
306
|
}
|
|
306
307
|
catch { }
|
package/dist/commands/init.js
CHANGED
|
@@ -508,6 +508,7 @@ export async function initCommand(options = {}) {
|
|
|
508
508
|
copyRecursive(sampleE2eSrc, targetE2eDir, {
|
|
509
509
|
'playground-e2e': e2eProjectName,
|
|
510
510
|
'packages/playground/e2e': path.relative(cwd, targetE2eDir).replace(/\\/g, '/'),
|
|
511
|
+
'testspectra:scope:playground': `testspectra:scope:${featureName}`,
|
|
511
512
|
'scope:playground': `scope:${featureName}`,
|
|
512
513
|
});
|
|
513
514
|
// Adjust project.json
|
|
@@ -519,7 +520,7 @@ export async function initCommand(options = {}) {
|
|
|
519
520
|
pObj.sourceRoot = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
|
|
520
521
|
pObj.targets.e2e.options.cwd = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
|
|
521
522
|
pObj.targets['type-check'].options.cwd = '.';
|
|
522
|
-
pObj.tags = [`scope:${featureName}
|
|
523
|
+
pObj.tags = ['testspectra:e2e', `testspectra:scope:${featureName}`];
|
|
523
524
|
fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
|
|
524
525
|
}
|
|
525
526
|
catch { }
|
package/dist/commands/test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
|
+
import { renderSummaryView } from '../reporter/summary-view.js';
|
|
5
|
+
import { Reporter } from '../runner/reporter.js';
|
|
4
6
|
import { runSystemChecks } from './doctor.js';
|
|
5
7
|
/**
|
|
6
8
|
* Finds the Chrome-for-Testing binary that `spectra doctor` already installs/manages for the
|
|
@@ -63,14 +65,40 @@ export async function testCommand(options = {}) {
|
|
|
63
65
|
headless = options.headless;
|
|
64
66
|
}
|
|
65
67
|
const concurrency = options.concurrency !== undefined ? parseInt(String(options.concurrency), 10) : undefined;
|
|
68
|
+
const outputFile = options.output ? path.resolve(cwd, options.output) : undefined;
|
|
69
|
+
// Reuses the exact same semantic terminal reporter `spectra run` drives (see
|
|
70
|
+
// docs/v2/cli/matchers-semantic-reporter.md) — @testspectra/react emits the identical
|
|
71
|
+
// [TESTSPECTRA_*] protocol lines the E2E Rust orchestrator does (see tools/react/src/prelude.ts
|
|
72
|
+
// / setup.ts), so the same Reporter class parses them with zero changes on either side.
|
|
73
|
+
const reporter = new Reporter();
|
|
74
|
+
const runStartedAt = Date.now();
|
|
66
75
|
const ok = await reactPkg.runComponentTests({
|
|
67
76
|
cwd,
|
|
68
77
|
headless,
|
|
69
78
|
stepDelayMs,
|
|
70
79
|
spec: options.spec,
|
|
71
|
-
outputFile
|
|
80
|
+
outputFile,
|
|
72
81
|
concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
|
|
73
82
|
browserExecutablePath,
|
|
83
|
+
onLine: (line) => {
|
|
84
|
+
if (!line.startsWith('[TESTSPECTRA_'))
|
|
85
|
+
return;
|
|
86
|
+
reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'INFO', message: line });
|
|
87
|
+
if (!reporter.isInteractiveMode()) {
|
|
88
|
+
console.log(line);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const durationMs = Date.now() - runStartedAt;
|
|
93
|
+
const result = reporter.generateResult(ok ? 'passed' : 'failed', `${(durationMs / 1000).toFixed(1)}s`);
|
|
94
|
+
const totalTests = result.passedCount + result.failedCount;
|
|
95
|
+
renderSummaryView({
|
|
96
|
+
totalSuites: totalTests,
|
|
97
|
+
totalTests,
|
|
98
|
+
passedCount: result.passedCount,
|
|
99
|
+
failedCount: result.failedCount,
|
|
100
|
+
durationMs,
|
|
101
|
+
reportLocation: outputFile ? path.relative(cwd, outputFile) : '(use -o/--output to save a report)',
|
|
74
102
|
});
|
|
75
103
|
process.exit(ok ? 0 : 1);
|
|
76
104
|
}
|
|
@@ -383,4 +383,42 @@ async function verifyZeroImport() {
|
|
|
383
383
|
expect(fs.existsSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'))).toBe(false);
|
|
384
384
|
expect(fs.existsSync(path.join(tmpDir, '.testspectra', 'types', 'common.d.ts'))).toBe(false);
|
|
385
385
|
});
|
|
386
|
+
it('nx monorepo: does NOT treat feature library as shared library and discovers nested e2e subproject', () => {
|
|
387
|
+
// 1. Shared library with testspectra:shared
|
|
388
|
+
const sharedDir = path.join(tmpDir, 'shared', 'testing');
|
|
389
|
+
fs.mkdirSync(path.join(sharedDir, 'page-objects', 'SharedNav'), { recursive: true });
|
|
390
|
+
fs.writeFileSync(path.join(sharedDir, 'project.json'), JSON.stringify({ name: 'shared-testing', projectType: 'library', tags: ['testspectra:shared'] }));
|
|
391
|
+
fs.writeFileSync(path.join(sharedDir, 'page-objects', 'SharedNav', 'common.ts'), 'export default class SharedNav {}');
|
|
392
|
+
// 2. Nx Feature library (projectType: "library", standard Nx tags, NO testspectra tags)
|
|
393
|
+
const featureLibDir = path.join(tmpDir, 'packages', 'features', 'auth');
|
|
394
|
+
fs.mkdirSync(featureLibDir, { recursive: true });
|
|
395
|
+
fs.writeFileSync(path.join(featureLibDir, 'project.json'), JSON.stringify({
|
|
396
|
+
name: '@tagsamurai/feature-auth',
|
|
397
|
+
projectType: 'library',
|
|
398
|
+
sourceRoot: 'packages/features/auth',
|
|
399
|
+
targets: {},
|
|
400
|
+
tags: ['type:feature', 'scope:feature-auth'],
|
|
401
|
+
}));
|
|
402
|
+
// 3. Nested E2E subproject inside feature library
|
|
403
|
+
const featureE2eDir = path.join(featureLibDir, 'e2e');
|
|
404
|
+
fs.mkdirSync(path.join(featureE2eDir, 'specs', 'AuthSuite', 'TC-001'), { recursive: true });
|
|
405
|
+
fs.writeFileSync(path.join(featureE2eDir, 'project.json'), JSON.stringify({
|
|
406
|
+
name: 'feature-auth-e2e',
|
|
407
|
+
projectType: 'application',
|
|
408
|
+
tags: ['testspectra:e2e', 'testspectra:scope:feature-auth'],
|
|
409
|
+
}));
|
|
410
|
+
fs.writeFileSync(path.join(featureE2eDir, 'specs', 'AuthSuite', 'TC-001', 'web.test.ts'), 'it("auth test", async () => {});');
|
|
411
|
+
const scopes = TypeGenerator.getEntityScopes(tmpDir);
|
|
412
|
+
expect(scopes).toHaveLength(2);
|
|
413
|
+
const sharedScope = scopes.find((s) => s.isShared);
|
|
414
|
+
expect(sharedScope).toBeDefined();
|
|
415
|
+
expect(sharedScope?.name).toBe('shared');
|
|
416
|
+
expect(sharedScope?.dir).toBe(path.resolve(sharedDir));
|
|
417
|
+
const featureScope = scopes.find((s) => !s.isShared);
|
|
418
|
+
expect(featureScope).toBeDefined();
|
|
419
|
+
expect(featureScope?.name).toBe('feature-auth');
|
|
420
|
+
expect(featureScope?.dir).toBe(path.resolve(featureE2eDir));
|
|
421
|
+
// Verify packages/features/auth is NOT in scopes
|
|
422
|
+
expect(scopes.some((s) => s.dir === path.resolve(featureLibDir))).toBe(false);
|
|
423
|
+
});
|
|
386
424
|
});
|
|
@@ -9,8 +9,11 @@ export interface EntityScope {
|
|
|
9
9
|
* Discovers all feature directories and shared library directories.
|
|
10
10
|
* Standard discovery hierarchy:
|
|
11
11
|
* 1. Nx / Monorepo Project Descriptors (`project.json`):
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
12
|
+
* - tags: ["testspectra:shared"] => Shared Library Scope (isShared: true)
|
|
13
|
+
* - tags: ["testspectra:e2e"] or ["testspectra:scope:<name>"] => Feature Scope (isShared: false)
|
|
14
|
+
* - Generic Nx libraries/apps without testspectra tags are not TestSpectra scopes and recursion
|
|
15
|
+
* continues into their subdirectories so nested e2e projects (e.g. `packages/features/auth/e2e`)
|
|
16
|
+
* are discovered.
|
|
14
17
|
* 2. Explicit User Configuration (`tsconfig.spectra.shared.json`):
|
|
15
18
|
* - Custom include path for shared testing utilities
|
|
16
19
|
* 3. Dynamic Workspace Filesystem Discovery (Fallback for non-Nx / custom layouts):
|
|
@@ -13,8 +13,11 @@ export const PLATFORM_HIERARCHY = {
|
|
|
13
13
|
* Discovers all feature directories and shared library directories.
|
|
14
14
|
* Standard discovery hierarchy:
|
|
15
15
|
* 1. Nx / Monorepo Project Descriptors (`project.json`):
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
16
|
+
* - tags: ["testspectra:shared"] => Shared Library Scope (isShared: true)
|
|
17
|
+
* - tags: ["testspectra:e2e"] or ["testspectra:scope:<name>"] => Feature Scope (isShared: false)
|
|
18
|
+
* - Generic Nx libraries/apps without testspectra tags are not TestSpectra scopes and recursion
|
|
19
|
+
* continues into their subdirectories so nested e2e projects (e.g. `packages/features/auth/e2e`)
|
|
20
|
+
* are discovered.
|
|
18
21
|
* 2. Explicit User Configuration (`tsconfig.spectra.shared.json`):
|
|
19
22
|
* - Custom include path for shared testing utilities
|
|
20
23
|
* 3. Dynamic Workspace Filesystem Discovery (Fallback for non-Nx / custom layouts):
|
|
@@ -48,16 +51,9 @@ export function discoverEntityScopes(cwd) {
|
|
|
48
51
|
if (fs.existsSync(projectJsonPath)) {
|
|
49
52
|
try {
|
|
50
53
|
const pObj = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
51
|
-
const projectType = pObj.projectType;
|
|
52
54
|
const tags = Array.isArray(pObj.tags) ? pObj.tags : [];
|
|
53
|
-
const isSharedLib =
|
|
54
|
-
|
|
55
|
-
tags.includes('type:library') ||
|
|
56
|
-
tags.includes('scope:shared');
|
|
57
|
-
const isE2eApp = projectType === 'application' ||
|
|
58
|
-
tags.includes('type:e2e') ||
|
|
59
|
-
tags.includes('type:app') ||
|
|
60
|
-
tags.some((t) => t.startsWith('scope:') && t !== 'scope:shared');
|
|
55
|
+
const isSharedLib = tags.includes('testspectra:shared');
|
|
56
|
+
const isE2eApp = tags.includes('testspectra:e2e') || tags.some((t) => t.startsWith('testspectra:scope:'));
|
|
61
57
|
if (isSharedLib) {
|
|
62
58
|
if (!scopes.some((s) => s.dir === dir)) {
|
|
63
59
|
scopes.push({ name: 'shared', dir, isShared: true });
|
|
@@ -65,9 +61,9 @@ export function discoverEntityScopes(cwd) {
|
|
|
65
61
|
return; // Do not recurse into subdirectories of an identified project
|
|
66
62
|
}
|
|
67
63
|
else if (isE2eApp) {
|
|
68
|
-
// Extract scope name from tags (e.g. "scope:matchers" => "matchers") or project name
|
|
69
|
-
const scopeTag = tags.find((t) => t.startsWith('
|
|
70
|
-
let featureName = scopeTag ? scopeTag.replace('scope:', '') : '';
|
|
64
|
+
// Extract scope name from tags (e.g. "testspectra:scope:matchers" => "matchers") or project name
|
|
65
|
+
const scopeTag = tags.find((t) => t.startsWith('testspectra:scope:'));
|
|
66
|
+
let featureName = scopeTag ? scopeTag.replace('testspectra:scope:', '') : '';
|
|
71
67
|
if (!featureName && pObj.name) {
|
|
72
68
|
featureName = pObj.name.replace(/-e2e$/, '').replace(/^e2e-/, '');
|
|
73
69
|
}
|
|
@@ -329,6 +325,9 @@ export class TypeGenerator {
|
|
|
329
325
|
const hierarchy = PLATFORM_HIERARCHY[platform];
|
|
330
326
|
let content = `// Auto-generated ambient declarations for TestSpectra [${platform.toUpperCase()}]\n`;
|
|
331
327
|
content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
|
|
328
|
+
content += `/// <reference types="@testspectra/matchers" />\n`;
|
|
329
|
+
content += `/// <reference path="./fixtures.d.ts" />\n`;
|
|
330
|
+
content += `/// <reference path="${envRefPath}" />\n\n`;
|
|
332
331
|
// TestSpectra's Rust-native orchestrator worker (core/orchestrator/src/worker/bdd_runner.ts)
|
|
333
332
|
// registers its own `it`/`test`/lifecycle-hook globals directly — there is no WDIO or Mocha
|
|
334
333
|
// runtime involved at all (not just for component testing; this is true across every
|
|
@@ -346,9 +345,7 @@ export class TypeGenerator {
|
|
|
346
345
|
// — the Rust orchestrator's BDD runtime has no `expect` global at all.
|
|
347
346
|
content += `declare const expect: import('@testspectra/react').ExpectStatic;\n`;
|
|
348
347
|
}
|
|
349
|
-
content +=
|
|
350
|
-
content += `/// <reference path="./fixtures.d.ts" />\n`;
|
|
351
|
-
content += `/// <reference path="${envRefPath}" />\n\n`;
|
|
348
|
+
content += `\n`;
|
|
352
349
|
// 1. Page Objects Global Declarations
|
|
353
350
|
const declaredPOMs = new Set();
|
|
354
351
|
for (const poDir of poDirs) {
|
|
@@ -17,16 +17,16 @@ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
describe('findSharedLibraryRoots()', () => {
|
|
20
|
-
it('discovers parent folders of project.json files containing
|
|
20
|
+
it('discovers parent folders of project.json files containing testspectra:shared', () => {
|
|
21
21
|
const sharedDir1 = path.join(tempDir, 'shared', 'testing');
|
|
22
22
|
const sharedDir2 = path.join(tempDir, 'libs', 'common-helpers');
|
|
23
23
|
const nonSharedDir = path.join(tempDir, 'packages', 'playground', 'e2e');
|
|
24
24
|
fs.mkdirSync(sharedDir1, { recursive: true });
|
|
25
25
|
fs.mkdirSync(sharedDir2, { recursive: true });
|
|
26
26
|
fs.mkdirSync(nonSharedDir, { recursive: true });
|
|
27
|
-
fs.writeFileSync(path.join(sharedDir1, 'project.json'), JSON.stringify({ name: 'shared-testing', tags: ['
|
|
28
|
-
fs.writeFileSync(path.join(sharedDir2, 'project.json'), JSON.stringify({ name: 'common-helpers', tags: ['
|
|
29
|
-
fs.writeFileSync(path.join(nonSharedDir, 'project.json'), JSON.stringify({ name: 'playground-e2e', tags: ['
|
|
27
|
+
fs.writeFileSync(path.join(sharedDir1, 'project.json'), JSON.stringify({ name: 'shared-testing', tags: ['testspectra:shared'] }));
|
|
28
|
+
fs.writeFileSync(path.join(sharedDir2, 'project.json'), JSON.stringify({ name: 'common-helpers', tags: ['testspectra:shared'] }));
|
|
29
|
+
fs.writeFileSync(path.join(nonSharedDir, 'project.json'), JSON.stringify({ name: 'playground-e2e', tags: ['testspectra:e2e', 'testspectra:scope:playground'] }));
|
|
30
30
|
const roots = findSharedLibraryRoots(tempDir);
|
|
31
31
|
expect(roots).toHaveLength(2);
|
|
32
32
|
expect(roots).toContain(sharedDir1);
|
|
@@ -35,12 +35,12 @@ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
describe('isInsideSharedLibrary()', () => {
|
|
38
|
-
it('returns true for files inside a project tagged with
|
|
38
|
+
it('returns true for files inside a project tagged with testspectra:shared in project.json', () => {
|
|
39
39
|
const sharedDir = path.join(tempDir, 'shared', 'testing');
|
|
40
40
|
fs.mkdirSync(sharedDir, { recursive: true });
|
|
41
41
|
fs.writeFileSync(path.join(sharedDir, 'project.json'), JSON.stringify({
|
|
42
42
|
name: 'shared-testing',
|
|
43
|
-
tags: ['
|
|
43
|
+
tags: ['testspectra:shared'],
|
|
44
44
|
}));
|
|
45
45
|
const stepFile = path.join(sharedDir, 'support', 'steps', 'web.step.ts');
|
|
46
46
|
const specFile = path.join(sharedDir, 'specs', 'TC-001', 'spec.md');
|
|
@@ -55,7 +55,7 @@ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
|
|
|
55
55
|
fs.mkdirSync(customLibDir, { recursive: true });
|
|
56
56
|
fs.writeFileSync(path.join(customLibDir, 'project.json'), JSON.stringify({
|
|
57
57
|
name: 'custom-common-lib',
|
|
58
|
-
tags: ['
|
|
58
|
+
tags: ['testspectra:shared'],
|
|
59
59
|
}));
|
|
60
60
|
const actionFile = path.join(customLibDir, 'support', 'actions', 'click.action.ts');
|
|
61
61
|
expect(isInsideSharedLibrary(actionFile)).toBe(true);
|
|
@@ -65,7 +65,7 @@ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
|
|
|
65
65
|
fs.mkdirSync(sharedAppDir, { recursive: true });
|
|
66
66
|
fs.writeFileSync(path.join(sharedAppDir, 'project.json'), JSON.stringify({
|
|
67
67
|
name: 'shared-app',
|
|
68
|
-
tags: ['
|
|
68
|
+
tags: ['testspectra:e2e', 'testspectra:scope:shared-app'],
|
|
69
69
|
}));
|
|
70
70
|
const specFile = path.join(sharedAppDir, 'specs', 'TC-001', 'spec.md');
|
|
71
71
|
expect(isInsideSharedLibrary(specFile)).toBe(false);
|
|
@@ -75,7 +75,7 @@ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
|
|
|
75
75
|
fs.mkdirSync(featureDir, { recursive: true });
|
|
76
76
|
fs.writeFileSync(path.join(featureDir, 'project.json'), JSON.stringify({
|
|
77
77
|
name: 'playground-e2e',
|
|
78
|
-
tags: ['
|
|
78
|
+
tags: ['testspectra:e2e', 'testspectra:scope:playground'],
|
|
79
79
|
}));
|
|
80
80
|
const specFile = path.join(featureDir, 'specs', 'TC-001', 'spec.md');
|
|
81
81
|
expect(isInsideSharedLibrary(specFile)).toBe(false);
|
|
@@ -91,7 +91,7 @@ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
|
|
|
91
91
|
fs.mkdirSync(sharedLibDir, { recursive: true });
|
|
92
92
|
fs.writeFileSync(path.join(sharedLibDir, 'project.json'), JSON.stringify({
|
|
93
93
|
name: 'shared-testing',
|
|
94
|
-
tags: ['
|
|
94
|
+
tags: ['testspectra:shared'],
|
|
95
95
|
}));
|
|
96
96
|
const sharedSpecDir = path.join(sharedLibDir, 'specs', 'TC-error');
|
|
97
97
|
fs.mkdirSync(sharedSpecDir, { recursive: true });
|
|
@@ -119,7 +119,7 @@ status: draft
|
|
|
119
119
|
fs.mkdirSync(sharedLibDir, { recursive: true });
|
|
120
120
|
fs.writeFileSync(path.join(sharedLibDir, 'project.json'), JSON.stringify({
|
|
121
121
|
name: 'shared-testing',
|
|
122
|
-
tags: ['
|
|
122
|
+
tags: ['testspectra:shared'],
|
|
123
123
|
}));
|
|
124
124
|
const sharedSuiteDir = path.join(sharedLibDir, 'support');
|
|
125
125
|
fs.mkdirSync(sharedSuiteDir, { recursive: true });
|
|
@@ -141,7 +141,7 @@ status: active
|
|
|
141
141
|
fs.mkdirSync(customLibDir, { recursive: true });
|
|
142
142
|
fs.writeFileSync(path.join(customLibDir, 'project.json'), JSON.stringify({
|
|
143
143
|
name: 'my-testing-utils',
|
|
144
|
-
tags: ['
|
|
144
|
+
tags: ['testspectra:shared'],
|
|
145
145
|
}));
|
|
146
146
|
const specDir = path.join(customLibDir, 'specs', 'TC-custom');
|
|
147
147
|
fs.mkdirSync(specDir, { recursive: true });
|
|
@@ -169,7 +169,7 @@ status: draft
|
|
|
169
169
|
fs.mkdirSync(featureDir, { recursive: true });
|
|
170
170
|
fs.writeFileSync(path.join(featureDir, 'project.json'), JSON.stringify({
|
|
171
171
|
name: 'playground-e2e',
|
|
172
|
-
tags: ['
|
|
172
|
+
tags: ['testspectra:e2e', 'testspectra:scope:playground'],
|
|
173
173
|
}));
|
|
174
174
|
const featureSpecDir = path.join(featureDir, 'specs', 'TC-001');
|
|
175
175
|
fs.mkdirSync(featureSpecDir, { recursive: true });
|
|
@@ -195,7 +195,7 @@ status: draft
|
|
|
195
195
|
fs.mkdirSync(sharedE2eDir, { recursive: true });
|
|
196
196
|
fs.writeFileSync(path.join(sharedE2eDir, 'project.json'), JSON.stringify({
|
|
197
197
|
name: 'shared-feature-e2e',
|
|
198
|
-
tags: ['
|
|
198
|
+
tags: ['testspectra:e2e', 'testspectra:scope:shared-feature'],
|
|
199
199
|
}));
|
|
200
200
|
const specDir = path.join(sharedE2eDir, 'specs', 'TC-001');
|
|
201
201
|
fs.mkdirSync(specDir, { recursive: true });
|
|
@@ -241,7 +241,7 @@ status: draft
|
|
|
241
241
|
fs.mkdirSync(sharedLibDir, { recursive: true });
|
|
242
242
|
fs.writeFileSync(path.join(sharedLibDir, 'project.json'), JSON.stringify({
|
|
243
243
|
name: 'shared-testing',
|
|
244
|
-
tags: ['
|
|
244
|
+
tags: ['testspectra:shared'],
|
|
245
245
|
}));
|
|
246
246
|
const sharedDir1 = path.join(sharedLibDir, 'specs', 'TC-001');
|
|
247
247
|
const sharedDir2 = path.join(sharedLibDir, 'specs', 'TC-002');
|
|
@@ -4,14 +4,14 @@ export interface WorkspaceAssets {
|
|
|
4
4
|
fixtures: Set<string>;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Scans a workspace directory for all `project.json` files that have
|
|
8
|
-
* "
|
|
7
|
+
* Scans a workspace directory for all `project.json` files that have the
|
|
8
|
+
* "testspectra:shared" tag, and returns their parent folder paths.
|
|
9
9
|
*/
|
|
10
10
|
export declare function findSharedLibraryRoots(workspaceDir: string): string[];
|
|
11
11
|
/**
|
|
12
12
|
* Resolves the root directory of a shared testing library if the file path is inside one.
|
|
13
13
|
* Determines shared library status based on `project.json` containing
|
|
14
|
-
*
|
|
14
|
+
* the "testspectra:shared" tag in ancestor directories,
|
|
15
15
|
* taking the parent folder of that `project.json` as the shared library root.
|
|
16
16
|
*/
|
|
17
17
|
export declare function getSharedLibraryRoot(filePath: string, workspaceDir?: string): string | null;
|
package/dist/linter/discovery.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
/**
|
|
4
|
-
* Scans a workspace directory for all `project.json` files that have
|
|
5
|
-
* "
|
|
4
|
+
* Scans a workspace directory for all `project.json` files that have the
|
|
5
|
+
* "testspectra:shared" tag, and returns their parent folder paths.
|
|
6
6
|
*/
|
|
7
7
|
export function findSharedLibraryRoots(workspaceDir) {
|
|
8
8
|
const sharedRoots = [];
|
|
@@ -23,7 +23,7 @@ export function findSharedLibraryRoots(workspaceDir) {
|
|
|
23
23
|
const raw = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
|
|
24
24
|
const parsed = JSON.parse(raw);
|
|
25
25
|
const tags = Array.isArray(parsed.tags) ? parsed.tags : [];
|
|
26
|
-
if (tags.includes('
|
|
26
|
+
if (tags.includes('testspectra:shared')) {
|
|
27
27
|
sharedRoots.push(dir);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -39,7 +39,7 @@ export function findSharedLibraryRoots(workspaceDir) {
|
|
|
39
39
|
/**
|
|
40
40
|
* Resolves the root directory of a shared testing library if the file path is inside one.
|
|
41
41
|
* Determines shared library status based on `project.json` containing
|
|
42
|
-
*
|
|
42
|
+
* the "testspectra:shared" tag in ancestor directories,
|
|
43
43
|
* taking the parent folder of that `project.json` as the shared library root.
|
|
44
44
|
*/
|
|
45
45
|
export function getSharedLibraryRoot(filePath, workspaceDir) {
|
|
@@ -67,7 +67,7 @@ export function getSharedLibraryRoot(filePath, workspaceDir) {
|
|
|
67
67
|
const raw = fs.readFileSync(projectJsonPath, 'utf-8');
|
|
68
68
|
const projectConfig = JSON.parse(raw);
|
|
69
69
|
const tags = Array.isArray(projectConfig.tags) ? projectConfig.tags : [];
|
|
70
|
-
if (tags.includes('
|
|
70
|
+
if (tags.includes('testspectra:shared')) {
|
|
71
71
|
return current;
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
@@ -11,11 +11,11 @@ export declare const CoverageSchema: z.ZodObject<{
|
|
|
11
11
|
web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
12
12
|
mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
|
-
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
15
14
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
16
|
-
}, {
|
|
17
15
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
16
|
+
}, {
|
|
18
17
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
18
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
19
19
|
}>;
|
|
20
20
|
export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
21
21
|
id: z.ZodString;
|
|
@@ -27,33 +27,33 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
|
27
27
|
web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
28
28
|
mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
29
29
|
}, "strip", z.ZodTypeAny, {
|
|
30
|
-
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
31
30
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
32
|
-
}, {
|
|
33
31
|
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
32
|
+
}, {
|
|
34
33
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
34
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
37
37
|
}, "strip", z.ZodTypeAny, {
|
|
38
|
-
status: "draft" | "ready-for-automation" | "automated" | "deprecated";
|
|
39
|
-
title: string;
|
|
40
38
|
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
status: "draft" | "ready-for-automation" | "automated" | "deprecated";
|
|
41
41
|
priority: "Critical" | "High" | "Medium" | "Low";
|
|
42
42
|
caseType: "Positive" | "Negative" | "Edge";
|
|
43
43
|
coverage?: {
|
|
44
|
-
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
45
44
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
45
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
46
46
|
} | undefined;
|
|
47
47
|
tags?: string[] | undefined;
|
|
48
48
|
}, {
|
|
49
|
-
status: "draft" | "ready-for-automation" | "automated" | "deprecated";
|
|
50
|
-
title: string;
|
|
51
49
|
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
status: "draft" | "ready-for-automation" | "automated" | "deprecated";
|
|
52
52
|
priority: "Critical" | "High" | "Medium" | "Low";
|
|
53
53
|
caseType: "Positive" | "Negative" | "Edge";
|
|
54
54
|
coverage?: {
|
|
55
|
-
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
56
55
|
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
56
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
57
57
|
} | undefined;
|
|
58
58
|
tags?: string[] | undefined;
|
|
59
59
|
}>;
|
|
@@ -14,36 +14,36 @@ export declare const SuiteFrontmatterSchema: z.ZodEffects<z.ZodObject<{
|
|
|
14
14
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
15
15
|
}, "strip", z.ZodTypeAny, {
|
|
16
16
|
id: string;
|
|
17
|
-
status?: "active" | "draft" | "deprecated" | undefined;
|
|
18
|
-
title?: string | undefined;
|
|
19
17
|
name?: string | undefined;
|
|
18
|
+
title?: string | undefined;
|
|
19
|
+
status?: "draft" | "deprecated" | "active" | undefined;
|
|
20
20
|
tags?: string[] | undefined;
|
|
21
21
|
description?: string | undefined;
|
|
22
22
|
executionOrder?: number | undefined;
|
|
23
23
|
parallel?: boolean | undefined;
|
|
24
24
|
}, {
|
|
25
25
|
id: string;
|
|
26
|
-
status?: "active" | "draft" | "deprecated" | undefined;
|
|
27
|
-
title?: string | undefined;
|
|
28
26
|
name?: string | undefined;
|
|
27
|
+
title?: string | undefined;
|
|
28
|
+
status?: "draft" | "deprecated" | "active" | undefined;
|
|
29
29
|
tags?: string[] | undefined;
|
|
30
30
|
description?: string | undefined;
|
|
31
31
|
executionOrder?: number | undefined;
|
|
32
32
|
parallel?: boolean | undefined;
|
|
33
33
|
}>, {
|
|
34
34
|
id: string;
|
|
35
|
-
status?: "active" | "draft" | "deprecated" | undefined;
|
|
36
|
-
title?: string | undefined;
|
|
37
35
|
name?: string | undefined;
|
|
36
|
+
title?: string | undefined;
|
|
37
|
+
status?: "draft" | "deprecated" | "active" | undefined;
|
|
38
38
|
tags?: string[] | undefined;
|
|
39
39
|
description?: string | undefined;
|
|
40
40
|
executionOrder?: number | undefined;
|
|
41
41
|
parallel?: boolean | undefined;
|
|
42
42
|
}, {
|
|
43
43
|
id: string;
|
|
44
|
-
status?: "active" | "draft" | "deprecated" | undefined;
|
|
45
|
-
title?: string | undefined;
|
|
46
44
|
name?: string | undefined;
|
|
45
|
+
title?: string | undefined;
|
|
46
|
+
status?: "draft" | "deprecated" | "active" | undefined;
|
|
47
47
|
tags?: string[] | undefined;
|
|
48
48
|
description?: string | undefined;
|
|
49
49
|
executionOrder?: number | undefined;
|
|
@@ -50,6 +50,11 @@ function formatCollectionTarget(target) {
|
|
|
50
50
|
export function formatAction(actionKey, target, args = {}) {
|
|
51
51
|
const el = formatElementTarget(target);
|
|
52
52
|
switch (actionKey) {
|
|
53
|
+
// Component testing only (@testspectra/react's Spectra.mount()) — target is a component name,
|
|
54
|
+
// not a DOM selector, so it's rendered plainly rather than through formatElementTarget's
|
|
55
|
+
// `element "..."` wrapping.
|
|
56
|
+
case 'mount':
|
|
57
|
+
return `Mount component "${extractTargetString(target) || '{component}'}"`;
|
|
53
58
|
case 'click':
|
|
54
59
|
return `Click on ${el}`;
|
|
55
60
|
case 'doubleClick':
|
package/dist/runner/bridge.js
CHANGED
|
@@ -1,15 +1,60 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = path.dirname(__filename);
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
// Maps process.platform/process.arch to the optionalDependency package that carries the
|
|
10
|
+
// prebuilt native binary for that platform (see cli/npm/*). Keep in sync with the
|
|
11
|
+
// optionalDependencies list in cli/package.json and the packages built by
|
|
12
|
+
// scripts/release-cli-local.sh.
|
|
13
|
+
function platformPackageName() {
|
|
14
|
+
const { platform, arch } = process;
|
|
15
|
+
if (platform === 'darwin' && arch === 'arm64')
|
|
16
|
+
return '@testspectra/cli-darwin-arm64';
|
|
17
|
+
if (platform === 'linux' && arch === 'x64')
|
|
18
|
+
return '@testspectra/cli-linux-x64';
|
|
19
|
+
if (platform === 'win32' && arch === 'arm64')
|
|
20
|
+
return '@testspectra/cli-win32-arm64';
|
|
21
|
+
if (platform === 'win32' && arch === 'x64')
|
|
22
|
+
return '@testspectra/cli-win32-x64';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
7
25
|
export class RustCoreBridge {
|
|
8
26
|
static resolveBinaryPath() {
|
|
9
|
-
// 1. Look for precompiled release/debug binary in workspace target
|
|
10
|
-
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
11
27
|
const isWindows = process.platform === 'win32';
|
|
12
28
|
const binaryName = isWindows ? 'testspectra-runner.exe' : 'testspectra-runner';
|
|
29
|
+
// 1. Production install: resolve the prebuilt binary from the platform-specific
|
|
30
|
+
// optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
|
|
31
|
+
// published npm package ships native binaries for every supported platform without
|
|
32
|
+
// bundling all of them into @testspectra/cli itself.
|
|
33
|
+
const pkgName = platformPackageName();
|
|
34
|
+
if (pkgName) {
|
|
35
|
+
try {
|
|
36
|
+
const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
|
|
37
|
+
const pkgBinPath = path.join(path.dirname(pkgJsonPath), 'bin', binaryName);
|
|
38
|
+
if (fs.existsSync(pkgBinPath)) {
|
|
39
|
+
if (process.platform !== 'win32') {
|
|
40
|
+
try {
|
|
41
|
+
const stat = fs.statSync(pkgBinPath);
|
|
42
|
+
if ((stat.mode & 0o111) === 0) {
|
|
43
|
+
fs.chmodSync(pkgBinPath, 0o755);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
return pkgBinPath;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Optional dependency not installed — fall through to the workspace dev paths below.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2. Monorepo dev fallback: look for a precompiled release/debug binary in the workspace
|
|
56
|
+
// target, as produced by scripts/ensure-runner-binary.js during local development.
|
|
57
|
+
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
13
58
|
const candidatePaths = [
|
|
14
59
|
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
15
60
|
path.join(workspaceRoot, 'target/release', binaryName),
|
|
@@ -29,8 +74,11 @@ export class RustCoreBridge {
|
|
|
29
74
|
return `${p}.exe`;
|
|
30
75
|
}
|
|
31
76
|
}
|
|
32
|
-
throw new Error(
|
|
33
|
-
|
|
77
|
+
throw new Error(pkgName
|
|
78
|
+
? `[TestSpectra] Native core runner binary not found for ${process.platform}/${process.arch}.\n` +
|
|
79
|
+
`Expected the optional dependency '${pkgName}' to provide it. Try reinstalling ` +
|
|
80
|
+
"('npm install' / 'pnpm install') without --no-optional, or build locally with 'pnpm run build:core'."
|
|
81
|
+
: `[TestSpectra] ${process.platform}/${process.arch} is not currently supported by @testspectra/cli.`);
|
|
34
82
|
}
|
|
35
83
|
static async run(options, reporter) {
|
|
36
84
|
const binPath = this.resolveBinaryPath();
|