@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
|
@@ -5,6 +5,7 @@ export interface RenameOptions {
|
|
|
5
5
|
oldName: string;
|
|
6
6
|
newName: string;
|
|
7
7
|
dryRun?: boolean;
|
|
8
|
+
skipPathRename?: boolean;
|
|
8
9
|
}
|
|
9
10
|
export interface RefactorResult {
|
|
10
11
|
status: 'success' | 'failed';
|
|
@@ -19,7 +20,31 @@ export interface RefactorResult {
|
|
|
19
20
|
}>;
|
|
20
21
|
error?: string;
|
|
21
22
|
}
|
|
23
|
+
export declare const ENTITY_NAMING_RULES: Record<RefactorType, {
|
|
24
|
+
regex: RegExp;
|
|
25
|
+
convention: string;
|
|
26
|
+
}>;
|
|
27
|
+
export declare const SUPPORTED_FIXTURE_EXTENSIONS: string[];
|
|
22
28
|
export declare class RefactorEngine {
|
|
29
|
+
/**
|
|
30
|
+
* Deterministically parses testing entity type and symbol name from a filesystem path.
|
|
31
|
+
* Supports directory paths, file paths, and paths with trailing slashes.
|
|
32
|
+
*/
|
|
33
|
+
static parseEntityFromPath(filePath: string): {
|
|
34
|
+
type: RefactorType;
|
|
35
|
+
symbol: string;
|
|
36
|
+
} | null;
|
|
37
|
+
/**
|
|
38
|
+
* Deterministically parses source and target paths, validates entity types,
|
|
39
|
+
* and executes symbol refactoring across the workspace.
|
|
40
|
+
*/
|
|
41
|
+
static renameFromPaths(options: {
|
|
42
|
+
workspaceRoot: string;
|
|
43
|
+
fromPath: string;
|
|
44
|
+
toPath: string;
|
|
45
|
+
dryRun?: boolean;
|
|
46
|
+
skipPathRename?: boolean;
|
|
47
|
+
}): Promise<RefactorResult>;
|
|
23
48
|
private static findFiles;
|
|
24
49
|
static renameSymbol(options: RenameOptions): Promise<RefactorResult>;
|
|
25
50
|
}
|
|
@@ -1,7 +1,130 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { TypeGenerator } from '../generator/type-generator.js';
|
|
4
|
+
export const ENTITY_NAMING_RULES = {
|
|
5
|
+
'page-object': {
|
|
6
|
+
regex: /^[A-Z][a-zA-Z0-9]*$/,
|
|
7
|
+
convention: 'PascalCase (e.g. "LoginPage", "AuthPage")',
|
|
8
|
+
},
|
|
9
|
+
step: {
|
|
10
|
+
regex: /^[a-z][a-zA-Z0-9]*$/,
|
|
11
|
+
convention: 'camelCase (e.g. "loginUser", "loginAsAdmin")',
|
|
12
|
+
},
|
|
13
|
+
action: {
|
|
14
|
+
regex: /^[a-z][a-zA-Z0-9]*$/,
|
|
15
|
+
convention: 'camelCase (e.g. "customSwipe", "swipeTop")',
|
|
16
|
+
},
|
|
17
|
+
fixture: {
|
|
18
|
+
regex: /^[a-z][a-zA-Z0-9_]*$/,
|
|
19
|
+
convention: 'camelCase or snake_case starting with lowercase (e.g. "users", "apiPayloads", "user_accounts")',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export const SUPPORTED_FIXTURE_EXTENSIONS = ['.json', '.csv', '.yaml', '.yml', '.ts', '.js'];
|
|
4
23
|
export class RefactorEngine {
|
|
24
|
+
/**
|
|
25
|
+
* Deterministically parses testing entity type and symbol name from a filesystem path.
|
|
26
|
+
* Supports directory paths, file paths, and paths with trailing slashes.
|
|
27
|
+
*/
|
|
28
|
+
static parseEntityFromPath(filePath) {
|
|
29
|
+
if (!filePath || typeof filePath !== 'string')
|
|
30
|
+
return null;
|
|
31
|
+
const normalized = filePath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
32
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
33
|
+
// 1. Page Objects: page-objects/<name> or page-objects/<name>/web.ts or page-objects/<name>.ts
|
|
34
|
+
const poIdx = parts.findIndex((p) => p === 'page-objects');
|
|
35
|
+
if (poIdx !== -1 && poIdx + 1 < parts.length) {
|
|
36
|
+
const rawSymbol = parts[poIdx + 1];
|
|
37
|
+
const symbol = path.parse(rawSymbol).name;
|
|
38
|
+
if (symbol)
|
|
39
|
+
return { type: 'page-object', symbol };
|
|
40
|
+
}
|
|
41
|
+
// 2. Steps: support/steps/<name> or steps/<name> or steps/<name>/step.md or steps/<name>.ts
|
|
42
|
+
const stepIdx = parts.findIndex((p) => p === 'steps');
|
|
43
|
+
if (stepIdx !== -1 && stepIdx + 1 < parts.length) {
|
|
44
|
+
const rawSymbol = parts[stepIdx + 1];
|
|
45
|
+
const symbol = path.parse(rawSymbol).name;
|
|
46
|
+
if (symbol)
|
|
47
|
+
return { type: 'step', symbol };
|
|
48
|
+
}
|
|
49
|
+
// 3. Actions: support/actions/<name> or actions/<name> or actions/<name>/action.ts or actions/<name>.ts
|
|
50
|
+
const actionIdx = parts.findIndex((p) => p === 'actions');
|
|
51
|
+
if (actionIdx !== -1 && actionIdx + 1 < parts.length) {
|
|
52
|
+
const rawSymbol = parts[actionIdx + 1];
|
|
53
|
+
const symbol = path.parse(rawSymbol).name;
|
|
54
|
+
if (symbol)
|
|
55
|
+
return { type: 'action', symbol };
|
|
56
|
+
}
|
|
57
|
+
// 4. Fixtures: fixtures/<name>.<ext> or fixtures/nested/<name>.<ext> or fixtures/<name>
|
|
58
|
+
const fixIdx = parts.findIndex((p) => p === 'fixtures');
|
|
59
|
+
if (fixIdx !== -1 && fixIdx + 1 < parts.length) {
|
|
60
|
+
const lastPart = parts[parts.length - 1];
|
|
61
|
+
const symbol = path.parse(lastPart).name;
|
|
62
|
+
if (symbol)
|
|
63
|
+
return { type: 'fixture', symbol };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Deterministically parses source and target paths, validates entity types,
|
|
69
|
+
* and executes symbol refactoring across the workspace.
|
|
70
|
+
*/
|
|
71
|
+
static async renameFromPaths(options) {
|
|
72
|
+
const { workspaceRoot, fromPath, toPath, dryRun, skipPathRename } = options;
|
|
73
|
+
if (!fromPath || !toPath) {
|
|
74
|
+
return {
|
|
75
|
+
status: 'failed',
|
|
76
|
+
entityType: 'page-object',
|
|
77
|
+
oldSymbol: fromPath || '',
|
|
78
|
+
newSymbol: toPath || '',
|
|
79
|
+
modifiedFiles: [],
|
|
80
|
+
occurrencesReplaced: 0,
|
|
81
|
+
error: 'Both source and target paths must be specified for path-based refactoring.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const fromParsed = RefactorEngine.parseEntityFromPath(fromPath);
|
|
85
|
+
if (!fromParsed) {
|
|
86
|
+
return {
|
|
87
|
+
status: 'failed',
|
|
88
|
+
entityType: 'page-object',
|
|
89
|
+
oldSymbol: fromPath,
|
|
90
|
+
newSymbol: toPath,
|
|
91
|
+
modifiedFiles: [],
|
|
92
|
+
occurrencesReplaced: 0,
|
|
93
|
+
error: `Unable to determine entity type from source path: "${fromPath}". Path must be within page-objects/, support/steps/, support/actions/, or fixtures/.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const toParsed = RefactorEngine.parseEntityFromPath(toPath);
|
|
97
|
+
if (!toParsed) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'failed',
|
|
100
|
+
entityType: fromParsed.type,
|
|
101
|
+
oldSymbol: fromParsed.symbol,
|
|
102
|
+
newSymbol: toPath,
|
|
103
|
+
modifiedFiles: [],
|
|
104
|
+
occurrencesReplaced: 0,
|
|
105
|
+
error: `Unable to determine entity type from target path: "${toPath}". Path must be within page-objects/, support/steps/, support/actions/, or fixtures/.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (fromParsed.type !== toParsed.type) {
|
|
109
|
+
return {
|
|
110
|
+
status: 'failed',
|
|
111
|
+
entityType: fromParsed.type,
|
|
112
|
+
oldSymbol: fromParsed.symbol,
|
|
113
|
+
newSymbol: toParsed.symbol,
|
|
114
|
+
modifiedFiles: [],
|
|
115
|
+
occurrencesReplaced: 0,
|
|
116
|
+
error: `Entity type mismatch: source is "${fromParsed.type}" but target is "${toParsed.type}". Cross-entity refactoring is not supported.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return RefactorEngine.renameSymbol({
|
|
120
|
+
workspaceRoot,
|
|
121
|
+
type: fromParsed.type,
|
|
122
|
+
oldName: fromParsed.symbol,
|
|
123
|
+
newName: toParsed.symbol,
|
|
124
|
+
dryRun,
|
|
125
|
+
skipPathRename,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
5
128
|
static findFiles(dir, extensions) {
|
|
6
129
|
const results = [];
|
|
7
130
|
if (!fs.existsSync(dir))
|
|
@@ -30,7 +153,7 @@ export class RefactorEngine {
|
|
|
30
153
|
return results;
|
|
31
154
|
}
|
|
32
155
|
static async renameSymbol(options) {
|
|
33
|
-
const { workspaceRoot, type, oldName, newName, dryRun = false } = options;
|
|
156
|
+
const { workspaceRoot, type, oldName, newName, dryRun = false, skipPathRename = false } = options;
|
|
34
157
|
if (!oldName || !newName || oldName === newName) {
|
|
35
158
|
return {
|
|
36
159
|
status: 'failed',
|
|
@@ -42,60 +165,133 @@ export class RefactorEngine {
|
|
|
42
165
|
error: 'Invalid or identical old/new symbol names',
|
|
43
166
|
};
|
|
44
167
|
}
|
|
168
|
+
let targetOldName = oldName;
|
|
169
|
+
let targetNewName = newName;
|
|
170
|
+
if (type === 'fixture') {
|
|
171
|
+
const parsedOld = path.parse(oldName);
|
|
172
|
+
if (parsedOld.ext && SUPPORTED_FIXTURE_EXTENSIONS.includes(parsedOld.ext.toLowerCase())) {
|
|
173
|
+
targetOldName = parsedOld.name;
|
|
174
|
+
}
|
|
175
|
+
const parsedNew = path.parse(newName);
|
|
176
|
+
if (parsedNew.ext && SUPPORTED_FIXTURE_EXTENSIONS.includes(parsedNew.ext.toLowerCase())) {
|
|
177
|
+
targetNewName = parsedNew.name;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const rule = ENTITY_NAMING_RULES[type];
|
|
181
|
+
if (rule && !rule.regex.test(targetNewName)) {
|
|
182
|
+
return {
|
|
183
|
+
status: 'failed',
|
|
184
|
+
entityType: type,
|
|
185
|
+
oldSymbol: oldName,
|
|
186
|
+
newSymbol: newName,
|
|
187
|
+
modifiedFiles: [],
|
|
188
|
+
occurrencesReplaced: 0,
|
|
189
|
+
error: `Invalid new symbol name "${targetNewName}". Symbol name for ${type} must follow ${rule.convention}.`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
45
192
|
// Phase 1: Determine file operations and replacement patterns
|
|
46
193
|
const plannedRenames = [];
|
|
47
194
|
const plannedEdits = [];
|
|
48
|
-
let oldSymbol =
|
|
49
|
-
let newSymbol =
|
|
50
|
-
|
|
195
|
+
let oldSymbol = targetOldName;
|
|
196
|
+
let newSymbol = targetNewName;
|
|
197
|
+
const fixtureDirs = [
|
|
198
|
+
path.join(workspaceRoot, 'fixtures'),
|
|
199
|
+
path.join(workspaceRoot, 'support', 'fixtures'),
|
|
200
|
+
path.join(workspaceRoot, 'shared', 'fixtures'),
|
|
201
|
+
];
|
|
202
|
+
function findFixtureFiles(dir) {
|
|
203
|
+
const files = [];
|
|
204
|
+
if (!fs.existsSync(dir))
|
|
205
|
+
return files;
|
|
206
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
const fullPath = path.join(dir, entry.name);
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
211
|
+
files.push(...findFixtureFiles(fullPath));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (entry.isFile()) {
|
|
215
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
216
|
+
if (SUPPORTED_FIXTURE_EXTENSIONS.includes(ext)) {
|
|
217
|
+
files.push(fullPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return files;
|
|
222
|
+
}
|
|
223
|
+
// 1. Target Directory / File Discovery for Source Entity
|
|
51
224
|
if (type === 'page-object') {
|
|
52
|
-
oldSymbol =
|
|
53
|
-
newSymbol =
|
|
54
|
-
const
|
|
225
|
+
oldSymbol = targetOldName;
|
|
226
|
+
newSymbol = targetNewName;
|
|
227
|
+
const targetPoDir = path.join(workspaceRoot, 'page-objects', targetNewName);
|
|
228
|
+
const poDir = path.join(workspaceRoot, 'page-objects', targetOldName);
|
|
55
229
|
if (fs.existsSync(poDir)) {
|
|
56
|
-
plannedRenames.push({ from: poDir, to:
|
|
230
|
+
plannedRenames.push({ from: poDir, to: targetPoDir });
|
|
231
|
+
}
|
|
232
|
+
else if (skipPathRename && fs.existsSync(targetPoDir)) {
|
|
233
|
+
plannedRenames.push({ from: poDir, to: targetPoDir });
|
|
57
234
|
}
|
|
58
235
|
}
|
|
59
236
|
else if (type === 'step') {
|
|
60
|
-
oldSymbol = `Step.${
|
|
61
|
-
newSymbol = `Step.${
|
|
237
|
+
oldSymbol = `Step.${targetOldName}`;
|
|
238
|
+
newSymbol = `Step.${targetNewName}`;
|
|
62
239
|
const stepCandidates = [
|
|
63
|
-
path.join(workspaceRoot, 'support', 'steps',
|
|
64
|
-
path.join(workspaceRoot, 'steps',
|
|
240
|
+
path.join(workspaceRoot, 'support', 'steps', targetOldName),
|
|
241
|
+
path.join(workspaceRoot, 'steps', targetOldName),
|
|
65
242
|
];
|
|
66
243
|
for (const cand of stepCandidates) {
|
|
67
244
|
if (fs.existsSync(cand)) {
|
|
68
245
|
const parent = path.dirname(cand);
|
|
69
|
-
plannedRenames.push({ from: cand, to: path.join(parent,
|
|
246
|
+
plannedRenames.push({ from: cand, to: path.join(parent, targetNewName) });
|
|
247
|
+
}
|
|
248
|
+
else if (skipPathRename) {
|
|
249
|
+
const parent = path.dirname(cand);
|
|
250
|
+
const targetCand = path.join(parent, targetNewName);
|
|
251
|
+
if (fs.existsSync(targetCand)) {
|
|
252
|
+
plannedRenames.push({ from: cand, to: targetCand });
|
|
253
|
+
}
|
|
70
254
|
}
|
|
71
255
|
}
|
|
72
256
|
}
|
|
73
257
|
else if (type === 'action') {
|
|
74
|
-
oldSymbol = `Spectra.${
|
|
75
|
-
newSymbol = `Spectra.${
|
|
258
|
+
oldSymbol = `Spectra.${targetOldName}`;
|
|
259
|
+
newSymbol = `Spectra.${targetNewName}`;
|
|
76
260
|
const actionCandidates = [
|
|
77
|
-
path.join(workspaceRoot, 'support', 'actions',
|
|
78
|
-
path.join(workspaceRoot, 'actions',
|
|
261
|
+
path.join(workspaceRoot, 'support', 'actions', targetOldName),
|
|
262
|
+
path.join(workspaceRoot, 'actions', targetOldName),
|
|
79
263
|
];
|
|
80
264
|
for (const cand of actionCandidates) {
|
|
81
265
|
if (fs.existsSync(cand)) {
|
|
82
266
|
const parent = path.dirname(cand);
|
|
83
|
-
plannedRenames.push({ from: cand, to: path.join(parent,
|
|
267
|
+
plannedRenames.push({ from: cand, to: path.join(parent, targetNewName) });
|
|
268
|
+
}
|
|
269
|
+
else if (skipPathRename) {
|
|
270
|
+
const parent = path.dirname(cand);
|
|
271
|
+
const targetCand = path.join(parent, targetNewName);
|
|
272
|
+
if (fs.existsSync(targetCand)) {
|
|
273
|
+
plannedRenames.push({ from: cand, to: targetCand });
|
|
274
|
+
}
|
|
84
275
|
}
|
|
85
276
|
}
|
|
86
277
|
}
|
|
87
278
|
else if (type === 'fixture') {
|
|
88
|
-
oldSymbol = `Fixture.${
|
|
89
|
-
newSymbol = `Fixture.${
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
279
|
+
oldSymbol = `Fixture.${targetOldName}`;
|
|
280
|
+
newSymbol = `Fixture.${targetNewName}`;
|
|
281
|
+
for (const dir of fixtureDirs) {
|
|
282
|
+
const fixtureFiles = findFixtureFiles(dir);
|
|
283
|
+
for (const file of fixtureFiles) {
|
|
284
|
+
const parsed = path.parse(file);
|
|
285
|
+
if (parsed.name === targetOldName) {
|
|
286
|
+
plannedRenames.push({
|
|
287
|
+
from: file,
|
|
288
|
+
to: path.join(parsed.dir, `${targetNewName}${parsed.ext}`),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (skipPathRename && parsed.name === targetNewName) {
|
|
96
292
|
plannedRenames.push({
|
|
97
|
-
from: path.join(
|
|
98
|
-
to:
|
|
293
|
+
from: path.join(parsed.dir, `${targetOldName}${parsed.ext}`),
|
|
294
|
+
to: file,
|
|
99
295
|
});
|
|
100
296
|
}
|
|
101
297
|
}
|
|
@@ -110,6 +306,9 @@ export class RefactorEngine {
|
|
|
110
306
|
path.join(workspaceRoot, 'steps'),
|
|
111
307
|
path.join(workspaceRoot, 'actions'),
|
|
112
308
|
path.join(workspaceRoot, 'global-hooks'),
|
|
309
|
+
path.join(workspaceRoot, 'packages'),
|
|
310
|
+
path.join(workspaceRoot, 'apps'),
|
|
311
|
+
path.join(workspaceRoot, 'shared'),
|
|
113
312
|
];
|
|
114
313
|
const allCodeFiles = searchDirs.flatMap((dir) => this.findFiles(dir, ['.ts', '.js', '.md']));
|
|
115
314
|
const uniqueFiles = Array.from(new Set(allCodeFiles));
|
|
@@ -126,6 +325,20 @@ export class RefactorEngine {
|
|
|
126
325
|
modified = modified.replace(classReg, `class ${newName}`);
|
|
127
326
|
count += classMatches;
|
|
128
327
|
}
|
|
328
|
+
// Replace `new OldName` -> `new NewName` (e.g. `export default new OldName()`, `new OldName(...)`)
|
|
329
|
+
const newReg = new RegExp(`\\bnew\\s+${oldName}\\b`, 'g');
|
|
330
|
+
const newMatches = (modified.match(newReg) || []).length;
|
|
331
|
+
if (newMatches > 0) {
|
|
332
|
+
modified = modified.replace(newReg, `new ${newName}`);
|
|
333
|
+
count += newMatches;
|
|
334
|
+
}
|
|
335
|
+
// Replace `export default OldName` -> `export default NewName`
|
|
336
|
+
const exportDefaultReg = new RegExp(`\\bexport\\s+default\\s+${oldName}\\b`, 'g');
|
|
337
|
+
const exportDefaultMatches = (modified.match(exportDefaultReg) || []).length;
|
|
338
|
+
if (exportDefaultMatches > 0) {
|
|
339
|
+
modified = modified.replace(exportDefaultReg, `export default ${newName}`);
|
|
340
|
+
count += exportDefaultMatches;
|
|
341
|
+
}
|
|
129
342
|
// Replace `OldName.` -> `NewName.`
|
|
130
343
|
const refReg = new RegExp(`\\b${oldName}\\.`, 'g');
|
|
131
344
|
const refMatches = (modified.match(refReg) || []).length;
|
|
@@ -161,12 +374,20 @@ export class RefactorEngine {
|
|
|
161
374
|
}
|
|
162
375
|
else if (type === 'fixture') {
|
|
163
376
|
// Replace `Fixture.oldFixture` -> `Fixture.newFixture`
|
|
164
|
-
const fixReg = new RegExp(`\\bFixture\\.${
|
|
377
|
+
const fixReg = new RegExp(`\\bFixture\\.${targetOldName}\\b`, 'g');
|
|
165
378
|
const fixMatches = (modified.match(fixReg) || []).length;
|
|
166
379
|
if (fixMatches > 0) {
|
|
167
|
-
modified = modified.replace(fixReg, `Fixture.${
|
|
380
|
+
modified = modified.replace(fixReg, `Fixture.${targetNewName}`);
|
|
168
381
|
count += fixMatches;
|
|
169
382
|
}
|
|
383
|
+
// Also replace file references with supported extensions (e.g. 'users.csv', 'users.yaml', etc.)
|
|
384
|
+
const extPattern = SUPPORTED_FIXTURE_EXTENSIONS.map((e) => e.replace('.', '')).join('|');
|
|
385
|
+
const fixFileReg = new RegExp(`(?<!Fixture\\.)\\b${targetOldName}\\.(${extPattern})\\b`, 'g');
|
|
386
|
+
const fixFileMatches = (modified.match(fixFileReg) || []).length;
|
|
387
|
+
if (fixFileMatches > 0) {
|
|
388
|
+
modified = modified.replace(fixFileReg, `${targetNewName}.$1`);
|
|
389
|
+
count += fixFileMatches;
|
|
390
|
+
}
|
|
170
391
|
}
|
|
171
392
|
if (count > 0) {
|
|
172
393
|
totalOccurrences += count;
|
|
@@ -178,6 +399,108 @@ export class RefactorEngine {
|
|
|
178
399
|
});
|
|
179
400
|
}
|
|
180
401
|
}
|
|
402
|
+
// Source Existence Guard:
|
|
403
|
+
// If no filesystem files/directories match the source symbol AND no code occurrences were found,
|
|
404
|
+
// the source symbol does not exist anywhere in the workspace.
|
|
405
|
+
if (plannedRenames.length === 0 && totalOccurrences === 0) {
|
|
406
|
+
return {
|
|
407
|
+
status: 'failed',
|
|
408
|
+
entityType: type,
|
|
409
|
+
oldSymbol,
|
|
410
|
+
newSymbol,
|
|
411
|
+
modifiedFiles: [],
|
|
412
|
+
occurrencesReplaced: 0,
|
|
413
|
+
error: `Source ${type} "${oldName}" was not found in the workspace (no matching files or symbol occurrences).`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
// Target Collision Guards (Priority 2 - checked only if source entity exists and skipPathRename is false)
|
|
417
|
+
if (!skipPathRename) {
|
|
418
|
+
if (type === 'page-object') {
|
|
419
|
+
const targetPoDir = path.join(workspaceRoot, 'page-objects', targetNewName);
|
|
420
|
+
if (fs.existsSync(targetPoDir)) {
|
|
421
|
+
return {
|
|
422
|
+
status: 'failed',
|
|
423
|
+
entityType: type,
|
|
424
|
+
oldSymbol,
|
|
425
|
+
newSymbol,
|
|
426
|
+
modifiedFiles: [],
|
|
427
|
+
occurrencesReplaced: 0,
|
|
428
|
+
error: `Target page-object "${targetNewName}" already exists at "${path.relative(workspaceRoot, targetPoDir)}"`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else if (type === 'step') {
|
|
433
|
+
const targetStepCandidates = [
|
|
434
|
+
path.join(workspaceRoot, 'support', 'steps', targetNewName),
|
|
435
|
+
path.join(workspaceRoot, 'steps', targetNewName),
|
|
436
|
+
];
|
|
437
|
+
for (const cand of targetStepCandidates) {
|
|
438
|
+
if (fs.existsSync(cand)) {
|
|
439
|
+
return {
|
|
440
|
+
status: 'failed',
|
|
441
|
+
entityType: type,
|
|
442
|
+
oldSymbol,
|
|
443
|
+
newSymbol,
|
|
444
|
+
modifiedFiles: [],
|
|
445
|
+
occurrencesReplaced: 0,
|
|
446
|
+
error: `Target step "${targetNewName}" already exists at "${path.relative(workspaceRoot, cand)}"`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else if (type === 'action') {
|
|
452
|
+
const targetActionCandidates = [
|
|
453
|
+
path.join(workspaceRoot, 'support', 'actions', targetNewName),
|
|
454
|
+
path.join(workspaceRoot, 'actions', targetNewName),
|
|
455
|
+
];
|
|
456
|
+
for (const cand of targetActionCandidates) {
|
|
457
|
+
if (fs.existsSync(cand)) {
|
|
458
|
+
return {
|
|
459
|
+
status: 'failed',
|
|
460
|
+
entityType: type,
|
|
461
|
+
oldSymbol,
|
|
462
|
+
newSymbol,
|
|
463
|
+
modifiedFiles: [],
|
|
464
|
+
occurrencesReplaced: 0,
|
|
465
|
+
error: `Target action "${targetNewName}" already exists at "${path.relative(workspaceRoot, cand)}"`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else if (type === 'fixture') {
|
|
471
|
+
for (const dir of fixtureDirs) {
|
|
472
|
+
const fixtureFiles = findFixtureFiles(dir);
|
|
473
|
+
for (const file of fixtureFiles) {
|
|
474
|
+
const parsed = path.parse(file);
|
|
475
|
+
if (parsed.name === targetNewName) {
|
|
476
|
+
return {
|
|
477
|
+
status: 'failed',
|
|
478
|
+
entityType: type,
|
|
479
|
+
oldSymbol,
|
|
480
|
+
newSymbol,
|
|
481
|
+
modifiedFiles: [],
|
|
482
|
+
occurrencesReplaced: 0,
|
|
483
|
+
error: `Target fixture "${targetNewName}" already exists at "${path.relative(workspaceRoot, file)}"`,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Universal pre-commit collision guard
|
|
490
|
+
for (const rn of plannedRenames) {
|
|
491
|
+
if (fs.existsSync(rn.to)) {
|
|
492
|
+
return {
|
|
493
|
+
status: 'failed',
|
|
494
|
+
entityType: type,
|
|
495
|
+
oldSymbol,
|
|
496
|
+
newSymbol,
|
|
497
|
+
modifiedFiles: [],
|
|
498
|
+
occurrencesReplaced: 0,
|
|
499
|
+
error: `Target destination already exists on disk: "${path.relative(workspaceRoot, rn.to)}"`,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
181
504
|
if (dryRun) {
|
|
182
505
|
return {
|
|
183
506
|
status: 'success',
|
|
@@ -200,19 +523,26 @@ export class RefactorEngine {
|
|
|
200
523
|
modifiedFilesWritten.push(edit.filePath);
|
|
201
524
|
}
|
|
202
525
|
// 2. Perform path renames (directories and files)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
fs.
|
|
208
|
-
|
|
526
|
+
if (!skipPathRename) {
|
|
527
|
+
for (const rn of plannedRenames) {
|
|
528
|
+
if (fs.existsSync(rn.from)) {
|
|
529
|
+
const parent = path.dirname(rn.to);
|
|
530
|
+
if (!fs.existsSync(parent))
|
|
531
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
532
|
+
fs.renameSync(rn.from, rn.to);
|
|
533
|
+
}
|
|
209
534
|
}
|
|
210
535
|
}
|
|
211
536
|
// 3. Automatically regenerate ambient declarations post-refactor
|
|
212
537
|
TypeGenerator.generateAll(workspaceRoot);
|
|
213
538
|
const typesDir = path.join(workspaceRoot, '.testspectra', 'types');
|
|
214
539
|
if (fs.existsSync(typesDir)) {
|
|
215
|
-
|
|
540
|
+
if (fs.existsSync(path.join(typesDir, 'web.d.ts'))) {
|
|
541
|
+
modifiedFilesWritten.push(path.join(typesDir, 'web.d.ts'));
|
|
542
|
+
}
|
|
543
|
+
if (fs.existsSync(path.join(typesDir, 'fixtures.d.ts'))) {
|
|
544
|
+
modifiedFilesWritten.push(path.join(typesDir, 'fixtures.d.ts'));
|
|
545
|
+
}
|
|
216
546
|
}
|
|
217
547
|
return {
|
|
218
548
|
status: 'success',
|
|
@@ -233,12 +563,14 @@ export class RefactorEngine {
|
|
|
233
563
|
catch { }
|
|
234
564
|
}
|
|
235
565
|
// Reverse path renames
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
566
|
+
if (!skipPathRename) {
|
|
567
|
+
for (const rn of plannedRenames) {
|
|
568
|
+
if (fs.existsSync(rn.to)) {
|
|
569
|
+
try {
|
|
570
|
+
fs.renameSync(rn.to, rn.from);
|
|
571
|
+
}
|
|
572
|
+
catch { }
|
|
240
573
|
}
|
|
241
|
-
catch { }
|
|
242
574
|
}
|
|
243
575
|
}
|
|
244
576
|
return {
|
|
@@ -13,6 +13,17 @@ function extractTargetString(target) {
|
|
|
13
13
|
}
|
|
14
14
|
return String(target);
|
|
15
15
|
}
|
|
16
|
+
/** Renders a `ScopedSelector`'s `.parent` chain (produced by `element.get()`/`.getAll()`
|
|
17
|
+
* chaining) as `"parent" within "grandparent"`, nearest ancestor first — or null when the
|
|
18
|
+
* target isn't scoped. */
|
|
19
|
+
function extractWithinChain(target) {
|
|
20
|
+
if (!target || typeof target !== 'object' || !target.parent)
|
|
21
|
+
return null;
|
|
22
|
+
const chain = [];
|
|
23
|
+
for (let p = target.parent; p; p = p.parent)
|
|
24
|
+
chain.push(p.selector);
|
|
25
|
+
return chain.join('" within "');
|
|
26
|
+
}
|
|
16
27
|
function formatElementTarget(target) {
|
|
17
28
|
const sel = extractTargetString(target);
|
|
18
29
|
if (!sel)
|
|
@@ -20,7 +31,8 @@ function formatElementTarget(target) {
|
|
|
20
31
|
if (sel.startsWith('element "') || sel.startsWith('collection "')) {
|
|
21
32
|
return sel;
|
|
22
33
|
}
|
|
23
|
-
|
|
34
|
+
const within = extractWithinChain(target);
|
|
35
|
+
return within ? `element "${sel}" within "${within}"` : `element "${sel}"`;
|
|
24
36
|
}
|
|
25
37
|
function formatCollectionTarget(target) {
|
|
26
38
|
const sel = extractTargetString(target);
|
|
@@ -29,7 +41,8 @@ function formatCollectionTarget(target) {
|
|
|
29
41
|
if (sel.startsWith('collection "')) {
|
|
30
42
|
return sel;
|
|
31
43
|
}
|
|
32
|
-
|
|
44
|
+
const within = extractWithinChain(target);
|
|
45
|
+
return within ? `collection "${sel}" within "${within}"` : `collection "${sel}"`;
|
|
33
46
|
}
|
|
34
47
|
/**
|
|
35
48
|
* Transforms raw matcher assertions and element commands into grammatically correct English sentences.
|
|
@@ -37,6 +50,11 @@ function formatCollectionTarget(target) {
|
|
|
37
50
|
export function formatAction(actionKey, target, args = {}) {
|
|
38
51
|
const el = formatElementTarget(target);
|
|
39
52
|
switch (actionKey) {
|
|
53
|
+
// Component testing only (@testspectra/react's Spectra.mount()) — target is a component name,
|
|
54
|
+
// not a DOM selector, so it's rendered plainly rather than through formatElementTarget's
|
|
55
|
+
// `element "..."` wrapping.
|
|
56
|
+
case 'mount':
|
|
57
|
+
return `Mount component "${extractTargetString(target) || '{component}'}"`;
|
|
40
58
|
case 'click':
|
|
41
59
|
return `Click on ${el}`;
|
|
42
60
|
case 'doubleClick':
|
|
@@ -83,6 +101,8 @@ export function formatAction(actionKey, target, args = {}) {
|
|
|
83
101
|
return `Intercept ${args.method ?? '{method}'} "${args.url ?? '{url}'}" mock rule registered`;
|
|
84
102
|
case 'clearCookies':
|
|
85
103
|
return `Clear all browser cookies`;
|
|
104
|
+
case 'setCookies':
|
|
105
|
+
return `Set cookies for "${args.url ?? '{url}'}"`;
|
|
86
106
|
case 'clearLocalStorage':
|
|
87
107
|
return `Clear browser localStorage`;
|
|
88
108
|
default:
|
package/dist/reporter/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'intercept' | 'clearCookies' | 'clearLocalStorage';
|
|
1
|
+
export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'intercept' | 'clearCookies' | 'setCookies' | 'clearLocalStorage';
|
|
2
2
|
export type AssertionKey = 'be.visible' | 'not.be.visible' | 'exist' | 'not.exist' | 'be.clickable' | 'not.be.clickable' | 'be.enabled' | 'be.disabled' | 'be.checked' | 'not.be.checked' | 'be.selected' | 'not.be.selected' | 'be.focused' | 'not.be.focused' | 'have.text' | 'not.have.text' | 'contain.text' | 'not.contain.text' | 'have.value' | 'not.have.value' | 'contain.value' | 'not.contain.value' | 'have.attr' | 'not.have.attr' | 'have.class' | 'not.have.class' | 'have.css' | 'not.have.css' | 'have.length' | 'not.have.length' | 'have.length.greaterThan' | 'have.length.lessThan' | 'be.empty' | 'not.be.empty' | 'shouldHaveUrl' | 'shouldContainUrl' | 'shouldHaveTitle' | 'shouldContainTitle' | 'shouldBeLoaded' | 'shouldBePageLoaded' | 'shouldHaveNoConsoleErrors' | 'have.url' | 'contain.url' | 'have.title' | 'contain.title' | 'be.loaded' | 'be.pageLoaded' | 'have.noConsoleErrors';
|
package/dist/runner/bridge.d.ts
CHANGED