@testspectra/cli 1.1.7 → 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/demo-app/assets/index-BhlW-eXG.css +1 -0
- package/demo-app/assets/index-DOtRypCa.js +154 -0
- package/demo-app/index.html +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/run-e2e.test.js +254 -13
- package/dist/commands/__tests__/run.test.js +92 -16
- package/dist/commands/add.js +2 -1
- package/dist/commands/demo.js +17 -7
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +8 -2
- package/dist/commands/lint.d.ts +5 -0
- package/dist/commands/lint.js +55 -0
- package/dist/commands/refactor.d.ts +2 -1
- package/dist/commands/refactor.js +129 -35
- package/dist/commands/run.d.ts +2 -3
- package/dist/commands/run.js +93 -28
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.js +28 -0
- package/dist/commands/sync-types.js +1 -0
- package/dist/commands/test.d.ts +17 -0
- package/dist/commands/test.js +104 -0
- package/dist/commands/watch.js +9 -3
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +5 -114
- package/dist/config/schema.js +1 -17
- package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
- package/dist/generator/__tests__/type-generator.test.js +39 -1
- package/dist/generator/tsconfig-generator.js +44 -16
- package/dist/generator/type-generator.d.ts +29 -3
- package/dist/generator/type-generator.js +115 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +65 -29
- package/dist/linter/__tests__/anonymous-export.test.js +184 -0
- 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/__tests__/shared-lib-boundary.test.d.ts +1 -0
- package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
- package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
- package/dist/linter/__tests__/spec-schema.test.js +369 -0
- package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
- package/dist/linter/__tests__/step-parity.test.js +372 -0
- package/dist/linter/discovery.d.ts +30 -0
- package/dist/linter/discovery.js +249 -0
- package/dist/linter/extractor.d.ts +67 -0
- package/dist/linter/extractor.js +245 -0
- package/dist/linter/index.d.ts +7 -0
- package/dist/linter/index.js +7 -0
- package/dist/linter/parser.d.ts +24 -0
- package/dist/linter/parser.js +219 -0
- package/dist/linter/schemas/spec.schema.d.ts +83 -0
- package/dist/linter/schemas/spec.schema.js +150 -0
- package/dist/linter/schemas/suite.schema.d.ts +59 -0
- package/dist/linter/schemas/suite.schema.js +83 -0
- package/dist/linter/spec-linter.d.ts +23 -0
- package/dist/linter/spec-linter.js +630 -0
- package/dist/linter/types.d.ts +28 -0
- package/dist/linter/types.js +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
- package/dist/refactor/refactor-engine.d.ts +25 -0
- package/dist/refactor/refactor-engine.js +374 -42
- package/dist/reporter/semantic-formatter.js +22 -2
- package/dist/reporter/types.d.ts +1 -1
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +85 -24
- package/dist/runner/reporter.d.ts +2 -0
- package/dist/runner/reporter.js +11 -3
- package/dist/utils/__tests__/api-server.test.d.ts +1 -0
- package/dist/utils/__tests__/api-server.test.js +62 -0
- package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
- package/dist/utils/__tests__/demo-server.test.js +35 -0
- package/dist/utils/api-server.d.ts +19 -0
- package/dist/utils/api-server.js +179 -0
- package/dist/utils/demo-server.d.ts +5 -0
- package/dist/utils/demo-server.js +8 -1
- package/package.json +13 -4
- package/templates/default/CLAUDE.md +5 -0
- package/templates/default/package.json +3 -3
- 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 +26 -2
- package/templates/default/page-objects/MatchersPage/web.ts +8 -2
- package/templates/default/page-objects/NavigationPage/mobile.ts +1 -1
- package/templates/default/page-objects/NavigationPage/web.ts +1 -1
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +12 -6
- package/templates/default/page-objects/PlaygroundPage/web.ts +2 -2
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +45 -3
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +7 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +8 -4
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +13 -2
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +7 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +6 -4
- package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
- package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
- package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
- package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
- package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
- package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
- package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
- package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
- package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
- package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
- package/templates/default/specs/ApiInterception/suite.md +1 -0
- package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
- package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
- package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
- package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
- package/templates/default/specs/ApiSeeding/suite.md +9 -0
- 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-0007-title-and-navigation/mobile.test.ts +1 -1
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
- 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-0005-text-and-values/common.test.ts +26 -0
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
- 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/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
- package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
- package/templates/default/specs/DeepLink/suite.md +9 -0
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/suite.md +2 -1
- 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/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
- package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
- package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
- package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
- package/templates/default/specs/PermissionRationale/suite.md +9 -0
- package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
- package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
- package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
- package/templates/default/spectra.config.ts +1 -17
- package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
- package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
- package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
- package/templates/default/support/actions/seedSession/web.action.ts +13 -0
- package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +1 -1
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +1 -1
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +5803 -168
- package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
- package/templates/nx/.nx/workspace-data/file-map.json +464 -0
- package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
- package/templates/nx/.nx/workspace-data/project-graph.json +14388 -0
- package/templates/nx/package.json +3 -3
- package/templates/nx/packages/matchers/e2e/project.json +2 -2
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
- 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/packages/playground/e2e/project.json +2 -2
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
- package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
- package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
- package/templates/nx/shared/testing/project.json +1 -2
- package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
- package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
- package/templates/nx/spectra.config.ts +1 -17
- 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/bin/.testspectra-runner.hash +0 -1
- package/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-DPfv5_EF.js +0 -116
- package/demo-app/assets/index-gIwu2EMV.css +0 -1
- package/dist/commands/__tests__/cloud-run.test.js +0 -93
- package/dist/commands/cloud-run.d.ts +0 -13
- package/dist/commands/cloud-run.js +0 -120
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
- /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
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)) {
|
|
@@ -503,6 +508,7 @@ export async function initCommand(options = {}) {
|
|
|
503
508
|
copyRecursive(sampleE2eSrc, targetE2eDir, {
|
|
504
509
|
'playground-e2e': e2eProjectName,
|
|
505
510
|
'packages/playground/e2e': path.relative(cwd, targetE2eDir).replace(/\\/g, '/'),
|
|
511
|
+
'testspectra:scope:playground': `testspectra:scope:${featureName}`,
|
|
506
512
|
'scope:playground': `scope:${featureName}`,
|
|
507
513
|
});
|
|
508
514
|
// Adjust project.json
|
|
@@ -514,7 +520,7 @@ export async function initCommand(options = {}) {
|
|
|
514
520
|
pObj.sourceRoot = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
|
|
515
521
|
pObj.targets.e2e.options.cwd = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
|
|
516
522
|
pObj.targets['type-check'].options.cwd = '.';
|
|
517
|
-
pObj.tags = [`scope:${featureName}
|
|
523
|
+
pObj.tags = ['testspectra:e2e', `testspectra:scope:${featureName}`];
|
|
518
524
|
fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
|
|
519
525
|
}
|
|
520
526
|
catch { }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { SpecLinter } from '../linter/spec-linter.js';
|
|
4
|
+
export async function lintCommand(targetPath, options = {}) {
|
|
5
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
6
|
+
try {
|
|
7
|
+
const result = SpecLinter.validateWorkspace(cwd, targetPath);
|
|
8
|
+
if (options.json) {
|
|
9
|
+
console.log(JSON.stringify(result, null, 2));
|
|
10
|
+
if (!result.valid || (options.strict && result.stats.warningCount > 0)) {
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.log(chalk.bold('\n🔍 TestSpectra Spec & Code Linter\n'));
|
|
16
|
+
// Group diagnostics by file
|
|
17
|
+
const diagnosticsByFile = new Map();
|
|
18
|
+
for (const diag of result.diagnostics) {
|
|
19
|
+
const relPath = path.relative(cwd, diag.filePath);
|
|
20
|
+
const list = diagnosticsByFile.get(relPath) || [];
|
|
21
|
+
list.push(diag);
|
|
22
|
+
diagnosticsByFile.set(relPath, list);
|
|
23
|
+
}
|
|
24
|
+
if (diagnosticsByFile.size === 0) {
|
|
25
|
+
console.log(chalk.green(` ✓ ${result.stats.totalSpecs} specs and ${result.stats.totalSuites} suites verified.`));
|
|
26
|
+
console.log(chalk.green.bold('\n✓ All spec schemas are valid. 0 lint errors found.\n'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const [file, diags] of diagnosticsByFile.entries()) {
|
|
30
|
+
console.log(chalk.bold(` ${file}`));
|
|
31
|
+
for (const diag of diags) {
|
|
32
|
+
const icon = diag.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
|
|
33
|
+
const codeTag = chalk.gray(`[${diag.code}]`);
|
|
34
|
+
const loc = chalk.dim(`:${diag.line}`);
|
|
35
|
+
console.log(` ${icon} ${codeTag} ${diag.message} ${loc}`);
|
|
36
|
+
}
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
const { errorCount, warningCount } = result.stats;
|
|
40
|
+
if (errorCount > 0) {
|
|
41
|
+
console.log(chalk.red.bold(`✖ ${errorCount} lint error${errorCount > 1 ? 's' : ''}${warningCount > 0 ? ` and ${warningCount} warning${warningCount > 1 ? 's' : ''}` : ''} found.\n`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
else if (warningCount > 0) {
|
|
45
|
+
console.log(chalk.yellow.bold(`⚠ ${warningCount} lint warning${warningCount > 1 ? 's' : ''} found.\n`));
|
|
46
|
+
if (options.strict) {
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error(chalk.red(`\n✖ Fatal error during linting: ${err?.message || err}\n`));
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RefactorResult } from '../refactor/refactor-engine.js';
|
|
1
2
|
export interface RefactorCommandOptions {
|
|
2
3
|
cwd?: string;
|
|
3
4
|
dryRun?: boolean;
|
|
@@ -5,4 +6,4 @@ export interface RefactorCommandOptions {
|
|
|
5
6
|
from?: string;
|
|
6
7
|
to?: string;
|
|
7
8
|
}
|
|
8
|
-
export declare function refactorCommand(entityType?: string, oldName?: string, newName?: string, options?: RefactorCommandOptions): Promise<
|
|
9
|
+
export declare function refactorCommand(entityType?: string, oldName?: string, newName?: string, options?: RefactorCommandOptions): Promise<RefactorResult>;
|
|
@@ -5,52 +5,142 @@ export async function refactorCommand(entityType, oldName, newName, options = {}
|
|
|
5
5
|
let targetType = 'page-object';
|
|
6
6
|
let fromSymbol = oldName || '';
|
|
7
7
|
let toSymbol = newName || '';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (options.from.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
// 1. Path-based refactoring via --from and --to flags
|
|
9
|
+
if (options.from || options.to) {
|
|
10
|
+
if (!options.from || !options.to) {
|
|
11
|
+
const errorMsg = 'Both --from and --to must be specified for path-based refactoring.';
|
|
12
|
+
const result = {
|
|
13
|
+
status: 'failed',
|
|
14
|
+
entityType: 'page-object',
|
|
15
|
+
oldSymbol: options.from || '',
|
|
16
|
+
newSymbol: options.to || '',
|
|
17
|
+
modifiedFiles: [],
|
|
18
|
+
occurrencesReplaced: 0,
|
|
19
|
+
error: errorMsg,
|
|
20
|
+
};
|
|
21
|
+
if (options.json) {
|
|
22
|
+
console.log(JSON.stringify(result, null, 2));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.error(`\x1b[31m✗ ${errorMsg}\x1b[0m`);
|
|
26
|
+
}
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return result;
|
|
14
29
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
const pathResult = await RefactorEngine.renameFromPaths({
|
|
31
|
+
workspaceRoot,
|
|
32
|
+
fromPath: options.from,
|
|
33
|
+
toPath: options.to,
|
|
34
|
+
dryRun: options.dryRun,
|
|
35
|
+
});
|
|
36
|
+
if (options.json) {
|
|
37
|
+
console.log(JSON.stringify(pathResult, null, 2));
|
|
38
|
+
if (pathResult.status === 'failed') {
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
return pathResult;
|
|
19
42
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
if (pathResult.status === 'success') {
|
|
44
|
+
const modeStr = options.dryRun ? '[Dry-Run] ' : '';
|
|
45
|
+
console.log(`\x1b[32m✓ ${modeStr}Refactored ${pathResult.entityType} "${pathResult.oldSymbol}" → "${pathResult.newSymbol}"\x1b[0m`);
|
|
46
|
+
console.log(` Modified files: ${pathResult.modifiedFiles.length}`);
|
|
47
|
+
console.log(` Occurrences replaced: ${pathResult.occurrencesReplaced}`);
|
|
48
|
+
for (const f of pathResult.modifiedFiles) {
|
|
49
|
+
console.log(` ↳ ${f}`);
|
|
50
|
+
}
|
|
24
51
|
}
|
|
25
|
-
else
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
toSymbol = path.parse(options.to).name;
|
|
52
|
+
else {
|
|
53
|
+
console.error(`\x1b[31m✗ Refactoring failed: ${pathResult.error}\x1b[0m`);
|
|
54
|
+
process.exitCode = 1;
|
|
29
55
|
}
|
|
56
|
+
return pathResult;
|
|
30
57
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
// 2. Also support positional path syntax: spectra refactor <fromPath> <toPath>
|
|
59
|
+
if (entityType && oldName && !newName && RefactorEngine.parseEntityFromPath(entityType)) {
|
|
60
|
+
const pathResult = await RefactorEngine.renameFromPaths({
|
|
61
|
+
workspaceRoot,
|
|
62
|
+
fromPath: entityType,
|
|
63
|
+
toPath: oldName,
|
|
64
|
+
dryRun: options.dryRun,
|
|
65
|
+
});
|
|
66
|
+
if (options.json) {
|
|
67
|
+
console.log(JSON.stringify(pathResult, null, 2));
|
|
68
|
+
if (pathResult.status === 'failed') {
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
return pathResult;
|
|
35
72
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
if (pathResult.status === 'success') {
|
|
74
|
+
const modeStr = options.dryRun ? '[Dry-Run] ' : '';
|
|
75
|
+
console.log(`\x1b[32m✓ ${modeStr}Refactored ${pathResult.entityType} "${pathResult.oldSymbol}" → "${pathResult.newSymbol}"\x1b[0m`);
|
|
76
|
+
console.log(` Modified files: ${pathResult.modifiedFiles.length}`);
|
|
77
|
+
console.log(` Occurrences replaced: ${pathResult.occurrencesReplaced}`);
|
|
78
|
+
for (const f of pathResult.modifiedFiles) {
|
|
79
|
+
console.log(` ↳ ${f}`);
|
|
80
|
+
}
|
|
39
81
|
}
|
|
40
|
-
else
|
|
41
|
-
|
|
82
|
+
else {
|
|
83
|
+
console.error(`\x1b[31m✗ Refactoring failed: ${pathResult.error}\x1b[0m`);
|
|
84
|
+
process.exitCode = 1;
|
|
42
85
|
}
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
return pathResult;
|
|
87
|
+
}
|
|
88
|
+
// 3. Positional syntax: spectra refactor <entityType> <oldName> <newName>
|
|
89
|
+
if (!entityType || !oldName || !newName) {
|
|
90
|
+
const errorMsg = 'Usage: spectra refactor <page-object|step|action|fixture> <oldName> <newName> or spectra refactor --from <path> --to <path>';
|
|
91
|
+
const result = {
|
|
92
|
+
status: 'failed',
|
|
93
|
+
entityType: 'page-object',
|
|
94
|
+
oldSymbol: oldName || '',
|
|
95
|
+
newSymbol: newName || '',
|
|
96
|
+
modifiedFiles: [],
|
|
97
|
+
occurrencesReplaced: 0,
|
|
98
|
+
error: errorMsg,
|
|
99
|
+
};
|
|
100
|
+
if (options.json) {
|
|
101
|
+
console.log(JSON.stringify(result, null, 2));
|
|
45
102
|
}
|
|
46
|
-
else
|
|
47
|
-
|
|
103
|
+
else {
|
|
104
|
+
console.error(errorMsg);
|
|
105
|
+
}
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
const normalized = entityType.toLowerCase().replace(/_/g, '-');
|
|
110
|
+
if (['page-object', 'po', 'page', 'pageobject'].includes(normalized)) {
|
|
111
|
+
targetType = 'page-object';
|
|
112
|
+
}
|
|
113
|
+
else if (['step', 'steps'].includes(normalized)) {
|
|
114
|
+
targetType = 'step';
|
|
115
|
+
}
|
|
116
|
+
else if (['action', 'actions'].includes(normalized)) {
|
|
117
|
+
targetType = 'action';
|
|
118
|
+
}
|
|
119
|
+
else if (['fixture', 'fixtures'].includes(normalized)) {
|
|
120
|
+
targetType = 'fixture';
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const errorMsg = `Unknown refactor entity type: "${entityType}". Valid types: page-object, step, action, fixture.`;
|
|
124
|
+
const result = {
|
|
125
|
+
status: 'failed',
|
|
126
|
+
entityType: 'page-object',
|
|
127
|
+
oldSymbol: oldName,
|
|
128
|
+
newSymbol: newName,
|
|
129
|
+
modifiedFiles: [],
|
|
130
|
+
occurrencesReplaced: 0,
|
|
131
|
+
error: errorMsg,
|
|
132
|
+
};
|
|
133
|
+
if (options.json) {
|
|
134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
135
|
}
|
|
49
136
|
else {
|
|
50
|
-
console.error(
|
|
51
|
-
process.exit(1);
|
|
137
|
+
console.error(`\x1b[31m✗ ${errorMsg}\x1b[0m`);
|
|
52
138
|
}
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return result;
|
|
53
141
|
}
|
|
142
|
+
fromSymbol = oldName;
|
|
143
|
+
toSymbol = newName;
|
|
54
144
|
const result = await RefactorEngine.renameSymbol({
|
|
55
145
|
workspaceRoot,
|
|
56
146
|
type: targetType,
|
|
@@ -60,7 +150,10 @@ export async function refactorCommand(entityType, oldName, newName, options = {}
|
|
|
60
150
|
});
|
|
61
151
|
if (options.json) {
|
|
62
152
|
console.log(JSON.stringify(result, null, 2));
|
|
63
|
-
|
|
153
|
+
if (result.status === 'failed') {
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
64
157
|
}
|
|
65
158
|
if (result.status === 'success') {
|
|
66
159
|
const modeStr = options.dryRun ? '[Dry-Run] ' : '';
|
|
@@ -73,6 +166,7 @@ export async function refactorCommand(entityType, oldName, newName, options = {}
|
|
|
73
166
|
}
|
|
74
167
|
else {
|
|
75
168
|
console.error(`\x1b[31m✗ Refactoring failed: ${result.error}\x1b[0m`);
|
|
76
|
-
process.
|
|
169
|
+
process.exitCode = 1;
|
|
77
170
|
}
|
|
171
|
+
return result;
|
|
78
172
|
}
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ export interface RunCommandOptions {
|
|
|
12
12
|
workdir?: string;
|
|
13
13
|
output?: string;
|
|
14
14
|
demo?: boolean;
|
|
15
|
-
baseUrl?: string;
|
|
16
15
|
failed?: boolean | string;
|
|
17
16
|
concurrency?: string | number;
|
|
18
17
|
licenseKey?: string;
|
|
@@ -28,5 +27,5 @@ export declare function renderTimingBreakdown(performance: {
|
|
|
28
27
|
wallClockDurationMs?: number;
|
|
29
28
|
totalTests?: number;
|
|
30
29
|
duration?: string;
|
|
31
|
-
}): void;
|
|
32
|
-
export declare function runCommand(
|
|
30
|
+
}, platform?: string): void;
|
|
31
|
+
export declare function runCommand(specPaths?: string[], options?: RunCommandOptions): Promise<void>;
|
package/dist/commands/run.js
CHANGED
|
@@ -5,6 +5,7 @@ import { RustCoreBridge } from '../runner/bridge.js';
|
|
|
5
5
|
import { Reporter } from '../runner/reporter.js';
|
|
6
6
|
import { TypeGenerator } from '../types/generator.js';
|
|
7
7
|
import { DemoServer } from '../utils/demo-server.js';
|
|
8
|
+
import { DemoApiServer } from '../utils/api-server.js';
|
|
8
9
|
function renderSummaryCard(options) {
|
|
9
10
|
const width = 64;
|
|
10
11
|
const borderColor = options.isPassed ? '\x1b[38;2;52;211;153m' : '\x1b[38;2;248;113;113m';
|
|
@@ -46,6 +47,11 @@ function renderSummaryCard(options) {
|
|
|
46
47
|
const statusBadge = options.isPassed ? `${green}✓ PASSED${reset}` : `${red}✗ FAILED${reset}`;
|
|
47
48
|
const passedSuites = options.passedSuites ?? (options.isPassed ? options.suiteCount : 0);
|
|
48
49
|
const failedSuites = options.failedSuites ?? (options.isPassed ? 0 : options.suiteCount);
|
|
50
|
+
const testsPassedText = options.carriedPassed
|
|
51
|
+
? `${options.passedCount > 0 ? `${green}${options.passedCount} passed${reset}` : `${options.passedCount} passed`} ${dim}(${options.carriedPassed} carried over)${reset}`
|
|
52
|
+
: options.passedCount > 0
|
|
53
|
+
? `${green}${options.passedCount} passed${reset}`
|
|
54
|
+
: `${options.passedCount} passed`;
|
|
49
55
|
console.log(`\n${top}`);
|
|
50
56
|
console.log(headerRow(`${boldWhite}✨ TESTSPECTRA EXECUTION SUMMARY${reset}`));
|
|
51
57
|
console.log(mid);
|
|
@@ -53,7 +59,7 @@ function renderSummaryCard(options) {
|
|
|
53
59
|
console.log(row(`${dim}Suites${reset}`, `${passedSuites > 0 ? `${green}${passedSuites} passed${reset}` : `${passedSuites} passed`}, ` +
|
|
54
60
|
`${failedSuites > 0 ? `${red}${failedSuites} failed${reset}` : `${failedSuites} failed`} ` +
|
|
55
61
|
`${dim}(${options.suiteCount} total)${reset}`));
|
|
56
|
-
console.log(row(`${dim}Tests${reset}`, `${
|
|
62
|
+
console.log(row(`${dim}Tests${reset}`, `${testsPassedText}, ` +
|
|
57
63
|
`${options.failedCount > 0 ? `${red}${options.failedCount} failed${reset}` : `${options.failedCount} failed`} ` +
|
|
58
64
|
`${dim}(${options.testCount} total)${reset}`));
|
|
59
65
|
console.log(row(`${dim}Platform${reset}`, `${cyan}${options.platform}${reset}`));
|
|
@@ -64,7 +70,13 @@ function renderSummaryCard(options) {
|
|
|
64
70
|
}
|
|
65
71
|
console.log(`${bot}\n`);
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
const TIMING_PHASE_LABELS = {
|
|
74
|
+
web: { boot: '1. Master Chromium Process Boot', handshake: '3. Incognito Context & CDP Handshake' },
|
|
75
|
+
android: { boot: '1. Android Driver Bootstrap (ADB + Agent)', handshake: '3. Device Session Handshake' },
|
|
76
|
+
ios: { boot: '1. iOS Driver Bootstrap (Appium)', handshake: '3. WDA Session Handshake' },
|
|
77
|
+
};
|
|
78
|
+
export function renderTimingBreakdown(performance, platform = 'web') {
|
|
79
|
+
const phaseLabels = TIMING_PHASE_LABELS[platform] ?? TIMING_PHASE_LABELS.web;
|
|
68
80
|
const yellowBold = '\x1b[1;38;2;251;191;36m';
|
|
69
81
|
const reset = '\x1b[0m';
|
|
70
82
|
const borderColor = '\x1b[38;2;75;85;99m';
|
|
@@ -89,20 +101,31 @@ export function renderTimingBreakdown(performance) {
|
|
|
89
101
|
console.log(`${borderColor}┌──────────────────────────────────────────┬───────────────────┐${reset}`);
|
|
90
102
|
console.log(`${borderColor}│${reset} Execution Phase ${borderColor}│${reset} Duration / Detail ${borderColor}│${reset}`);
|
|
91
103
|
console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
|
|
92
|
-
console.log(`${borderColor}│${reset}
|
|
93
|
-
console.log(`${borderColor}│${reset} 2. Worker Spawning & Runtime Overhead
|
|
94
|
-
console.log(`${borderColor}│${reset}
|
|
104
|
+
console.log(`${borderColor}│${reset} ${padRight(phaseLabels.boot, 40)} ${borderColor}│${reset} ${pad(performance.masterBootLatencyMs ?? 0, 13)} ms ${borderColor}│${reset}`);
|
|
105
|
+
console.log(`${borderColor}│${reset} ${padRight('2. Worker Spawning & Runtime Overhead', 40)} ${borderColor}│${reset} ${pad(performance.runnerOverheadMs ?? 0, 13)} ms ${borderColor}│${reset}`);
|
|
106
|
+
console.log(`${borderColor}│${reset} ${padRight(phaseLabels.handshake, 40)} ${borderColor}│${reset} ${pad(contextHandshakeMs, 10)} ms (avg)${borderColor}│${reset}`);
|
|
95
107
|
console.log(`${borderColor}│${reset} ${padRight(testLabel, 40)} ${borderColor}│${reset} ${pad(testDetail, 17)} ${borderColor}│${reset}`);
|
|
96
|
-
console.log(`${borderColor}│${reset} 5. Context Teardown / Cleanup
|
|
108
|
+
console.log(`${borderColor}│${reset} ${padRight('5. Context Teardown / Cleanup', 40)} ${borderColor}│${reset} ${pad(performance.avgTeardownMs ?? 0, 10)} ms (avg)${borderColor}│${reset}`);
|
|
97
109
|
console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
|
|
98
110
|
console.log(`${borderColor}│${reset} Total Wall-Clock Execution Time ${borderColor}│${reset} ${pad(wallClock, 15)} ${borderColor}│${reset}`);
|
|
99
111
|
console.log(`${borderColor}└──────────────────────────────────────────┴───────────────────┘${reset}\n`);
|
|
100
112
|
}
|
|
101
|
-
export async function runCommand(
|
|
113
|
+
export async function runCommand(specPaths, options = {}) {
|
|
102
114
|
const cwd = process.cwd();
|
|
103
|
-
|
|
115
|
+
// Opt-in duration logging (TESTSPECTRA_DURATION_LOG): resolve to one absolute JSONL path that
|
|
116
|
+
// both this host process (Rust orchestrator) and every spawned Bun worker append to, and stamp a
|
|
117
|
+
// run id so repeated runs can be told apart. Off by default; toggled purely via the env var.
|
|
118
|
+
if (process.env.TESTSPECTRA_DURATION_LOG) {
|
|
119
|
+
const v = process.env.TESTSPECTRA_DURATION_LOG;
|
|
120
|
+
const abs = v === '1' || v === 'true' ? path.join(cwd, '.testspectra', 'logs', 'duration.jsonl') : path.resolve(cwd, v);
|
|
121
|
+
process.env.TESTSPECTRA_DURATION_LOG = abs;
|
|
122
|
+
if (!process.env.TESTSPECTRA_RUN_ID)
|
|
123
|
+
process.env.TESTSPECTRA_RUN_ID = String(Date.now());
|
|
124
|
+
}
|
|
125
|
+
const effectiveSpecPaths = specPaths && specPaths.length > 0 ? specPaths : options.spec ? [options.spec] : undefined;
|
|
104
126
|
// Ensure latest ambient types are up-to-date
|
|
105
127
|
TypeGenerator.writeDeclarationFiles(cwd);
|
|
128
|
+
await TypeGenerator.syncEnvDeclaration(cwd);
|
|
106
129
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
107
130
|
const globalAppDataPath = path.join(home, '.testspectra');
|
|
108
131
|
const appDataPath = options.workdir ? path.resolve(options.workdir) : globalAppDataPath;
|
|
@@ -135,6 +158,7 @@ export async function runCommand(specPath, options = {}) {
|
|
|
135
158
|
}
|
|
136
159
|
const config = await ConfigLoader.loadConfig(cwd);
|
|
137
160
|
let demoServer = null;
|
|
161
|
+
let apiServer = null;
|
|
138
162
|
try {
|
|
139
163
|
if (options.demo) {
|
|
140
164
|
demoServer = new DemoServer(5173);
|
|
@@ -146,9 +170,14 @@ export async function runCommand(specPath, options = {}) {
|
|
|
146
170
|
catch (err) {
|
|
147
171
|
console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo server: ${err.message}\x1b[0m`);
|
|
148
172
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
apiServer = new DemoApiServer();
|
|
174
|
+
try {
|
|
175
|
+
const { url, port } = await apiServer.start();
|
|
176
|
+
console.log(`\x1b[36m[TestSpectra]\x1b[0m 🌐 Demo API server automatically started at: \x1b[33m${url}\x1b[0m (port ${port}, self-hosted /api/*)`);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo API server: ${err.message}\x1b[0m`);
|
|
180
|
+
}
|
|
152
181
|
}
|
|
153
182
|
if (options.headed) {
|
|
154
183
|
config.webConfig.headless = false;
|
|
@@ -207,7 +236,7 @@ export async function runCommand(specPath, options = {}) {
|
|
|
207
236
|
targetDevice,
|
|
208
237
|
licenseKey: options.licenseKey,
|
|
209
238
|
outputJsonPath,
|
|
210
|
-
|
|
239
|
+
specs: effectiveSpecPaths,
|
|
211
240
|
concurrency: userConcurrency,
|
|
212
241
|
failedOnly: isFailedMode,
|
|
213
242
|
failedTarget,
|
|
@@ -216,16 +245,41 @@ export async function runCommand(specPath, options = {}) {
|
|
|
216
245
|
const relReport = path.relative(cwd, outputJsonPath);
|
|
217
246
|
const networkReportFile = path.join(path.dirname(outputJsonPath), 'network-resources.json');
|
|
218
247
|
const relNetworkReport = fs.existsSync(networkReportFile) ? path.relative(cwd, networkReportFile) : undefined;
|
|
248
|
+
let carriedOver;
|
|
249
|
+
if (process.env.TESTSPECTRA_CARRIED_OVER) {
|
|
250
|
+
try {
|
|
251
|
+
carriedOver = JSON.parse(process.env.TESTSPECTRA_CARRIED_OVER);
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
}
|
|
255
|
+
const carriedPassed = carriedOver?.passed || 0;
|
|
256
|
+
const carriedDurationMs = carriedOver?.durationMs || 0;
|
|
219
257
|
let suiteCount = 1;
|
|
220
258
|
let passedSuites = isPassed ? 1 : 0;
|
|
221
259
|
let failedSuites = isPassed ? 0 : 1;
|
|
222
|
-
let
|
|
260
|
+
let basePassed = result.passedCount ?? (isPassed ? 1 : 0);
|
|
261
|
+
let baseFailed = result.failedCount ?? (isPassed ? 0 : 1);
|
|
262
|
+
const totalPassed = basePassed + carriedPassed;
|
|
263
|
+
const totalFailed = baseFailed;
|
|
264
|
+
const totalTests = basePassed + baseFailed + carriedPassed;
|
|
265
|
+
let finalDuration = result.duration;
|
|
266
|
+
if (carriedDurationMs > 0) {
|
|
267
|
+
const match = result.duration.match(/([\d.]+)\s*(ms|s|m)/);
|
|
268
|
+
let runMs = 0;
|
|
269
|
+
if (match) {
|
|
270
|
+
const val = parseFloat(match[1]);
|
|
271
|
+
const unit = match[2];
|
|
272
|
+
runMs = unit === 's' ? val * 1000 : unit === 'm' ? val * 60000 : val;
|
|
273
|
+
}
|
|
274
|
+
const totalMs = runMs + carriedDurationMs;
|
|
275
|
+
finalDuration = totalMs < 1000 ? `${Math.round(totalMs)}ms` : `${(totalMs / 1000).toFixed(1)}s`;
|
|
276
|
+
}
|
|
277
|
+
const overallPassed = isPassed && totalFailed === 0;
|
|
223
278
|
if (fs.existsSync(outputJsonPath)) {
|
|
224
279
|
try {
|
|
225
280
|
const fullReport = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
|
|
226
281
|
if (fullReport.summary) {
|
|
227
282
|
suiteCount = fullReport.summary.totalSuites ?? 1;
|
|
228
|
-
testCount = fullReport.summary.totalTests ?? testCount;
|
|
229
283
|
}
|
|
230
284
|
if (fullReport.suites && Array.isArray(fullReport.suites)) {
|
|
231
285
|
failedSuites = fullReport.suites.filter((s) => (typeof s.failed === 'number' && s.failed > 0) ||
|
|
@@ -235,16 +289,17 @@ export async function runCommand(specPath, options = {}) {
|
|
|
235
289
|
}
|
|
236
290
|
if (fullReport.performance) {
|
|
237
291
|
renderSummaryCard({
|
|
238
|
-
isPassed,
|
|
239
|
-
statusText:
|
|
240
|
-
duration:
|
|
292
|
+
isPassed: overallPassed,
|
|
293
|
+
statusText: overallPassed ? 'PASSED' : 'FAILED',
|
|
294
|
+
duration: finalDuration,
|
|
241
295
|
platform,
|
|
242
296
|
suiteCount,
|
|
243
297
|
passedSuites,
|
|
244
298
|
failedSuites,
|
|
245
|
-
testCount,
|
|
246
|
-
passedCount:
|
|
247
|
-
failedCount:
|
|
299
|
+
testCount: totalTests,
|
|
300
|
+
passedCount: totalPassed,
|
|
301
|
+
failedCount: totalFailed,
|
|
302
|
+
carriedPassed,
|
|
248
303
|
reportPath: relReport,
|
|
249
304
|
networkReportPath: relNetworkReport,
|
|
250
305
|
});
|
|
@@ -252,8 +307,11 @@ export async function runCommand(specPath, options = {}) {
|
|
|
252
307
|
...fullReport.performance,
|
|
253
308
|
wallClockDurationMs: fullReport.wallClockDurationMs,
|
|
254
309
|
duration: fullReport.duration,
|
|
255
|
-
totalTests: fullReport.summary?.totalTests ??
|
|
256
|
-
});
|
|
310
|
+
totalTests: fullReport.summary?.totalTests ?? totalTests,
|
|
311
|
+
}, platform);
|
|
312
|
+
if (!overallPassed || totalFailed > 0) {
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
}
|
|
257
315
|
return;
|
|
258
316
|
}
|
|
259
317
|
}
|
|
@@ -262,19 +320,23 @@ export async function runCommand(specPath, options = {}) {
|
|
|
262
320
|
}
|
|
263
321
|
}
|
|
264
322
|
renderSummaryCard({
|
|
265
|
-
isPassed,
|
|
266
|
-
statusText:
|
|
267
|
-
duration:
|
|
323
|
+
isPassed: overallPassed,
|
|
324
|
+
statusText: overallPassed ? 'PASSED' : 'FAILED',
|
|
325
|
+
duration: finalDuration,
|
|
268
326
|
platform,
|
|
269
327
|
suiteCount,
|
|
270
328
|
passedSuites,
|
|
271
329
|
failedSuites,
|
|
272
|
-
testCount,
|
|
273
|
-
passedCount:
|
|
274
|
-
failedCount:
|
|
330
|
+
testCount: totalTests,
|
|
331
|
+
passedCount: totalPassed,
|
|
332
|
+
failedCount: totalFailed,
|
|
333
|
+
carriedPassed,
|
|
275
334
|
reportPath: relReport,
|
|
276
335
|
networkReportPath: relNetworkReport,
|
|
277
336
|
});
|
|
337
|
+
if (!overallPassed || totalFailed > 0) {
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
}
|
|
278
340
|
}
|
|
279
341
|
catch (e) {
|
|
280
342
|
console.error(`\x1b[31m[TestSpectra] Execution error:\x1b[0m`, e);
|
|
@@ -284,5 +346,8 @@ export async function runCommand(specPath, options = {}) {
|
|
|
284
346
|
if (demoServer) {
|
|
285
347
|
await demoServer.stop();
|
|
286
348
|
}
|
|
349
|
+
if (apiServer) {
|
|
350
|
+
await apiServer.stop();
|
|
351
|
+
}
|
|
287
352
|
}
|
|
288
353
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SkillsCommandOptions {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
agent?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function skillsInitCommand(options?: SkillsCommandOptions): Promise<void>;
|
|
6
|
+
export declare function skillsListCommand(options?: SkillsCommandOptions): void;
|
|
7
|
+
export declare function skillsAddCommand(ids?: string[], options?: SkillsCommandOptions): Promise<void>;
|
|
8
|
+
export declare function skillsUpdateCommand(ids?: string[], options?: SkillsCommandOptions): void;
|
|
9
|
+
export declare function skillsRemoveCommand(ids?: string[], options?: SkillsCommandOptions): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { initSkills, listSkills, addSkills, updateSkills, removeSkills, getKnownAgentIds, resolveAgentSelection, } from '@testspectra/skills';
|
|
4
|
+
function resolveBaseDir(options = {}) {
|
|
5
|
+
return options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
6
|
+
}
|
|
7
|
+
function resolveAgentIds(options = {}) {
|
|
8
|
+
const { ids, unknown } = resolveAgentSelection(options.agent);
|
|
9
|
+
if (unknown.length > 0) {
|
|
10
|
+
console.log(chalk.yellow(`⚠️ Unknown --agent value(s): ${unknown.join(', ')}. Known ids: ${getKnownAgentIds().join(', ')}`));
|
|
11
|
+
}
|
|
12
|
+
return ids.length > 0 ? ids : undefined;
|
|
13
|
+
}
|
|
14
|
+
export async function skillsInitCommand(options = {}) {
|
|
15
|
+
await initSkills(resolveBaseDir(options));
|
|
16
|
+
}
|
|
17
|
+
export function skillsListCommand(options = {}) {
|
|
18
|
+
listSkills(resolveBaseDir(options), resolveAgentIds(options));
|
|
19
|
+
}
|
|
20
|
+
export async function skillsAddCommand(ids = [], options = {}) {
|
|
21
|
+
await addSkills(ids, resolveBaseDir(options), resolveAgentIds(options));
|
|
22
|
+
}
|
|
23
|
+
export function skillsUpdateCommand(ids = [], options = {}) {
|
|
24
|
+
updateSkills(ids, resolveBaseDir(options), resolveAgentIds(options));
|
|
25
|
+
}
|
|
26
|
+
export function skillsRemoveCommand(ids = [], options = {}) {
|
|
27
|
+
removeSkills(ids, resolveBaseDir(options), resolveAgentIds(options));
|
|
28
|
+
}
|
|
@@ -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>;
|