@testspectra/cli 1.1.0-rc.0 → 1.1.0-rc.1
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/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-DOtRypCa.js +154 -0
- package/demo-app/index.html +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/run.test.js +9 -13
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +6 -1
- package/dist/commands/run.js +1 -0
- package/dist/commands/sync-types.js +1 -0
- package/dist/commands/test.d.ts +17 -0
- package/dist/commands/test.js +76 -0
- package/dist/commands/watch.js +9 -2
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +5 -109
- package/dist/config/schema.js +1 -16
- package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
- package/dist/generator/tsconfig-generator.js +42 -7
- package/dist/generator/type-generator.d.ts +24 -1
- package/dist/generator/type-generator.js +104 -8
- package/dist/index.js +15 -1
- package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
- package/dist/linter/__tests__/component-test-imports.test.js +115 -0
- package/dist/linter/extractor.d.ts +1 -0
- package/dist/linter/extractor.js +11 -1
- package/dist/linter/schemas/spec.schema.d.ts +12 -12
- package/dist/linter/schemas/suite.schema.d.ts +8 -8
- package/dist/linter/spec-linter.js +38 -3
- package/dist/reporter/semantic-formatter.js +17 -2
- package/dist/reporter/types.d.ts +1 -1
- package/dist/runner/reporter.js +7 -3
- package/dist/utils/api-server.js +46 -1
- package/package.json +4 -3
- package/templates/default/package.json +2 -2
- package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
- package/templates/default/page-objects/AuthPage/web.ts +10 -0
- package/templates/default/page-objects/MatchersPage/mobile.ts +13 -0
- package/templates/default/page-objects/MatchersPage/web.ts +5 -0
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +6 -0
- package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +6 -9
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
- package/templates/default/specs/Authentication/suite.md +9 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
- package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
- package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
- package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
- package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
- package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
- package/templates/default/spectra.config.ts +1 -16
- package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
- package/templates/default/support/actions/seedSession/web.action.ts +13 -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 +1060 -0
- package/templates/nx/.nx/workspace-data/d/server-process.json +1 -1
- package/templates/nx/.nx/workspace-data/file-map.json +116 -104
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/package.json +2 -2
- package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
- package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
- package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
- package/templates/nx/spectra.config.ts +1 -16
- package/templates/react-component/.cursorrules +17 -0
- package/templates/react-component/.vscode/extensions.json +5 -0
- package/templates/react-component/CLAUDE.md +14 -0
- package/templates/react-component/README.md +69 -0
- package/templates/react-component/fixtures/component-mock.json +12 -0
- package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
- package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
- package/templates/react-component/package.json +27 -0
- package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
- package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
- package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
- package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
- package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
- package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
- package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
- package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
- package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
- package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
- package/templates/react-component/specs/InputComponent/suite.md +12 -0
- package/templates/react-component/spectra.config.ts +27 -0
- package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
- package/templates/react-component/src/components/Button/Button.tsx +57 -0
- package/templates/react-component/src/components/Input/Input.tsx +41 -0
- package/templates/react-component/src/test-utils.tsx +23 -0
- package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
- package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
- package/templates/react-component/tsconfig.json +8 -0
- package/demo-app/assets/index-BGllUp7o.js +0 -140
|
@@ -6,19 +6,17 @@ import { TypeGenerator } from '../../types/generator.js';
|
|
|
6
6
|
describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
7
7
|
it('passes calculated dynamic worker count to RustCoreBridge payload', async () => {
|
|
8
8
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
9
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
9
10
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
10
11
|
webConfig: {
|
|
11
12
|
baseUrl: 'http://localhost:3000',
|
|
12
13
|
maxConcurrentSessions: '1',
|
|
13
14
|
headless: true,
|
|
14
15
|
implicitWait: '5000',
|
|
15
|
-
pageLoadTimeout: '30000',
|
|
16
|
-
scriptTimeout: '30000',
|
|
17
16
|
parallelizationMode: 'suite',
|
|
18
17
|
},
|
|
19
18
|
androidConfig: {},
|
|
20
19
|
iosConfig: {},
|
|
21
|
-
browsers: [],
|
|
22
20
|
executionConfig: {},
|
|
23
21
|
});
|
|
24
22
|
let bridgeOptionsReceived = null;
|
|
@@ -39,19 +37,17 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
39
37
|
});
|
|
40
38
|
it('respects user-specified --concurrency option and passes it to RustCoreBridge', async () => {
|
|
41
39
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
40
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
42
41
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
43
42
|
webConfig: {
|
|
44
43
|
baseUrl: 'http://localhost:3000',
|
|
45
44
|
maxConcurrentSessions: '1',
|
|
46
45
|
headless: true,
|
|
47
46
|
implicitWait: '5000',
|
|
48
|
-
pageLoadTimeout: '30000',
|
|
49
|
-
scriptTimeout: '30000',
|
|
50
47
|
parallelizationMode: 'suite',
|
|
51
48
|
},
|
|
52
49
|
androidConfig: {},
|
|
53
50
|
iosConfig: {},
|
|
54
|
-
browsers: [],
|
|
55
51
|
executionConfig: {},
|
|
56
52
|
});
|
|
57
53
|
let bridgeOptionsReceived = null;
|
|
@@ -71,6 +67,7 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
71
67
|
});
|
|
72
68
|
it('correctly serializes target android and devices options into RustCoreBridge payload', async () => {
|
|
73
69
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
70
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
74
71
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
75
72
|
webConfig: {
|
|
76
73
|
baseUrl: 'http://localhost:3000',
|
|
@@ -81,7 +78,6 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
81
78
|
devices: [{ id: 'emulator-5554' }],
|
|
82
79
|
},
|
|
83
80
|
iosConfig: {},
|
|
84
|
-
browsers: [],
|
|
85
81
|
executionConfig: {},
|
|
86
82
|
});
|
|
87
83
|
let bridgeOptionsReceived = null;
|
|
@@ -109,11 +105,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
109
105
|
});
|
|
110
106
|
it('correctly serializes single --device option into devices array of objects', async () => {
|
|
111
107
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
108
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
112
109
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
113
110
|
webConfig: { baseUrl: 'http://localhost:3000' },
|
|
114
111
|
androidConfig: { driverServer: 'http://127.0.0.1:4800' },
|
|
115
112
|
iosConfig: {},
|
|
116
|
-
browsers: [],
|
|
117
113
|
executionConfig: {},
|
|
118
114
|
});
|
|
119
115
|
let bridgeOptionsReceived = null;
|
|
@@ -138,11 +134,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
138
134
|
});
|
|
139
135
|
it('correctly serializes --all-devices flag into auto devices in config', async () => {
|
|
140
136
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
137
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
141
138
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
142
139
|
webConfig: { baseUrl: 'http://localhost:3000' },
|
|
143
140
|
androidConfig: { driverServer: 'http://127.0.0.1:4800' },
|
|
144
141
|
iosConfig: {},
|
|
145
|
-
browsers: [],
|
|
146
142
|
executionConfig: {},
|
|
147
143
|
});
|
|
148
144
|
let bridgeOptionsReceived = null;
|
|
@@ -167,11 +163,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
167
163
|
});
|
|
168
164
|
it('correctly serializes --multi-device flag into auto devices in config', async () => {
|
|
169
165
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
166
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
170
167
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
171
168
|
webConfig: { baseUrl: 'http://localhost:3000' },
|
|
172
169
|
androidConfig: { driverServer: 'http://127.0.0.1:4800' },
|
|
173
170
|
iosConfig: {},
|
|
174
|
-
browsers: [],
|
|
175
171
|
executionConfig: {},
|
|
176
172
|
});
|
|
177
173
|
let bridgeOptionsReceived = null;
|
|
@@ -196,11 +192,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
196
192
|
});
|
|
197
193
|
it('sets process.exitCode to 1 when tests fail', async () => {
|
|
198
194
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
195
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
199
196
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
200
197
|
webConfig: { baseUrl: 'http://localhost:3000' },
|
|
201
198
|
androidConfig: {},
|
|
202
199
|
iosConfig: {},
|
|
203
|
-
browsers: [],
|
|
204
200
|
executionConfig: {},
|
|
205
201
|
});
|
|
206
202
|
vi.spyOn(RustCoreBridge, 'run').mockImplementation(async () => {
|
|
@@ -220,11 +216,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
220
216
|
});
|
|
221
217
|
it('incorporates TESTSPECTRA_CARRIED_OVER into summary card calculation', async () => {
|
|
222
218
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
219
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
223
220
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
224
221
|
webConfig: { baseUrl: 'http://localhost:3000' },
|
|
225
222
|
androidConfig: {},
|
|
226
223
|
iosConfig: {},
|
|
227
|
-
browsers: [],
|
|
228
224
|
executionConfig: {},
|
|
229
225
|
});
|
|
230
226
|
vi.spyOn(RustCoreBridge, 'run').mockImplementation(async () => {
|
|
@@ -253,11 +249,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
|
|
|
253
249
|
});
|
|
254
250
|
it('correctly passes multiple specs to RustCoreBridge payload', async () => {
|
|
255
251
|
vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
|
|
252
|
+
vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
|
|
256
253
|
vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
|
|
257
254
|
webConfig: { baseUrl: 'http://localhost:3000' },
|
|
258
255
|
androidConfig: {},
|
|
259
256
|
iosConfig: {},
|
|
260
|
-
browsers: [],
|
|
261
257
|
executionConfig: {},
|
|
262
258
|
});
|
|
263
259
|
let bridgeOptionsReceived = null;
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -215,10 +215,15 @@ export async function initCommand(options = {}) {
|
|
|
215
215
|
hint: 'Interactive workspace feature discovery & centralized config',
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
|
-
label: 'Standard Project',
|
|
218
|
+
label: 'Standard E2E Project',
|
|
219
219
|
value: 'default',
|
|
220
220
|
hint: 'Standalone single-suite E2E testing project',
|
|
221
221
|
},
|
|
222
|
+
{
|
|
223
|
+
label: 'React Component Testing',
|
|
224
|
+
value: 'react-component',
|
|
225
|
+
hint: 'Real-browser React component testing with Vite & Testing Library',
|
|
226
|
+
},
|
|
222
227
|
],
|
|
223
228
|
});
|
|
224
229
|
if (p.isCancel(templateChoice)) {
|
package/dist/commands/run.js
CHANGED
|
@@ -125,6 +125,7 @@ export async function runCommand(specPaths, options = {}) {
|
|
|
125
125
|
const effectiveSpecPaths = specPaths && specPaths.length > 0 ? specPaths : options.spec ? [options.spec] : undefined;
|
|
126
126
|
// Ensure latest ambient types are up-to-date
|
|
127
127
|
TypeGenerator.writeDeclarationFiles(cwd);
|
|
128
|
+
await TypeGenerator.syncEnvDeclaration(cwd);
|
|
128
129
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
129
130
|
const globalAppDataPath = path.join(home, '.testspectra');
|
|
130
131
|
const appDataPath = options.workdir ? path.resolve(options.workdir) : globalAppDataPath;
|
|
@@ -5,6 +5,7 @@ export async function syncTypesCommand(options = {}) {
|
|
|
5
5
|
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
6
6
|
try {
|
|
7
7
|
TypeGenerator.generateAll(cwd);
|
|
8
|
+
await TypeGenerator.syncEnvDeclaration(cwd);
|
|
8
9
|
console.log(chalk.green('✔ Successfully synchronized TestSpectra ambient type declarations in .testspectra/types/'));
|
|
9
10
|
}
|
|
10
11
|
catch (err) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TestCommandOptions {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
headed?: boolean;
|
|
4
|
+
headless?: boolean;
|
|
5
|
+
stepDelay?: string | number;
|
|
6
|
+
delay?: string | number;
|
|
7
|
+
spec?: string;
|
|
8
|
+
output?: string;
|
|
9
|
+
concurrency?: string | number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
|
|
13
|
+
* component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
|
|
14
|
+
* docs/v2/features/component-testing/react/README.md §2 for why these are two independent
|
|
15
|
+
* runners rather than one extended pipeline.
|
|
16
|
+
*/
|
|
17
|
+
export declare function testCommand(options?: TestCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import { runSystemChecks } from './doctor.js';
|
|
5
|
+
/**
|
|
6
|
+
* Finds the Chrome-for-Testing binary that `spectra doctor` already installs/manages for the
|
|
7
|
+
* E2E CDP driver (`core/driver-cdp`) — same install, same binary. `@testspectra/react` must not
|
|
8
|
+
* contain any browser-discovery logic of its own (see the package-level comment in
|
|
9
|
+
* tools/react/vitest.config.ts): this is the one place that resolves the path, passed down to
|
|
10
|
+
* the bundled Vitest/Playwright runner as a plain argument (env var), so component testing never
|
|
11
|
+
* requires a separate `playwright install` download alongside TestSpectra's own browser.
|
|
12
|
+
*/
|
|
13
|
+
async function resolveBrowserExecutable(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
const { dependencies } = await runSystemChecks(cwd);
|
|
16
|
+
const chrome = dependencies.find((d) => d.category === 'web' && (d.name.includes('Chrome') || d.name.includes('Chromium')));
|
|
17
|
+
return chrome?.installed && chrome.location ? chrome.location : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
|
|
25
|
+
* component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
|
|
26
|
+
* docs/v2/features/component-testing/react/README.md §2 for why these are two independent
|
|
27
|
+
* runners rather than one extended pipeline.
|
|
28
|
+
*/
|
|
29
|
+
export async function testCommand(options = {}) {
|
|
30
|
+
const cwd = options.cwd ?? process.cwd();
|
|
31
|
+
let reactPkg;
|
|
32
|
+
try {
|
|
33
|
+
reactPkg = await import('@testspectra/react');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
p.log.error(chalk.red('✖ @testspectra/react is not installed in this workspace.'));
|
|
37
|
+
p.log.info(chalk.dim('Component testing requires the @testspectra/react package.'));
|
|
38
|
+
p.log.info(chalk.dim(' 👉 Install it: pnpm add -D @testspectra/react'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const browserExecutablePath = await resolveBrowserExecutable(cwd);
|
|
43
|
+
if (!browserExecutablePath) {
|
|
44
|
+
p.log.error(chalk.red('✖ TestSpectra-managed Chrome for Testing was not found.'));
|
|
45
|
+
p.log.info(chalk.dim('Component testing reuses the same browser install as `spectra run` (web).'));
|
|
46
|
+
p.log.info(chalk.dim(' 👉 Install it: spectra doctor --fix'));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let stepDelayMs;
|
|
51
|
+
const rawDelay = options.stepDelay ?? options.delay;
|
|
52
|
+
if (rawDelay !== undefined) {
|
|
53
|
+
const delayNum = parseInt(String(rawDelay), 10);
|
|
54
|
+
if (!isNaN(delayNum) && delayNum >= 0) {
|
|
55
|
+
stepDelayMs = delayNum;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let headless = true;
|
|
59
|
+
if (options.headed) {
|
|
60
|
+
headless = false;
|
|
61
|
+
}
|
|
62
|
+
else if (options.headless !== undefined) {
|
|
63
|
+
headless = options.headless;
|
|
64
|
+
}
|
|
65
|
+
const concurrency = options.concurrency !== undefined ? parseInt(String(options.concurrency), 10) : undefined;
|
|
66
|
+
const ok = await reactPkg.runComponentTests({
|
|
67
|
+
cwd,
|
|
68
|
+
headless,
|
|
69
|
+
stepDelayMs,
|
|
70
|
+
spec: options.spec,
|
|
71
|
+
outputFile: options.output ? path.resolve(cwd, options.output) : undefined,
|
|
72
|
+
concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
|
|
73
|
+
browserExecutablePath,
|
|
74
|
+
});
|
|
75
|
+
process.exit(ok ? 0 : 1);
|
|
76
|
+
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -3,15 +3,22 @@ import path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { TypeGenerator } from '../types/generator.js';
|
|
5
5
|
export function startWatcher(cwd, debounceMs = 60) {
|
|
6
|
-
// Initial generation
|
|
6
|
+
// Initial generation. TypeGenerator.generateAll itself stays synchronous (reading
|
|
7
|
+
// spectra.config.ts is unavoidably async, so that part is handled by the separate
|
|
8
|
+
// syncEnvDeclaration call below); startWatcher stays sync so it can return the watcher handle
|
|
9
|
+
// immediately, with the async env sync fired-and-forgotten.
|
|
7
10
|
TypeGenerator.generateAll(cwd);
|
|
11
|
+
TypeGenerator.syncEnvDeclaration(cwd).catch((err) => {
|
|
12
|
+
console.error(chalk.red(`✖ Error generating initial env.d.ts: ${err?.message}`));
|
|
13
|
+
});
|
|
8
14
|
let debounceTimer = null;
|
|
9
15
|
const triggerRegen = (filePath, eventType) => {
|
|
10
16
|
if (debounceTimer)
|
|
11
17
|
clearTimeout(debounceTimer);
|
|
12
|
-
debounceTimer = setTimeout(() => {
|
|
18
|
+
debounceTimer = setTimeout(async () => {
|
|
13
19
|
try {
|
|
14
20
|
TypeGenerator.generateAll(cwd);
|
|
21
|
+
await TypeGenerator.syncEnvDeclaration(cwd);
|
|
15
22
|
const time = new Date().toLocaleTimeString();
|
|
16
23
|
const rel = path.relative(cwd, filePath) || filePath;
|
|
17
24
|
console.log(chalk.gray(`[${time}] `) + chalk.cyan(`✨ Updated ambient types `) + chalk.gray(`(${eventType}: ${rel})`));
|
package/dist/config/loader.js
CHANGED
package/dist/config/schema.d.ts
CHANGED
|
@@ -20,19 +20,11 @@ export interface WebConfig {
|
|
|
20
20
|
headless: boolean | 'new';
|
|
21
21
|
/**
|
|
22
22
|
* Implicit wait timeout (in milliseconds) for finding elements before throwing an error.
|
|
23
|
+
* Also the ceiling auto-waiting element assertions/actions poll against (capped at 2000ms for
|
|
24
|
+
* assertions — see `getAssertionTimeoutMs()`).
|
|
23
25
|
* @default "5000"
|
|
24
26
|
*/
|
|
25
27
|
implicitWait: string;
|
|
26
|
-
/**
|
|
27
|
-
* Maximum time (in milliseconds) allowed for a webpage to complete navigation and initial load.
|
|
28
|
-
* @default "30000"
|
|
29
|
-
*/
|
|
30
|
-
pageLoadTimeout: string;
|
|
31
|
-
/**
|
|
32
|
-
* Maximum time (in milliseconds) allowed for asynchronous scripts to finish executing.
|
|
33
|
-
* @default "30000"
|
|
34
|
-
*/
|
|
35
|
-
scriptTimeout: string;
|
|
36
28
|
/**
|
|
37
29
|
* Delay between consecutive actions and commands in milliseconds (slow motion).
|
|
38
30
|
* @default 0
|
|
@@ -46,37 +38,6 @@ export interface WebConfig {
|
|
|
46
38
|
*/
|
|
47
39
|
parallelizationMode: 'testcase' | 'suite';
|
|
48
40
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Browser target definition for web test execution.
|
|
51
|
-
*/
|
|
52
|
-
export interface Browser {
|
|
53
|
-
/**
|
|
54
|
-
* Unique identifier for this browser configuration.
|
|
55
|
-
*/
|
|
56
|
-
id: string;
|
|
57
|
-
/**
|
|
58
|
-
* Browser type/engine: `"chrome"`, `"firefox"`, `"safari"`, `"edge"`.
|
|
59
|
-
* @default "chrome"
|
|
60
|
-
*/
|
|
61
|
-
type: string;
|
|
62
|
-
/**
|
|
63
|
-
* Whether to simulate mobile viewport and touch metrics.
|
|
64
|
-
* @default false
|
|
65
|
-
*/
|
|
66
|
-
mobileEmulation: boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Emulated device name when `mobileEmulation` is true (e.g. `"iPhone 14"`, `"Pixel 7"`).
|
|
69
|
-
*/
|
|
70
|
-
deviceName?: string;
|
|
71
|
-
/**
|
|
72
|
-
* Custom browser window width in pixels.
|
|
73
|
-
*/
|
|
74
|
-
width?: string;
|
|
75
|
-
/**
|
|
76
|
-
* Custom browser window height in pixels.
|
|
77
|
-
*/
|
|
78
|
-
height?: string;
|
|
79
|
-
}
|
|
80
41
|
/**
|
|
81
42
|
* Target Android device specification for multi-device parallel execution.
|
|
82
43
|
*/
|
|
@@ -260,40 +221,6 @@ export interface IosConfig {
|
|
|
260
221
|
*/
|
|
261
222
|
parallelizationMode: 'testcase' | 'suite';
|
|
262
223
|
}
|
|
263
|
-
/**
|
|
264
|
-
* Load testing stage ramp-up profile.
|
|
265
|
-
*/
|
|
266
|
-
export interface LoadStage {
|
|
267
|
-
/**
|
|
268
|
-
* Stage identifier.
|
|
269
|
-
*/
|
|
270
|
-
id: string;
|
|
271
|
-
/**
|
|
272
|
-
* Duration for this load stage (e.g. `"30s"`, `"2m"`).
|
|
273
|
-
*/
|
|
274
|
-
duration: string;
|
|
275
|
-
/**
|
|
276
|
-
* Target virtual users (VUs) to scale to during this stage.
|
|
277
|
-
*/
|
|
278
|
-
targetVUs: string;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Performance and pass/fail metric threshold definition.
|
|
282
|
-
*/
|
|
283
|
-
export interface SuccessThreshold {
|
|
284
|
-
/**
|
|
285
|
-
* Unique threshold identifier.
|
|
286
|
-
*/
|
|
287
|
-
id: string;
|
|
288
|
-
/**
|
|
289
|
-
* Performance metric type (e.g. `"http_req_duration"`, `"http_req_failed"`).
|
|
290
|
-
*/
|
|
291
|
-
metricType: string;
|
|
292
|
-
/**
|
|
293
|
-
* Maximum acceptable value (e.g. `"500ms"`, `"0.01"`).
|
|
294
|
-
*/
|
|
295
|
-
maxValue: string;
|
|
296
|
-
}
|
|
297
224
|
/**
|
|
298
225
|
* Specific domain filter for real-time network interception and recording.
|
|
299
226
|
*/
|
|
@@ -307,19 +234,6 @@ export interface MonitoredDomain {
|
|
|
307
234
|
*/
|
|
308
235
|
enabled: boolean;
|
|
309
236
|
}
|
|
310
|
-
/**
|
|
311
|
-
* Custom environment variable key-value pair injected into the test execution sandbox.
|
|
312
|
-
*/
|
|
313
|
-
export interface EnvironmentVariable {
|
|
314
|
-
/**
|
|
315
|
-
* Environment variable name (e.g. `"API_SECRET_KEY"`).
|
|
316
|
-
*/
|
|
317
|
-
key: string;
|
|
318
|
-
/**
|
|
319
|
-
* Environment variable value.
|
|
320
|
-
*/
|
|
321
|
-
value: string;
|
|
322
|
-
}
|
|
323
237
|
/**
|
|
324
238
|
* Test execution, CDP network monitoring, and sandbox environment configuration.
|
|
325
239
|
*/
|
|
@@ -344,9 +258,10 @@ export interface ExecutionConfig {
|
|
|
344
258
|
*/
|
|
345
259
|
monitoredDomains?: MonitoredDomain[];
|
|
346
260
|
/**
|
|
347
|
-
* Custom environment variables injected into the test process runtime
|
|
261
|
+
* Custom environment variables injected into the test process runtime
|
|
262
|
+
* (e.g. `{ API_SECRET_KEY: "..." }`).
|
|
348
263
|
*/
|
|
349
|
-
environmentVariables?:
|
|
264
|
+
environmentVariables?: Record<string, string>;
|
|
350
265
|
}
|
|
351
266
|
/**
|
|
352
267
|
* Root configuration data structure matching TestSpectra project schema.
|
|
@@ -356,10 +271,6 @@ export interface ConfigData {
|
|
|
356
271
|
* Web browser execution settings.
|
|
357
272
|
*/
|
|
358
273
|
webConfig: WebConfig;
|
|
359
|
-
/**
|
|
360
|
-
* List of target browsers for web automation runs.
|
|
361
|
-
*/
|
|
362
|
-
browsers: Browser[];
|
|
363
274
|
/**
|
|
364
275
|
* Android Appium driver and device options.
|
|
365
276
|
*/
|
|
@@ -368,21 +279,6 @@ export interface ConfigData {
|
|
|
368
279
|
* iOS Appium driver and device options.
|
|
369
280
|
*/
|
|
370
281
|
iosConfig: IosConfig;
|
|
371
|
-
/**
|
|
372
|
-
* Load testing parameters.
|
|
373
|
-
*/
|
|
374
|
-
loadConfig: {
|
|
375
|
-
virtualUsers: string;
|
|
376
|
-
duration: string;
|
|
377
|
-
};
|
|
378
|
-
/**
|
|
379
|
-
* Load testing stages.
|
|
380
|
-
*/
|
|
381
|
-
loadStages: LoadStage[];
|
|
382
|
-
/**
|
|
383
|
-
* Pass/fail metric thresholds.
|
|
384
|
-
*/
|
|
385
|
-
thresholds: SuccessThreshold[];
|
|
386
282
|
/**
|
|
387
283
|
* Advanced execution and network recording settings.
|
|
388
284
|
*/
|
package/dist/config/schema.js
CHANGED
|
@@ -25,17 +25,8 @@ export const DEFAULT_CONFIG_DATA = {
|
|
|
25
25
|
maxConcurrentSessions: '1',
|
|
26
26
|
headless: true,
|
|
27
27
|
implicitWait: '5000',
|
|
28
|
-
pageLoadTimeout: '30000',
|
|
29
|
-
scriptTimeout: '30000',
|
|
30
28
|
parallelizationMode: 'testcase',
|
|
31
29
|
},
|
|
32
|
-
browsers: [
|
|
33
|
-
{
|
|
34
|
-
id: 'chrome-desktop',
|
|
35
|
-
type: 'chrome',
|
|
36
|
-
mobileEmulation: false,
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
30
|
androidConfig: {
|
|
40
31
|
driverServer: 'http://127.0.0.1:4800',
|
|
41
32
|
appPackage: '',
|
|
@@ -70,17 +61,11 @@ export const DEFAULT_CONFIG_DATA = {
|
|
|
70
61
|
},
|
|
71
62
|
],
|
|
72
63
|
},
|
|
73
|
-
loadConfig: {
|
|
74
|
-
virtualUsers: '10',
|
|
75
|
-
duration: '1m',
|
|
76
|
-
},
|
|
77
|
-
loadStages: [],
|
|
78
|
-
thresholds: [],
|
|
79
64
|
executionConfig: {
|
|
80
65
|
networkMonitoringEnabled: true,
|
|
81
66
|
fastResponseTime: '200',
|
|
82
67
|
normalResponseTime: '1000',
|
|
83
68
|
monitoredDomains: [],
|
|
84
|
-
environmentVariables:
|
|
69
|
+
environmentVariables: {},
|
|
85
70
|
},
|
|
86
71
|
};
|
|
@@ -261,7 +261,7 @@ describe('Platform TSConfigs & Type Isolation (docs/v2/cli/tsconfig-platform-iso
|
|
|
261
261
|
expect(threw).toBe(true);
|
|
262
262
|
expect(errorMessage).toContain("Cannot find name 'AuthLoginPage'");
|
|
263
263
|
});
|
|
264
|
-
it('E2E: compiles the full Nx monorepo template via tsc -b with 0 errors', () => {
|
|
264
|
+
it('E2E: compiles the full Nx monorepo template via tsc -b with 0 errors', async () => {
|
|
265
265
|
let root = process.cwd();
|
|
266
266
|
while (root && root !== path.dirname(root)) {
|
|
267
267
|
if (fs.existsSync(path.join(root, 'pnpm-workspace.yaml')))
|
|
@@ -269,8 +269,12 @@ describe('Platform TSConfigs & Type Isolation (docs/v2/cli/tsconfig-platform-iso
|
|
|
269
269
|
root = path.dirname(root);
|
|
270
270
|
}
|
|
271
271
|
const nxTemplateDir = path.join(root, 'cli', 'templates', 'nx');
|
|
272
|
-
// Generate latest ambient types for the nx template
|
|
272
|
+
// Generate latest ambient types for the nx template, including env.d.ts (a separate async
|
|
273
|
+
// step — see TypeGenerator.syncEnvDeclaration — since spectra.config.ts's
|
|
274
|
+
// executionConfig.environmentVariables is what the template's EnvironmentConfig suite
|
|
275
|
+
// exercises via the typed Env global).
|
|
273
276
|
TypeGenerator.generateAll(nxTemplateDir);
|
|
277
|
+
await TypeGenerator.syncEnvDeclaration(nxTemplateDir);
|
|
274
278
|
expect(() => {
|
|
275
279
|
execSync(`npx tsc -b ${nxTemplateDir}`, { stdio: 'pipe' });
|
|
276
280
|
}).not.toThrow();
|