@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
|
@@ -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):
|
|
@@ -18,11 +21,23 @@ export interface EntityScope {
|
|
|
18
21
|
* - Directory with page-objects/support/fixtures and NO specs => Shared Scope
|
|
19
22
|
*/
|
|
20
23
|
export declare function discoverEntityScopes(cwd: string): EntityScope[];
|
|
24
|
+
/**
|
|
25
|
+
* Detects a component-testing project (`@testspectra/react`'s Vitest browser-mode runner), via
|
|
26
|
+
* the same signal `cli/src/commands/test.ts` uses to gate `spectra test` itself. Used to augment
|
|
27
|
+
* the generated `Spectra` ambient type with `.mount()`, which only exists in that runtime.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isComponentTestingProject(cwd: string): boolean;
|
|
21
30
|
export declare class TypeGenerator {
|
|
22
31
|
/**
|
|
23
32
|
* Main entry point to generate all ambient type declarations.
|
|
24
33
|
*/
|
|
25
34
|
static generateAll(cwd: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Reloads `spectra.config.ts` and refreshes `.testspectra/types/env.d.ts` with the current
|
|
37
|
+
* `executionConfig.environmentVariables`. Separate from `writeDeclarationFiles`/`generateAll`
|
|
38
|
+
* (which must stay synchronous) because reading the config file is unavoidably async.
|
|
39
|
+
*/
|
|
40
|
+
static syncEnvDeclaration(cwd: string): Promise<void>;
|
|
26
41
|
/**
|
|
27
42
|
* Discovers all feature directories and shared library directories.
|
|
28
43
|
* Returns a map of scope -> base directory
|
|
@@ -30,7 +45,18 @@ export declare class TypeGenerator {
|
|
|
30
45
|
static getEntityScopes(cwd: string): EntityScope[];
|
|
31
46
|
static getPageObjectsDirsForScope(scopeDir: string, sharedDir?: string): string[];
|
|
32
47
|
static getEntityDirsForScope(entityType: 'actions' | 'steps' | 'fixtures', scopeDir: string, sharedDir?: string): string[];
|
|
33
|
-
static generateDeclarationsForScope(cwd: string, outputTypesDir: string, scopeDir: string, sharedDir?: string): Record<PlatformTarget, string>;
|
|
48
|
+
static generateDeclarationsForScope(cwd: string, outputTypesDir: string, scopeDir: string, sharedDir?: string, rootTypesDir?: string): Record<PlatformTarget, string>;
|
|
34
49
|
static generateFixturesDeclarationForScope(cwd: string, outputTypesDir: string, scopeDir: string, sharedDir?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Generates the typed `Spectra.env` ambient declaration from `spectra.config.ts`'s
|
|
52
|
+
* `executionConfig.environmentVariables` — mirrors `generateFixturesDeclarationForScope`, but
|
|
53
|
+
* env vars come from a single project-level config rather than a scanned directory.
|
|
54
|
+
*
|
|
55
|
+
* Emits a bare global `interface SpectraEnv { readonly KEY: string; ... }`, which TypeScript
|
|
56
|
+
* interface-merges with the empty `SpectraEnv` shipped by `@testspectra/matchers` (the same
|
|
57
|
+
* mechanism `@types/node` uses for projects to augment `NodeJS.ProcessEnv`) — so `Spectra.env.KEY`
|
|
58
|
+
* resolves to `string`, never `string | undefined` like raw `process.env.KEY`.
|
|
59
|
+
*/
|
|
60
|
+
static generateEnvDeclaration(environmentVariables: Record<string, string>): string;
|
|
35
61
|
static writeDeclarationFiles(cwd: string): void;
|
|
36
62
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { ConfigLoader } from '../config/loader.js';
|
|
3
4
|
import { TsConfigGenerator } from './tsconfig-generator.js';
|
|
4
5
|
export const PLATFORM_HIERARCHY = {
|
|
5
6
|
web: ['web', 'common'],
|
|
@@ -12,8 +13,11 @@ export const PLATFORM_HIERARCHY = {
|
|
|
12
13
|
* Discovers all feature directories and shared library directories.
|
|
13
14
|
* Standard discovery hierarchy:
|
|
14
15
|
* 1. Nx / Monorepo Project Descriptors (`project.json`):
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
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.
|
|
17
21
|
* 2. Explicit User Configuration (`tsconfig.spectra.shared.json`):
|
|
18
22
|
* - Custom include path for shared testing utilities
|
|
19
23
|
* 3. Dynamic Workspace Filesystem Discovery (Fallback for non-Nx / custom layouts):
|
|
@@ -47,16 +51,9 @@ export function discoverEntityScopes(cwd) {
|
|
|
47
51
|
if (fs.existsSync(projectJsonPath)) {
|
|
48
52
|
try {
|
|
49
53
|
const pObj = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
50
|
-
const projectType = pObj.projectType;
|
|
51
54
|
const tags = Array.isArray(pObj.tags) ? pObj.tags : [];
|
|
52
|
-
const isSharedLib =
|
|
53
|
-
|
|
54
|
-
tags.includes('type:library') ||
|
|
55
|
-
tags.includes('scope:shared');
|
|
56
|
-
const isE2eApp = projectType === 'application' ||
|
|
57
|
-
tags.includes('type:e2e') ||
|
|
58
|
-
tags.includes('type:app') ||
|
|
59
|
-
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:'));
|
|
60
57
|
if (isSharedLib) {
|
|
61
58
|
if (!scopes.some((s) => s.dir === dir)) {
|
|
62
59
|
scopes.push({ name: 'shared', dir, isShared: true });
|
|
@@ -64,9 +61,9 @@ export function discoverEntityScopes(cwd) {
|
|
|
64
61
|
return; // Do not recurse into subdirectories of an identified project
|
|
65
62
|
}
|
|
66
63
|
else if (isE2eApp) {
|
|
67
|
-
// Extract scope name from tags (e.g. "scope:matchers" => "matchers") or project name
|
|
68
|
-
const scopeTag = tags.find((t) => t.startsWith('
|
|
69
|
-
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:', '') : '';
|
|
70
67
|
if (!featureName && pObj.name) {
|
|
71
68
|
featureName = pObj.name.replace(/-e2e$/, '').replace(/^e2e-/, '');
|
|
72
69
|
}
|
|
@@ -180,6 +177,23 @@ export function discoverEntityScopes(cwd) {
|
|
|
180
177
|
fallbackScan(cwd, 5);
|
|
181
178
|
return scopes;
|
|
182
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Detects a component-testing project (`@testspectra/react`'s Vitest browser-mode runner), via
|
|
182
|
+
* the same signal `cli/src/commands/test.ts` uses to gate `spectra test` itself. Used to augment
|
|
183
|
+
* the generated `Spectra` ambient type with `.mount()`, which only exists in that runtime.
|
|
184
|
+
*/
|
|
185
|
+
export function isComponentTestingProject(cwd) {
|
|
186
|
+
try {
|
|
187
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
188
|
+
if (!fs.existsSync(pkgPath))
|
|
189
|
+
return false;
|
|
190
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
191
|
+
return Boolean(pkg.dependencies?.['@testspectra/react'] || pkg.devDependencies?.['@testspectra/react']);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
183
197
|
export class TypeGenerator {
|
|
184
198
|
/**
|
|
185
199
|
* Main entry point to generate all ambient type declarations.
|
|
@@ -188,6 +202,24 @@ export class TypeGenerator {
|
|
|
188
202
|
this.writeDeclarationFiles(cwd);
|
|
189
203
|
TsConfigGenerator.scaffoldConfigs(cwd);
|
|
190
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Reloads `spectra.config.ts` and refreshes `.testspectra/types/env.d.ts` with the current
|
|
207
|
+
* `executionConfig.environmentVariables`. Separate from `writeDeclarationFiles`/`generateAll`
|
|
208
|
+
* (which must stay synchronous) because reading the config file is unavoidably async.
|
|
209
|
+
*/
|
|
210
|
+
static async syncEnvDeclaration(cwd) {
|
|
211
|
+
const rootTypesDir = path.join(cwd, '.testspectra', 'types');
|
|
212
|
+
if (!fs.existsSync(rootTypesDir)) {
|
|
213
|
+
fs.mkdirSync(rootTypesDir, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
let environmentVariables = {};
|
|
216
|
+
try {
|
|
217
|
+
const config = await ConfigLoader.loadConfig(cwd);
|
|
218
|
+
environmentVariables = config.executionConfig?.environmentVariables ?? {};
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
fs.writeFileSync(path.join(rootTypesDir, 'env.d.ts'), this.generateEnvDeclaration(environmentVariables), 'utf-8');
|
|
222
|
+
}
|
|
191
223
|
/**
|
|
192
224
|
* Discovers all feature directories and shared library directories.
|
|
193
225
|
* Returns a map of scope -> base directory
|
|
@@ -267,10 +299,11 @@ export class TypeGenerator {
|
|
|
267
299
|
}
|
|
268
300
|
return dirs;
|
|
269
301
|
}
|
|
270
|
-
static generateDeclarationsForScope(cwd, outputTypesDir, scopeDir, sharedDir) {
|
|
302
|
+
static generateDeclarationsForScope(cwd, outputTypesDir, scopeDir, sharedDir, rootTypesDir) {
|
|
271
303
|
const poDirs = this.getPageObjectsDirsForScope(scopeDir, sharedDir);
|
|
272
304
|
const actionsDirs = this.getEntityDirsForScope('actions', scopeDir, sharedDir);
|
|
273
305
|
const stepsDirs = this.getEntityDirsForScope('steps', scopeDir, sharedDir);
|
|
306
|
+
const isComponentTesting = isComponentTestingProject(cwd);
|
|
274
307
|
const platforms = ['web', 'android', 'ios', 'mobile', 'common'];
|
|
275
308
|
const results = {
|
|
276
309
|
web: '',
|
|
@@ -279,14 +312,40 @@ export class TypeGenerator {
|
|
|
279
312
|
mobile: '',
|
|
280
313
|
common: '',
|
|
281
314
|
};
|
|
315
|
+
// `env.d.ts` always lives at the workspace root types dir (env vars come from a single
|
|
316
|
+
// `spectra.config.ts`, unlike per-scope fixtures/page-objects), so a shared/feature scope's
|
|
317
|
+
// own declarations reference it via a relative path back up to the root.
|
|
318
|
+
let envRefPath = './env.d.ts';
|
|
319
|
+
if (rootTypesDir && rootTypesDir !== outputTypesDir) {
|
|
320
|
+
envRefPath = path.relative(outputTypesDir, path.join(rootTypesDir, 'env.d.ts')).replace(/\\/g, '/');
|
|
321
|
+
if (!envRefPath.startsWith('.'))
|
|
322
|
+
envRefPath = `./${envRefPath}`;
|
|
323
|
+
}
|
|
282
324
|
for (const platform of platforms) {
|
|
283
325
|
const hierarchy = PLATFORM_HIERARCHY[platform];
|
|
284
326
|
let content = `// Auto-generated ambient declarations for TestSpectra [${platform.toUpperCase()}]\n`;
|
|
285
327
|
content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
|
|
286
|
-
content += `/// <reference types="@wdio/globals/types" />\n`;
|
|
287
|
-
content += `/// <reference types="@wdio/mocha-framework" />\n`;
|
|
288
328
|
content += `/// <reference types="@testspectra/matchers" />\n`;
|
|
289
|
-
content += `/// <reference path="./fixtures.d.ts" />\n
|
|
329
|
+
content += `/// <reference path="./fixtures.d.ts" />\n`;
|
|
330
|
+
content += `/// <reference path="${envRefPath}" />\n\n`;
|
|
331
|
+
// TestSpectra's Rust-native orchestrator worker (core/orchestrator/src/worker/bdd_runner.ts)
|
|
332
|
+
// registers its own `it`/`test`/lifecycle-hook globals directly — there is no WDIO or Mocha
|
|
333
|
+
// runtime involved at all (not just for component testing; this is true across every
|
|
334
|
+
// platform). `describe` is intentionally NOT declared here even though the runtime has a
|
|
335
|
+
// transparent no-op implementation of it — spec-schema-linter's no-describe-block rule
|
|
336
|
+
// forbids authoring it, so leaving it undeclared makes that a compile error too, not just a
|
|
337
|
+
// lint error.
|
|
338
|
+
content += `declare function it(title: string, fn: () => Promise<void> | void): void;\n`;
|
|
339
|
+
if (isComponentTesting) {
|
|
340
|
+
// Vitest's own `expect` global (test.globals: true in tools/react/vitest.config.ts) — the
|
|
341
|
+
// one thing component specs legitimately need beyond the Spectra.* DSL, for asserting
|
|
342
|
+
// plain closure/callback state (e.g. "was this onClick handler invoked") that has no DOM
|
|
343
|
+
// element to target. Reuses @testspectra/react's own re-exported `ExpectStatic` type
|
|
344
|
+
// rather than hand-duplicating Vitest's expect API surface. Not declared for E2E projects
|
|
345
|
+
// — the Rust orchestrator's BDD runtime has no `expect` global at all.
|
|
346
|
+
content += `declare const expect: import('@testspectra/react').ExpectStatic;\n`;
|
|
347
|
+
}
|
|
348
|
+
content += `\n`;
|
|
290
349
|
// 1. Page Objects Global Declarations
|
|
291
350
|
const declaredPOMs = new Set();
|
|
292
351
|
for (const poDir of poDirs) {
|
|
@@ -423,7 +482,10 @@ export class TypeGenerator {
|
|
|
423
482
|
}
|
|
424
483
|
}
|
|
425
484
|
content += `}\n`;
|
|
426
|
-
|
|
485
|
+
// Reuses @testspectra/react's own exported `mount` type rather than hand-duplicating its
|
|
486
|
+
// signature here — stays in sync automatically if that signature ever changes.
|
|
487
|
+
const mountAugment = isComponentTesting ? ` & { mount: typeof import('@testspectra/react').mount }` : '';
|
|
488
|
+
content += `type TestSpectraActions = import('@testspectra/matchers').SpectraStatic & TestSpectraCustomActions${mountAugment};\n`;
|
|
427
489
|
content += `declare const Spectra: TestSpectraActions;\n\n`;
|
|
428
490
|
// 3. Built-in Steps Declaration
|
|
429
491
|
content += `interface TestSpectraSteps {\n`;
|
|
@@ -543,11 +605,42 @@ export class TypeGenerator {
|
|
|
543
605
|
content += `declare const Fixture: TestSpectraFixtures;\n`;
|
|
544
606
|
return content;
|
|
545
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Generates the typed `Spectra.env` ambient declaration from `spectra.config.ts`'s
|
|
610
|
+
* `executionConfig.environmentVariables` — mirrors `generateFixturesDeclarationForScope`, but
|
|
611
|
+
* env vars come from a single project-level config rather than a scanned directory.
|
|
612
|
+
*
|
|
613
|
+
* Emits a bare global `interface SpectraEnv { readonly KEY: string; ... }`, which TypeScript
|
|
614
|
+
* interface-merges with the empty `SpectraEnv` shipped by `@testspectra/matchers` (the same
|
|
615
|
+
* mechanism `@types/node` uses for projects to augment `NodeJS.ProcessEnv`) — so `Spectra.env.KEY`
|
|
616
|
+
* resolves to `string`, never `string | undefined` like raw `process.env.KEY`.
|
|
617
|
+
*/
|
|
618
|
+
static generateEnvDeclaration(environmentVariables) {
|
|
619
|
+
let content = `// Auto-generated ambient Spectra.env declarations for TestSpectra\n`;
|
|
620
|
+
content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
|
|
621
|
+
content += `interface SpectraEnv {\n`;
|
|
622
|
+
for (const key of Object.keys(environmentVariables)) {
|
|
623
|
+
content += ` readonly ${JSON.stringify(key)}: string;\n`;
|
|
624
|
+
}
|
|
625
|
+
content += `}\n`;
|
|
626
|
+
return content;
|
|
627
|
+
}
|
|
546
628
|
static writeDeclarationFiles(cwd) {
|
|
547
629
|
const rootTypesDir = path.join(cwd, '.testspectra', 'types');
|
|
548
630
|
if (!fs.existsSync(rootTypesDir)) {
|
|
549
631
|
fs.mkdirSync(rootTypesDir, { recursive: true });
|
|
550
632
|
}
|
|
633
|
+
// env.d.ts always lives at the root, regardless of Nx scopes — a single spectra.config.ts's
|
|
634
|
+
// executionConfig.environmentVariables applies workspace-wide (see generateEnvDeclaration).
|
|
635
|
+
// Reading spectra.config.ts itself is unavoidably async (ESM dynamic import), which would ripple
|
|
636
|
+
// an async signature through every caller of this long-standing sync method — instead, this only
|
|
637
|
+
// guarantees env.d.ts *exists* (so the reference below never points at a missing file) and
|
|
638
|
+
// leaves it accurate as of the last real sync. `syncEnvDeclaration` (called at the actual
|
|
639
|
+
// execution entry points: run/watch/sync-types) is what keeps its content up to date.
|
|
640
|
+
const envDtsPath = path.join(rootTypesDir, 'env.d.ts');
|
|
641
|
+
if (!fs.existsSync(envDtsPath)) {
|
|
642
|
+
fs.writeFileSync(envDtsPath, this.generateEnvDeclaration({}), 'utf-8');
|
|
643
|
+
}
|
|
551
644
|
const scopes = this.getEntityScopes(cwd);
|
|
552
645
|
const sharedScope = scopes.find((s) => s.isShared);
|
|
553
646
|
const featureScopes = scopes.filter((s) => !s.isShared);
|
|
@@ -561,7 +654,7 @@ export class TypeGenerator {
|
|
|
561
654
|
if (!hasScopes || hasRootEntities) {
|
|
562
655
|
const rootFixtures = this.generateFixturesDeclarationForScope(cwd, rootTypesDir, cwd);
|
|
563
656
|
fs.writeFileSync(path.join(rootTypesDir, 'fixtures.d.ts'), rootFixtures, 'utf-8');
|
|
564
|
-
const rootDeclarations = this.generateDeclarationsForScope(cwd, rootTypesDir, cwd);
|
|
657
|
+
const rootDeclarations = this.generateDeclarationsForScope(cwd, rootTypesDir, cwd, undefined, rootTypesDir);
|
|
565
658
|
for (const [platform, content] of Object.entries(rootDeclarations)) {
|
|
566
659
|
fs.writeFileSync(path.join(rootTypesDir, `${platform}.d.ts`), content, 'utf-8');
|
|
567
660
|
}
|
|
@@ -587,7 +680,7 @@ export class TypeGenerator {
|
|
|
587
680
|
fs.mkdirSync(sharedTypesDir, { recursive: true });
|
|
588
681
|
const sharedFixtures = this.generateFixturesDeclarationForScope(cwd, sharedTypesDir, sharedScope.dir);
|
|
589
682
|
fs.writeFileSync(path.join(sharedTypesDir, 'fixtures.d.ts'), sharedFixtures, 'utf-8');
|
|
590
|
-
const sharedDeclarations = this.generateDeclarationsForScope(cwd, sharedTypesDir, sharedScope.dir);
|
|
683
|
+
const sharedDeclarations = this.generateDeclarationsForScope(cwd, sharedTypesDir, sharedScope.dir, undefined, rootTypesDir);
|
|
591
684
|
for (const [platform, content] of Object.entries(sharedDeclarations)) {
|
|
592
685
|
fs.writeFileSync(path.join(sharedTypesDir, `${platform}.d.ts`), content, 'utf-8');
|
|
593
686
|
}
|
|
@@ -609,7 +702,7 @@ export class TypeGenerator {
|
|
|
609
702
|
fs.mkdirSync(featureTypesDir, { recursive: true });
|
|
610
703
|
const featureFixtures = this.generateFixturesDeclarationForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
|
|
611
704
|
fs.writeFileSync(path.join(featureTypesDir, 'fixtures.d.ts'), featureFixtures, 'utf-8');
|
|
612
|
-
const featureDeclarations = this.generateDeclarationsForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
|
|
705
|
+
const featureDeclarations = this.generateDeclarationsForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir, rootTypesDir);
|
|
613
706
|
for (const [platform, content] of Object.entries(featureDeclarations)) {
|
|
614
707
|
fs.writeFileSync(path.join(featureTypesDir, `${platform}.d.ts`), content, 'utf-8');
|
|
615
708
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Command } from 'commander';
|
|
|
2
2
|
export * from './config/schema.js';
|
|
3
3
|
export * from './config/loader.js';
|
|
4
4
|
export * from './generator/index.js';
|
|
5
|
-
export * from './lifecycle/index.js';
|
|
6
5
|
export * from './refactor/index.js';
|
|
7
6
|
export * from './mobile/index.js';
|
|
7
|
+
export * from './linter/index.js';
|
|
8
8
|
export * from './types/generator.js';
|
|
9
9
|
export * from '@testspectra/matchers';
|
|
10
10
|
export declare function createCliProgram(): Command;
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { addCommand } from './commands/add.js';
|
|
3
|
-
import { cloudRunCommand } from './commands/cloud-run.js';
|
|
4
3
|
import { demoCommand } from './commands/demo.js';
|
|
5
4
|
import { devicesCommand } from './commands/devices.js';
|
|
6
5
|
import { doctorCommand } from './commands/doctor.js';
|
|
7
6
|
import { initCommand } from './commands/init.js';
|
|
8
7
|
import { installCommand } from './commands/install.js';
|
|
9
8
|
import { runCommand } from './commands/run.js';
|
|
9
|
+
import { testCommand } from './commands/test.js';
|
|
10
10
|
import { updateCommand } from './commands/update.js';
|
|
11
11
|
import { watchCommand } from './commands/watch.js';
|
|
12
12
|
import { syncTypesCommand } from './commands/sync-types.js';
|
|
13
13
|
import { refactorCommand } from './commands/refactor.js';
|
|
14
|
+
import { lintCommand } from './commands/lint.js';
|
|
14
15
|
import { licenseCommand } from './commands/license.js';
|
|
16
|
+
import { skillsInitCommand, skillsListCommand, skillsAddCommand, skillsUpdateCommand, skillsRemoveCommand, } from './commands/skills.js';
|
|
15
17
|
export * from './config/schema.js';
|
|
16
18
|
export * from './config/loader.js';
|
|
17
19
|
export * from './generator/index.js';
|
|
18
|
-
export * from './lifecycle/index.js';
|
|
19
20
|
export * from './refactor/index.js';
|
|
20
21
|
export * from './mobile/index.js';
|
|
22
|
+
export * from './linter/index.js';
|
|
21
23
|
export * from './types/generator.js';
|
|
22
24
|
export * from '@testspectra/matchers';
|
|
23
25
|
export function createCliProgram() {
|
|
@@ -26,7 +28,7 @@ export function createCliProgram() {
|
|
|
26
28
|
program
|
|
27
29
|
.command('init')
|
|
28
30
|
.description('Initialize testspectra.config.json and project directories')
|
|
29
|
-
.option('-t, --template <template>', 'Project template: default, nx')
|
|
31
|
+
.option('-t, --template <template>', 'Project template: default, nx, react-component')
|
|
30
32
|
.option('-f, --force', 'Overwrite existing configuration if present')
|
|
31
33
|
.action(initCommand);
|
|
32
34
|
program
|
|
@@ -71,7 +73,16 @@ Examples:
|
|
|
71
73
|
.option('--to <path>', 'Destination file or directory to refactor to')
|
|
72
74
|
.option('--dry-run', 'Preview changes without modifying files')
|
|
73
75
|
.option('--json', 'Output results as JSON')
|
|
74
|
-
.action(
|
|
76
|
+
.action(async (...args) => {
|
|
77
|
+
await refactorCommand(...args);
|
|
78
|
+
});
|
|
79
|
+
program
|
|
80
|
+
.command('lint [path]')
|
|
81
|
+
.description('Validate spec and suite schemas, frontmatter, and step integrity')
|
|
82
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
83
|
+
.option('--strict', 'Treat warnings as errors and exit with code 1')
|
|
84
|
+
.option('--json', 'Output diagnostic results as JSON')
|
|
85
|
+
.action(lintCommand);
|
|
75
86
|
program
|
|
76
87
|
.command('doctor')
|
|
77
88
|
.description('Verify local environment prerequisites (ADB, Spectra Android Driver, Chrome, Bun)')
|
|
@@ -100,8 +111,44 @@ Examples:
|
|
|
100
111
|
$ spectra license status Display active license organization, plan, and expiration
|
|
101
112
|
$ spectra license remove Remove locally activated license credentials`)
|
|
102
113
|
.action(licenseCommand);
|
|
114
|
+
const skills = program
|
|
115
|
+
.command('skills')
|
|
116
|
+
.description('Manage AI agent skills that standardize TestSpectra script authoring (@testspectra/skills)');
|
|
117
|
+
skills
|
|
118
|
+
.command('init')
|
|
119
|
+
.description('Prompt which AI agent(s) you use and save the choice for future add/update/remove/list')
|
|
120
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
121
|
+
.action(skillsInitCommand);
|
|
122
|
+
skills
|
|
123
|
+
.command('list')
|
|
124
|
+
.alias('ls')
|
|
125
|
+
.description('List all available and installed TestSpectra AI agent skills')
|
|
126
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
127
|
+
.option('-a, --agent <ids>', 'One-off override: comma-separated agent(s) to target, bypassing the saved "init" selection (claude, opencode, kilo, agents, gemini)')
|
|
128
|
+
.action(skillsListCommand);
|
|
129
|
+
skills
|
|
130
|
+
.command('add [skillIds...]')
|
|
131
|
+
.alias('install')
|
|
132
|
+
.description('Install specified skills into the detected AI agent directories (interactive if none provided)')
|
|
133
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
134
|
+
.option('-a, --agent <ids>', 'One-off override: comma-separated agent(s) to target, bypassing the saved "init" selection (claude, opencode, kilo, agents, gemini)')
|
|
135
|
+
.action(skillsAddCommand);
|
|
136
|
+
skills
|
|
137
|
+
.command('update [skillIds...]')
|
|
138
|
+
.alias('up')
|
|
139
|
+
.description('Update installed skills to the current bundled version (defaults to all installed)')
|
|
140
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
141
|
+
.option('-a, --agent <ids>', 'One-off override: comma-separated agent(s) to target, bypassing the saved "init" selection (claude, opencode, kilo, agents, gemini)')
|
|
142
|
+
.action(skillsUpdateCommand);
|
|
143
|
+
skills
|
|
144
|
+
.command('remove <skillIds...>')
|
|
145
|
+
.alias('rm')
|
|
146
|
+
.description('Remove specified skills from the detected AI agent directories')
|
|
147
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
148
|
+
.option('-a, --agent <ids>', 'One-off override: comma-separated agent(s) to target, bypassing the saved "init" selection (claude, opencode, kilo, agents, gemini)')
|
|
149
|
+
.action(skillsRemoveCommand);
|
|
103
150
|
program
|
|
104
|
-
.command('run [
|
|
151
|
+
.command('run [paths...]')
|
|
105
152
|
.description('Execute test suite or individual test case')
|
|
106
153
|
.option('-s, --spec <path>', 'Spec file path to execute')
|
|
107
154
|
.option('-t, --target <target>', 'Target platform: web, android, ios, common', 'web')
|
|
@@ -111,7 +158,6 @@ Examples:
|
|
|
111
158
|
.option('--devices <list>', 'Comma-separated list of device UDIDs to run on')
|
|
112
159
|
.option('--license-key <key>', 'Explicit commercial license key override')
|
|
113
160
|
.option('--demo', 'Automatically launch built-in demo web app server during test execution')
|
|
114
|
-
.option('--base-url <url>', "Override spectra.config.ts's webConfig.baseUrl for this run (e.g. a dev server started by CI just for this job)")
|
|
115
161
|
.option('--headless', 'Run in headless mode')
|
|
116
162
|
.option('--no-headless', 'Run in headed mode')
|
|
117
163
|
.option('--headed', 'Run browser in visible GUI mode (alias for --no-headless)')
|
|
@@ -122,29 +168,19 @@ Examples:
|
|
|
122
168
|
.option('-w, --workdir <dir>', 'Custom app data / temp workdir')
|
|
123
169
|
.option('-o, --output <path>', 'Custom test report JSON output path')
|
|
124
170
|
.action(runCommand);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.
|
|
128
|
-
.
|
|
129
|
-
.
|
|
130
|
-
.option('--
|
|
131
|
-
.option('--
|
|
132
|
-
.option('--
|
|
133
|
-
.option('--
|
|
134
|
-
.option('--
|
|
135
|
-
.option('--
|
|
136
|
-
.option('--
|
|
137
|
-
.
|
|
138
|
-
.option('--base-url <url>', 'Override spectra.config.ts\'s webConfig.baseUrl for this run — for a dev server the CI runner itself started in the background and exposed at a reachable URL, forwarded to the cloud server as `spectra run --base-url`')
|
|
139
|
-
.addHelpText('after', `
|
|
140
|
-
Requires SPECTRA_URL and SPECTRA_TOKEN (a Gitea Personal Access Token) as
|
|
141
|
-
environment variables — see docs/v2/cloud/git-spec-standards-and-workflow.md.
|
|
142
|
-
|
|
143
|
-
Examples:
|
|
144
|
-
$ spectra cloud run --project org/repo --branch main --platform web --wait
|
|
145
|
-
$ spectra cloud run --project org/repo --no-wait
|
|
146
|
-
$ spectra cloud run --project org/repo --base-url http://192.168.1.50:5173 --wait`)
|
|
147
|
-
.action(cloudRunCommand);
|
|
171
|
+
program
|
|
172
|
+
.command('test')
|
|
173
|
+
.description('Run React component tests via @testspectra/react (bundled Vitest browser mode)')
|
|
174
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
175
|
+
.option('-s, --spec <path>', 'Spec file path (or substring filter) to execute')
|
|
176
|
+
.option('--headless', 'Run in headless mode (default)')
|
|
177
|
+
.option('--no-headless', 'Run in headed mode')
|
|
178
|
+
.option('--headed', 'Run the browser in visible GUI mode (alias for --no-headless)')
|
|
179
|
+
.option('--step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
|
|
180
|
+
.option('--delay <ms>', 'Alias for --step-delay')
|
|
181
|
+
.option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default). Browser mode has no numeric N-worker sizing like `spectra run`.')
|
|
182
|
+
.option('-o, --output <path>', 'Write a Vitest JSON test report to this path')
|
|
183
|
+
.action(testCommand);
|
|
148
184
|
return program;
|
|
149
185
|
}
|
|
150
186
|
export async function runCli(args = process.argv) {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { extractModuleExports } from '../extractor.js';
|
|
6
|
+
import { SpecLinter } from '../spec-linter.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
describe('Anonymous Default Export (testspectra/require-anonymous-default-export)', () => {
|
|
10
|
+
const tempDir = path.join(__dirname, '__temp_anonymous_export__');
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (fs.existsSync(tempDir)) {
|
|
16
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
describe('extractModuleExports()', () => {
|
|
20
|
+
it('detects valid anonymous async default export in .step.ts', () => {
|
|
21
|
+
const code = `export default async function () {
|
|
22
|
+
await LoginPage.open();
|
|
23
|
+
}`;
|
|
24
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
25
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
26
|
+
expect(result.hasNamedExports).toBe(false);
|
|
27
|
+
expect(result.testBlocks).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
it('detects valid anonymous async arrow function default export in .step.ts', () => {
|
|
30
|
+
const code = `export default async () => {
|
|
31
|
+
await LoginPage.open();
|
|
32
|
+
};`;
|
|
33
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
34
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
35
|
+
expect(result.hasNamedExports).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('detects named function with export default (forbidden)', () => {
|
|
38
|
+
const code = `export default async function login() {
|
|
39
|
+
await LoginPage.open();
|
|
40
|
+
}`;
|
|
41
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
42
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
43
|
+
expect(result.hasNamedExports).toBe(true);
|
|
44
|
+
expect(result.namedExportLines).toHaveLength(1);
|
|
45
|
+
expect(result.namedExportLines[0].name).toBe('login');
|
|
46
|
+
expect(result.namedExportLines[0].line).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
it('detects named export function (forbidden)', () => {
|
|
49
|
+
const code = `export async function verifyUser() {
|
|
50
|
+
await UserPage.verify();
|
|
51
|
+
}`;
|
|
52
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
53
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
54
|
+
expect(result.hasNamedExports).toBe(true);
|
|
55
|
+
expect(result.namedExportLines[0].name).toBe('verifyUser');
|
|
56
|
+
});
|
|
57
|
+
it('detects named export class (forbidden)', () => {
|
|
58
|
+
const code = `export class Helper {}`;
|
|
59
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
60
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
61
|
+
expect(result.hasNamedExports).toBe(true);
|
|
62
|
+
expect(result.namedExportLines[0].name).toBe('Helper');
|
|
63
|
+
});
|
|
64
|
+
it('detects export { ... } clauses (forbidden)', () => {
|
|
65
|
+
const code = `const a = 1; export { a };`;
|
|
66
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
67
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
68
|
+
expect(result.hasNamedExports).toBe(true);
|
|
69
|
+
expect(result.namedExportLines[0].name).toBe('a');
|
|
70
|
+
});
|
|
71
|
+
it('detects named arrow function with export default (forbidden)', () => {
|
|
72
|
+
const code = `const fn = async () => {}; export default async function named() {}`;
|
|
73
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
74
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
75
|
+
expect(result.hasNamedExports).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('detects valid anonymous async default export in .action.ts', () => {
|
|
78
|
+
const code = `export default async function (email: string) {
|
|
79
|
+
await this.get("#email").type(email);
|
|
80
|
+
}`;
|
|
81
|
+
const result = extractModuleExports(code, 'web.action.ts');
|
|
82
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
83
|
+
expect(result.hasNamedExports).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('detects valid anonymous async default export in .hook.ts', () => {
|
|
86
|
+
const code = `export default async function () {}`;
|
|
87
|
+
const result = extractModuleExports(code, 'web.hook.ts');
|
|
88
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
89
|
+
expect(result.hasNamedExports).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it('detects missing default export', () => {
|
|
92
|
+
const code = `function verifyLogin() {
|
|
93
|
+
await LoginPage.open();
|
|
94
|
+
}`;
|
|
95
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
96
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it('detects non-async default export as invalid', () => {
|
|
99
|
+
const code = `export default function verifyLogin() {
|
|
100
|
+
console.log("hello");
|
|
101
|
+
}`;
|
|
102
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
103
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
it('detects forbidden it() and describe() blocks in shared module', () => {
|
|
106
|
+
const code = `export default async function () {}
|
|
107
|
+
it("test block", async () => {});
|
|
108
|
+
describe("suite block", () => {});
|
|
109
|
+
`;
|
|
110
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
111
|
+
expect(result.testBlocks).toHaveLength(2);
|
|
112
|
+
expect(result.testBlocks[0].name).toBe('it');
|
|
113
|
+
expect(result.testBlocks[1].name).toBe('describe');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('testspectra/require-anonymous-default-export workspace validation', () => {
|
|
117
|
+
it('emits testspectra/require-anonymous-default-export when shared step lacks anonymous async default export', () => {
|
|
118
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'login');
|
|
119
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `function verifyLogin() {
|
|
121
|
+
console.log("no export");
|
|
122
|
+
}
|
|
123
|
+
`);
|
|
124
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
125
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
126
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
127
|
+
expect(errors[0].message).toContain('anonymous async function as default export');
|
|
128
|
+
});
|
|
129
|
+
it('passes validation when shared step has valid anonymous async default export', () => {
|
|
130
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'login');
|
|
131
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `export default async function () {
|
|
133
|
+
await LoginPage.open();
|
|
134
|
+
}
|
|
135
|
+
`);
|
|
136
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
137
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
138
|
+
expect(errors).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
it('emits testspectra/require-anonymous-default-export when shared action has named exports', () => {
|
|
141
|
+
const actionsDir = path.join(tempDir, 'shared', 'testing', 'support', 'actions', 'fillForm');
|
|
142
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
143
|
+
fs.writeFileSync(path.join(actionsDir, 'web.action.ts'), `export default async function () {}
|
|
144
|
+
export function helper() {}
|
|
145
|
+
`);
|
|
146
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
147
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
148
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
149
|
+
expect(errors.some((d) => d.message.includes('Named export'))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it('emits testspectra/require-anonymous-default-export when shared hook has test blocks', () => {
|
|
152
|
+
const hooksDir = path.join(tempDir, 'shared', 'testing', 'global-hooks', 'before');
|
|
153
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
154
|
+
fs.writeFileSync(path.join(hooksDir, 'web.hook.ts'), `export default async function () {}
|
|
155
|
+
it("should run", async () => {});
|
|
156
|
+
`);
|
|
157
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
158
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
159
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
160
|
+
expect(errors.some((d) => d.message.includes('it() block is forbidden'))).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it('does NOT emit testspectra/require-anonymous-default-export for valid shared step files', () => {
|
|
163
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'verify');
|
|
164
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
165
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `export default async function () {
|
|
166
|
+
await Page.open();
|
|
167
|
+
await Page.element.should("be.visible");
|
|
168
|
+
}
|
|
169
|
+
`);
|
|
170
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
171
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
172
|
+
expect(errors).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
it('does NOT emit testspectra/require-anonymous-default-export for files outside shared/ directory', () => {
|
|
175
|
+
const featureDir = path.join(tempDir, 'packages', 'playground', 'e2e');
|
|
176
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
177
|
+
fs.writeFileSync(path.join(featureDir, 'helper.ts'), `export function helper() {}
|
|
178
|
+
`);
|
|
179
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
180
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
181
|
+
expect(errors).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|