@testspectra/cli 1.1.6 → 1.1.8-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/.testspectra-runner.hash +1 -1
- package/bin/testspectra-runner +0 -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/demo.js +17 -7
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +6 -1
- 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 +76 -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 +1 -1
- package/dist/generator/tsconfig-generator.js +44 -16
- package/dist/generator/type-generator.d.ts +24 -1
- package/dist/generator/type-generator.js +104 -8
- 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 +17 -2
- package/dist/reporter/types.d.ts +1 -1
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +34 -21
- 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 +29 -25
- 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 +5388 -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 +14389 -0
- package/templates/nx/package.json +3 -3
- 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/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/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/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
|
@@ -45,6 +45,63 @@ describe('CLI Workspace-Wide Symbol Refactoring Engine (docs/v2/cli/synchronized
|
|
|
45
45
|
expect(updatedSpec).toContain('AuthPage.username');
|
|
46
46
|
expect(updatedSpec).not.toContain('LoginPage.username');
|
|
47
47
|
});
|
|
48
|
+
it('renames page-object with export default new ClassName() pattern across web.ts and mobile.ts', async () => {
|
|
49
|
+
// 1. Setup MatchersPage directory with both mobile.ts and web.ts matching the default template
|
|
50
|
+
const poDir = path.join(tmpDir, 'page-objects', 'MatchersPage');
|
|
51
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(poDir, 'mobile.ts'), 'export class MatchersPage {\n get visibleElement() { return Spectra.get("~visible"); }\n}\n\nexport default new MatchersPage();\n');
|
|
53
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'export class MatchersPage {\n get visibleElement() { return Spectra.get("#visible"); }\n}\n\nexport default new MatchersPage();\n');
|
|
54
|
+
// 2. Setup spec referencing MatchersPage
|
|
55
|
+
const specDir = path.join(tmpDir, 'specs', 'Matchers');
|
|
56
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
57
|
+
const specFile = path.join(specDir, 'mobile.test.ts');
|
|
58
|
+
fs.writeFileSync(specFile, 'await MatchersPage.visibleElement.shouldBeVisible();\nconst instance = new MatchersPage();');
|
|
59
|
+
const result = await RefactorEngine.renameSymbol({
|
|
60
|
+
workspaceRoot: tmpDir,
|
|
61
|
+
type: 'page-object',
|
|
62
|
+
oldName: 'MatchersPage',
|
|
63
|
+
newName: 'AssertionsPage',
|
|
64
|
+
});
|
|
65
|
+
expect(result.status).toBe('success');
|
|
66
|
+
expect(result.oldSymbol).toBe('MatchersPage');
|
|
67
|
+
expect(result.newSymbol).toBe('AssertionsPage');
|
|
68
|
+
// Verify folder was renamed
|
|
69
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'MatchersPage'))).toBe(false);
|
|
70
|
+
const targetDir = path.join(tmpDir, 'page-objects', 'AssertionsPage');
|
|
71
|
+
expect(fs.existsSync(targetDir)).toBe(true);
|
|
72
|
+
// Verify mobile.ts updated both `class AssertionsPage` and `export default new AssertionsPage();`
|
|
73
|
+
const updatedMobile = fs.readFileSync(path.join(targetDir, 'mobile.ts'), 'utf-8');
|
|
74
|
+
expect(updatedMobile).toContain('export class AssertionsPage');
|
|
75
|
+
expect(updatedMobile).toContain('export default new AssertionsPage();');
|
|
76
|
+
expect(updatedMobile).not.toContain('MatchersPage');
|
|
77
|
+
// Verify web.ts updated both `class AssertionsPage` and `export default new AssertionsPage();`
|
|
78
|
+
const updatedWeb = fs.readFileSync(path.join(targetDir, 'web.ts'), 'utf-8');
|
|
79
|
+
expect(updatedWeb).toContain('export class AssertionsPage');
|
|
80
|
+
expect(updatedWeb).toContain('export default new AssertionsPage();');
|
|
81
|
+
expect(updatedWeb).not.toContain('MatchersPage');
|
|
82
|
+
// Verify spec updated both static access and new instantiation
|
|
83
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
84
|
+
expect(updatedSpec).toContain('AssertionsPage.visibleElement.shouldBeVisible()');
|
|
85
|
+
expect(updatedSpec).toContain('const instance = new AssertionsPage();');
|
|
86
|
+
expect(updatedSpec).not.toContain('MatchersPage');
|
|
87
|
+
});
|
|
88
|
+
it('renames page-object with export default ClassName (without new)', async () => {
|
|
89
|
+
const poDir = path.join(tmpDir, 'page-objects', 'CustomPage');
|
|
90
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'class CustomPage {}\n\nexport default CustomPage;\n');
|
|
92
|
+
const result = await RefactorEngine.renameSymbol({
|
|
93
|
+
workspaceRoot: tmpDir,
|
|
94
|
+
type: 'page-object',
|
|
95
|
+
oldName: 'CustomPage',
|
|
96
|
+
newName: 'BrandNewPage',
|
|
97
|
+
});
|
|
98
|
+
expect(result.status).toBe('success');
|
|
99
|
+
const targetDir = path.join(tmpDir, 'page-objects', 'BrandNewPage');
|
|
100
|
+
const updatedWeb = fs.readFileSync(path.join(targetDir, 'web.ts'), 'utf-8');
|
|
101
|
+
expect(updatedWeb).toContain('class BrandNewPage');
|
|
102
|
+
expect(updatedWeb).toContain('export default BrandNewPage;');
|
|
103
|
+
expect(updatedWeb).not.toContain('CustomPage');
|
|
104
|
+
});
|
|
48
105
|
it('renames shared step folder, spec directives, and Step call sites', async () => {
|
|
49
106
|
const stepDir = path.join(tmpDir, 'support', 'steps', 'loginUser');
|
|
50
107
|
fs.mkdirSync(stepDir, { recursive: true });
|
|
@@ -172,4 +229,645 @@ describe('CLI Workspace-Wide Symbol Refactoring Engine (docs/v2/cli/synchronized
|
|
|
172
229
|
writeSpy.mockRestore();
|
|
173
230
|
}
|
|
174
231
|
});
|
|
232
|
+
it('enforces entity-specific naming conventions (PascalCase, camelCase, snake_case)', async () => {
|
|
233
|
+
// 1. Page Object must be PascalCase
|
|
234
|
+
const poRes1 = await RefactorEngine.renameSymbol({
|
|
235
|
+
workspaceRoot: tmpDir,
|
|
236
|
+
type: 'page-object',
|
|
237
|
+
oldName: 'LoginPage',
|
|
238
|
+
newName: 'authPage', // rejected: starts with lowercase
|
|
239
|
+
});
|
|
240
|
+
expect(poRes1.status).toBe('failed');
|
|
241
|
+
expect(poRes1.error).toContain('PascalCase');
|
|
242
|
+
// oldName is not restricted by convention so legacy badly-named symbols can be fixed
|
|
243
|
+
fs.mkdirSync(path.join(tmpDir, 'page-objects', 'loginPage'), { recursive: true });
|
|
244
|
+
const poRes2 = await RefactorEngine.renameSymbol({
|
|
245
|
+
workspaceRoot: tmpDir,
|
|
246
|
+
type: 'page-object',
|
|
247
|
+
oldName: 'loginPage', // legacy lowercase symbol
|
|
248
|
+
newName: 'AuthPage', // valid PascalCase
|
|
249
|
+
dryRun: true,
|
|
250
|
+
});
|
|
251
|
+
expect(poRes2.status).toBe('success');
|
|
252
|
+
// 2. Step must be camelCase
|
|
253
|
+
const stepRes1 = await RefactorEngine.renameSymbol({
|
|
254
|
+
workspaceRoot: tmpDir,
|
|
255
|
+
type: 'step',
|
|
256
|
+
oldName: 'loginUser',
|
|
257
|
+
newName: 'LoginAsAdmin', // rejected: PascalCase
|
|
258
|
+
});
|
|
259
|
+
expect(stepRes1.status).toBe('failed');
|
|
260
|
+
expect(stepRes1.error).toContain('camelCase');
|
|
261
|
+
// 3. Action must be camelCase
|
|
262
|
+
const actionRes1 = await RefactorEngine.renameSymbol({
|
|
263
|
+
workspaceRoot: tmpDir,
|
|
264
|
+
type: 'action',
|
|
265
|
+
oldName: 'customSwipe',
|
|
266
|
+
newName: 'SwipeTop', // rejected: PascalCase
|
|
267
|
+
});
|
|
268
|
+
expect(actionRes1.status).toBe('failed');
|
|
269
|
+
expect(actionRes1.error).toContain('camelCase');
|
|
270
|
+
// 4. Fixture allows camelCase and snake_case starting with lowercase
|
|
271
|
+
fs.mkdirSync(path.join(tmpDir, 'fixtures'), { recursive: true });
|
|
272
|
+
fs.writeFileSync(path.join(tmpDir, 'fixtures', 'users.json'), '{}');
|
|
273
|
+
const fixCamelRes = await RefactorEngine.renameSymbol({
|
|
274
|
+
workspaceRoot: tmpDir,
|
|
275
|
+
type: 'fixture',
|
|
276
|
+
oldName: 'users',
|
|
277
|
+
newName: 'userAccounts',
|
|
278
|
+
dryRun: true,
|
|
279
|
+
});
|
|
280
|
+
expect(fixCamelRes.status).toBe('success');
|
|
281
|
+
const fixSnakeRes = await RefactorEngine.renameSymbol({
|
|
282
|
+
workspaceRoot: tmpDir,
|
|
283
|
+
type: 'fixture',
|
|
284
|
+
oldName: 'users',
|
|
285
|
+
newName: 'user_accounts',
|
|
286
|
+
dryRun: true,
|
|
287
|
+
});
|
|
288
|
+
expect(fixSnakeRes.status).toBe('success');
|
|
289
|
+
const fixInvalidRes = await RefactorEngine.renameSymbol({
|
|
290
|
+
workspaceRoot: tmpDir,
|
|
291
|
+
type: 'fixture',
|
|
292
|
+
oldName: 'users',
|
|
293
|
+
newName: 'UserAccounts', // rejected: PascalCase
|
|
294
|
+
});
|
|
295
|
+
expect(fixInvalidRes.status).toBe('failed');
|
|
296
|
+
expect(fixInvalidRes.error).toContain('camelCase or snake_case');
|
|
297
|
+
});
|
|
298
|
+
it('rejects refactoring if source symbol does not exist in the workspace', async () => {
|
|
299
|
+
// 1. Nonexistent page-object
|
|
300
|
+
const poRes = await RefactorEngine.renameSymbol({
|
|
301
|
+
workspaceRoot: tmpDir,
|
|
302
|
+
type: 'page-object',
|
|
303
|
+
oldName: 'NonExistentPage',
|
|
304
|
+
newName: 'ValidNewPage',
|
|
305
|
+
dryRun: true,
|
|
306
|
+
});
|
|
307
|
+
expect(poRes.status).toBe('failed');
|
|
308
|
+
expect(poRes.error).toContain('Source page-object "NonExistentPage" was not found in the workspace');
|
|
309
|
+
// 2. Nonexistent step
|
|
310
|
+
const stepRes = await RefactorEngine.renameSymbol({
|
|
311
|
+
workspaceRoot: tmpDir,
|
|
312
|
+
type: 'step',
|
|
313
|
+
oldName: 'nonExistentStep',
|
|
314
|
+
newName: 'validNewStep',
|
|
315
|
+
dryRun: true,
|
|
316
|
+
});
|
|
317
|
+
expect(stepRes.status).toBe('failed');
|
|
318
|
+
expect(stepRes.error).toContain('Source step "nonExistentStep" was not found in the workspace');
|
|
319
|
+
// 3. Nonexistent action
|
|
320
|
+
const actionRes = await RefactorEngine.renameSymbol({
|
|
321
|
+
workspaceRoot: tmpDir,
|
|
322
|
+
type: 'action',
|
|
323
|
+
oldName: 'nonExistentAction',
|
|
324
|
+
newName: 'validNewAction',
|
|
325
|
+
dryRun: true,
|
|
326
|
+
});
|
|
327
|
+
expect(actionRes.status).toBe('failed');
|
|
328
|
+
expect(actionRes.error).toContain('Source action "nonExistentAction" was not found in the workspace');
|
|
329
|
+
// 4. Nonexistent fixture
|
|
330
|
+
const fixRes = await RefactorEngine.renameSymbol({
|
|
331
|
+
workspaceRoot: tmpDir,
|
|
332
|
+
type: 'fixture',
|
|
333
|
+
oldName: 'nonExistentFixture',
|
|
334
|
+
newName: 'validNewFixture',
|
|
335
|
+
dryRun: true,
|
|
336
|
+
});
|
|
337
|
+
expect(fixRes.status).toBe('failed');
|
|
338
|
+
expect(fixRes.error).toContain('Source fixture "nonExistentFixture" was not found in the workspace');
|
|
339
|
+
});
|
|
340
|
+
it('prioritizes source non-existence error over target collision', async () => {
|
|
341
|
+
const poDir = path.join(tmpDir, 'page-objects', 'ExistingTargetPage');
|
|
342
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
343
|
+
// Target exists, but source does NOT exist
|
|
344
|
+
const result = await RefactorEngine.renameSymbol({
|
|
345
|
+
workspaceRoot: tmpDir,
|
|
346
|
+
type: 'page-object',
|
|
347
|
+
oldName: 'NonExistentSource',
|
|
348
|
+
newName: 'ExistingTargetPage',
|
|
349
|
+
dryRun: true,
|
|
350
|
+
});
|
|
351
|
+
expect(result.status).toBe('failed');
|
|
352
|
+
expect(result.error).toContain('Source page-object "NonExistentSource" was not found in the workspace');
|
|
353
|
+
expect(result.error).not.toContain('already exists');
|
|
354
|
+
});
|
|
355
|
+
it('rejects refactoring if target page-object directory already exists on disk', async () => {
|
|
356
|
+
const poDir1 = path.join(tmpDir, 'page-objects', 'LoginPage');
|
|
357
|
+
const poDir2 = path.join(tmpDir, 'page-objects', 'AuthPage');
|
|
358
|
+
fs.mkdirSync(poDir1, { recursive: true });
|
|
359
|
+
fs.mkdirSync(poDir2, { recursive: true });
|
|
360
|
+
const result = await RefactorEngine.renameSymbol({
|
|
361
|
+
workspaceRoot: tmpDir,
|
|
362
|
+
type: 'page-object',
|
|
363
|
+
oldName: 'LoginPage',
|
|
364
|
+
newName: 'AuthPage',
|
|
365
|
+
});
|
|
366
|
+
expect(result.status).toBe('failed');
|
|
367
|
+
expect(result.error).toContain('already exists');
|
|
368
|
+
});
|
|
369
|
+
it('rejects refactoring if target fixture file already exists on disk', async () => {
|
|
370
|
+
const fixturesDir = path.join(tmpDir, 'fixtures');
|
|
371
|
+
fs.mkdirSync(fixturesDir, { recursive: true });
|
|
372
|
+
fs.writeFileSync(path.join(fixturesDir, 'users.json'), '{}');
|
|
373
|
+
fs.writeFileSync(path.join(fixturesDir, 'accounts.json'), '{}');
|
|
374
|
+
const result = await RefactorEngine.renameSymbol({
|
|
375
|
+
workspaceRoot: tmpDir,
|
|
376
|
+
type: 'fixture',
|
|
377
|
+
oldName: 'users',
|
|
378
|
+
newName: 'accounts',
|
|
379
|
+
});
|
|
380
|
+
expect(result.status).toBe('failed');
|
|
381
|
+
expect(result.error).toContain('already exists');
|
|
382
|
+
});
|
|
383
|
+
it('refactors fixtures across multi-format extensions (.csv, .yaml, .yml, .ts, .js) and string references', async () => {
|
|
384
|
+
const fixturesDir = path.join(tmpDir, 'fixtures');
|
|
385
|
+
fs.mkdirSync(fixturesDir, { recursive: true });
|
|
386
|
+
fs.writeFileSync(path.join(fixturesDir, 'dataset.csv'), 'id,name\n1,Alpha');
|
|
387
|
+
fs.writeFileSync(path.join(fixturesDir, 'config.yaml'), 'env: production');
|
|
388
|
+
fs.writeFileSync(path.join(fixturesDir, 'rules.yml'), 'enabled: true');
|
|
389
|
+
fs.writeFileSync(path.join(fixturesDir, 'mockData.ts'), 'export default {}');
|
|
390
|
+
fs.writeFileSync(path.join(fixturesDir, 'helpers.js'), 'module.exports = {}');
|
|
391
|
+
const specDir = path.join(tmpDir, 'specs', 'Data');
|
|
392
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
393
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
394
|
+
fs.writeFileSync(specFile, "const d = Fixture.dataset;\nconst path = 'fixtures/dataset.csv';\nconst c = Fixture.config;\nconst m = Fixture.mockData;");
|
|
395
|
+
// 1. Refactor dataset.csv -> data_records.csv
|
|
396
|
+
const res1 = await RefactorEngine.renameSymbol({
|
|
397
|
+
workspaceRoot: tmpDir,
|
|
398
|
+
type: 'fixture',
|
|
399
|
+
oldName: 'dataset',
|
|
400
|
+
newName: 'data_records',
|
|
401
|
+
});
|
|
402
|
+
expect(res1.status).toBe('success');
|
|
403
|
+
expect(fs.existsSync(path.join(fixturesDir, 'dataset.csv'))).toBe(false);
|
|
404
|
+
expect(fs.existsSync(path.join(fixturesDir, 'data_records.csv'))).toBe(true);
|
|
405
|
+
// 2. Refactor mockData.ts -> appMocks.ts
|
|
406
|
+
const res2 = await RefactorEngine.renameSymbol({
|
|
407
|
+
workspaceRoot: tmpDir,
|
|
408
|
+
type: 'fixture',
|
|
409
|
+
oldName: 'mockData',
|
|
410
|
+
newName: 'appMocks',
|
|
411
|
+
});
|
|
412
|
+
expect(res2.status).toBe('success');
|
|
413
|
+
expect(fs.existsSync(path.join(fixturesDir, 'mockData.ts'))).toBe(false);
|
|
414
|
+
expect(fs.existsSync(path.join(fixturesDir, 'appMocks.ts'))).toBe(true);
|
|
415
|
+
// Verify spec updates for both Fixture.<name> and 'filename.ext'
|
|
416
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
417
|
+
expect(updatedSpec).toContain('Fixture.data_records');
|
|
418
|
+
expect(updatedSpec).toContain("'fixtures/data_records.csv'");
|
|
419
|
+
expect(updatedSpec).toContain('Fixture.appMocks');
|
|
420
|
+
expect(updatedSpec).not.toContain('Fixture.dataset');
|
|
421
|
+
expect(updatedSpec).not.toContain("'fixtures/dataset.csv'");
|
|
422
|
+
expect(updatedSpec).not.toContain('Fixture.mockData');
|
|
423
|
+
});
|
|
424
|
+
it('supports passing fixture name with extension and nested subdirectories', async () => {
|
|
425
|
+
const nestedDir = path.join(tmpDir, 'fixtures', 'auth');
|
|
426
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
427
|
+
fs.writeFileSync(path.join(nestedDir, 'tokens.yaml'), 'jwt: secret');
|
|
428
|
+
const result = await RefactorEngine.renameSymbol({
|
|
429
|
+
workspaceRoot: tmpDir,
|
|
430
|
+
type: 'fixture',
|
|
431
|
+
oldName: 'tokens.yaml',
|
|
432
|
+
newName: 'auth_tokens.yaml',
|
|
433
|
+
});
|
|
434
|
+
expect(result.status).toBe('success');
|
|
435
|
+
expect(fs.existsSync(path.join(nestedDir, 'tokens.yaml'))).toBe(false);
|
|
436
|
+
expect(fs.existsSync(path.join(nestedDir, 'auth_tokens.yaml'))).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
describe('parseEntityFromPath deterministic detection', () => {
|
|
439
|
+
it('parses page-object paths across directory, file, and trailing slash variants', () => {
|
|
440
|
+
expect(RefactorEngine.parseEntityFromPath('page-objects/LoginPage')).toEqual({
|
|
441
|
+
type: 'page-object',
|
|
442
|
+
symbol: 'LoginPage',
|
|
443
|
+
});
|
|
444
|
+
expect(RefactorEngine.parseEntityFromPath('page-objects/LoginPage/')).toEqual({
|
|
445
|
+
type: 'page-object',
|
|
446
|
+
symbol: 'LoginPage',
|
|
447
|
+
});
|
|
448
|
+
expect(RefactorEngine.parseEntityFromPath('page-objects/LoginPage/web.ts')).toEqual({
|
|
449
|
+
type: 'page-object',
|
|
450
|
+
symbol: 'LoginPage',
|
|
451
|
+
});
|
|
452
|
+
expect(RefactorEngine.parseEntityFromPath('page-objects/LoginPage.ts')).toEqual({
|
|
453
|
+
type: 'page-object',
|
|
454
|
+
symbol: 'LoginPage',
|
|
455
|
+
});
|
|
456
|
+
expect(RefactorEngine.parseEntityFromPath('C:\\repo\\page-objects\\LoginPage')).toEqual({
|
|
457
|
+
type: 'page-object',
|
|
458
|
+
symbol: 'LoginPage',
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
it('parses step paths across support/steps, steps, and nested step files', () => {
|
|
462
|
+
expect(RefactorEngine.parseEntityFromPath('support/steps/loginUser')).toEqual({
|
|
463
|
+
type: 'step',
|
|
464
|
+
symbol: 'loginUser',
|
|
465
|
+
});
|
|
466
|
+
expect(RefactorEngine.parseEntityFromPath('support/steps/loginUser/')).toEqual({
|
|
467
|
+
type: 'step',
|
|
468
|
+
symbol: 'loginUser',
|
|
469
|
+
});
|
|
470
|
+
expect(RefactorEngine.parseEntityFromPath('steps/loginUser')).toEqual({
|
|
471
|
+
type: 'step',
|
|
472
|
+
symbol: 'loginUser',
|
|
473
|
+
});
|
|
474
|
+
expect(RefactorEngine.parseEntityFromPath('support/steps/loginUser/step.md')).toEqual({
|
|
475
|
+
type: 'step',
|
|
476
|
+
symbol: 'loginUser',
|
|
477
|
+
});
|
|
478
|
+
expect(RefactorEngine.parseEntityFromPath('support/steps/loginUser/web.ts')).toEqual({
|
|
479
|
+
type: 'step',
|
|
480
|
+
symbol: 'loginUser',
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
it('parses action paths across support/actions and actions', () => {
|
|
484
|
+
expect(RefactorEngine.parseEntityFromPath('support/actions/customSwipe')).toEqual({
|
|
485
|
+
type: 'action',
|
|
486
|
+
symbol: 'customSwipe',
|
|
487
|
+
});
|
|
488
|
+
expect(RefactorEngine.parseEntityFromPath('actions/customSwipe')).toEqual({
|
|
489
|
+
type: 'action',
|
|
490
|
+
symbol: 'customSwipe',
|
|
491
|
+
});
|
|
492
|
+
expect(RefactorEngine.parseEntityFromPath('support/actions/customSwipe/action.ts')).toEqual({
|
|
493
|
+
type: 'action',
|
|
494
|
+
symbol: 'customSwipe',
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
it('parses fixture paths across extensions and nested subdirectories', () => {
|
|
498
|
+
expect(RefactorEngine.parseEntityFromPath('fixtures/users.json')).toEqual({
|
|
499
|
+
type: 'fixture',
|
|
500
|
+
symbol: 'users',
|
|
501
|
+
});
|
|
502
|
+
expect(RefactorEngine.parseEntityFromPath('fixtures/users.csv')).toEqual({
|
|
503
|
+
type: 'fixture',
|
|
504
|
+
symbol: 'users',
|
|
505
|
+
});
|
|
506
|
+
expect(RefactorEngine.parseEntityFromPath('fixtures/auth/tokens.yaml')).toEqual({
|
|
507
|
+
type: 'fixture',
|
|
508
|
+
symbol: 'tokens',
|
|
509
|
+
});
|
|
510
|
+
expect(RefactorEngine.parseEntityFromPath('fixtures/users')).toEqual({
|
|
511
|
+
type: 'fixture',
|
|
512
|
+
symbol: 'users',
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
it('returns null for non-entity paths or root directory paths without entity symbol', () => {
|
|
516
|
+
expect(RefactorEngine.parseEntityFromPath('')).toBeNull();
|
|
517
|
+
expect(RefactorEngine.parseEntityFromPath('src/utils/helpers.ts')).toBeNull();
|
|
518
|
+
expect(RefactorEngine.parseEntityFromPath('page-objects')).toBeNull();
|
|
519
|
+
expect(RefactorEngine.parseEntityFromPath('page-objects/')).toBeNull();
|
|
520
|
+
expect(RefactorEngine.parseEntityFromPath('fixtures/')).toBeNull();
|
|
521
|
+
expect(RefactorEngine.parseEntityFromPath('support/steps')).toBeNull();
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
describe('renameFromPaths path-based refactoring', () => {
|
|
525
|
+
it('successfully refactors page-object using paths', async () => {
|
|
526
|
+
const poDir = path.join(tmpDir, 'page-objects', 'LoginPage');
|
|
527
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
528
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'export default class LoginPage {}');
|
|
529
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
530
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
531
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
532
|
+
fs.writeFileSync(specFile, 'LoginPage.submit();');
|
|
533
|
+
const result = await RefactorEngine.renameFromPaths({
|
|
534
|
+
workspaceRoot: tmpDir,
|
|
535
|
+
fromPath: 'page-objects/LoginPage',
|
|
536
|
+
toPath: 'page-objects/AuthPage',
|
|
537
|
+
});
|
|
538
|
+
expect(result.status).toBe('success');
|
|
539
|
+
expect(result.entityType).toBe('page-object');
|
|
540
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'LoginPage'))).toBe(false);
|
|
541
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'AuthPage'))).toBe(true);
|
|
542
|
+
expect(fs.readFileSync(specFile, 'utf-8')).toContain('AuthPage.submit();');
|
|
543
|
+
});
|
|
544
|
+
it('successfully previews changes with dry-run mode using paths', async () => {
|
|
545
|
+
const poDir = path.join(tmpDir, 'page-objects', 'LoginPage');
|
|
546
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
547
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'export default class LoginPage {}');
|
|
548
|
+
const result = await RefactorEngine.renameFromPaths({
|
|
549
|
+
workspaceRoot: tmpDir,
|
|
550
|
+
fromPath: 'page-objects/LoginPage',
|
|
551
|
+
toPath: 'page-objects/AuthPage',
|
|
552
|
+
dryRun: true,
|
|
553
|
+
});
|
|
554
|
+
expect(result.status).toBe('success');
|
|
555
|
+
// Disk state must NOT be modified in dry-run
|
|
556
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'LoginPage'))).toBe(true);
|
|
557
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'AuthPage'))).toBe(false);
|
|
558
|
+
});
|
|
559
|
+
it('rejects cross-entity refactoring with type mismatch error', async () => {
|
|
560
|
+
const result = await RefactorEngine.renameFromPaths({
|
|
561
|
+
workspaceRoot: tmpDir,
|
|
562
|
+
fromPath: 'page-objects/LoginPage',
|
|
563
|
+
toPath: 'support/steps/loginUser',
|
|
564
|
+
});
|
|
565
|
+
expect(result.status).toBe('failed');
|
|
566
|
+
expect(result.error).toContain('Entity type mismatch');
|
|
567
|
+
});
|
|
568
|
+
it('rejects unparseable source or target paths', async () => {
|
|
569
|
+
const res1 = await RefactorEngine.renameFromPaths({
|
|
570
|
+
workspaceRoot: tmpDir,
|
|
571
|
+
fromPath: 'src/unknown/something',
|
|
572
|
+
toPath: 'page-objects/AuthPage',
|
|
573
|
+
});
|
|
574
|
+
expect(res1.status).toBe('failed');
|
|
575
|
+
expect(res1.error).toContain('Unable to determine entity type from source path');
|
|
576
|
+
const res2 = await RefactorEngine.renameFromPaths({
|
|
577
|
+
workspaceRoot: tmpDir,
|
|
578
|
+
fromPath: 'page-objects/LoginPage',
|
|
579
|
+
toPath: 'src/unknown/something',
|
|
580
|
+
});
|
|
581
|
+
expect(res2.status).toBe('failed');
|
|
582
|
+
expect(res2.error).toContain('Unable to determine entity type from target path');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
describe('Edge-Cases & Robustness Guards (Step 2.4)', () => {
|
|
586
|
+
it('rejects refactoring if target step directory already exists in support/steps or steps', async () => {
|
|
587
|
+
const stepDir1 = path.join(tmpDir, 'support', 'steps', 'loginUser');
|
|
588
|
+
const stepDir2 = path.join(tmpDir, 'support', 'steps', 'loginAdmin');
|
|
589
|
+
fs.mkdirSync(stepDir1, { recursive: true });
|
|
590
|
+
fs.mkdirSync(stepDir2, { recursive: true });
|
|
591
|
+
const res = await RefactorEngine.renameSymbol({
|
|
592
|
+
workspaceRoot: tmpDir,
|
|
593
|
+
type: 'step',
|
|
594
|
+
oldName: 'loginUser',
|
|
595
|
+
newName: 'loginAdmin',
|
|
596
|
+
});
|
|
597
|
+
expect(res.status).toBe('failed');
|
|
598
|
+
expect(res.error).toContain('already exists');
|
|
599
|
+
// Also test root steps directory collision
|
|
600
|
+
const rootStep1 = path.join(tmpDir, 'steps', 'swipeLeft');
|
|
601
|
+
const rootStep2 = path.join(tmpDir, 'steps', 'swipeRight');
|
|
602
|
+
fs.mkdirSync(rootStep1, { recursive: true });
|
|
603
|
+
fs.mkdirSync(rootStep2, { recursive: true });
|
|
604
|
+
const res2 = await RefactorEngine.renameSymbol({
|
|
605
|
+
workspaceRoot: tmpDir,
|
|
606
|
+
type: 'step',
|
|
607
|
+
oldName: 'swipeLeft',
|
|
608
|
+
newName: 'swipeRight',
|
|
609
|
+
});
|
|
610
|
+
expect(res2.status).toBe('failed');
|
|
611
|
+
expect(res2.error).toContain('already exists');
|
|
612
|
+
});
|
|
613
|
+
it('rejects refactoring if target action directory already exists in support/actions or actions', async () => {
|
|
614
|
+
const actionDir1 = path.join(tmpDir, 'support', 'actions', 'customClick');
|
|
615
|
+
const actionDir2 = path.join(tmpDir, 'support', 'actions', 'tapElement');
|
|
616
|
+
fs.mkdirSync(actionDir1, { recursive: true });
|
|
617
|
+
fs.mkdirSync(actionDir2, { recursive: true });
|
|
618
|
+
const res = await RefactorEngine.renameSymbol({
|
|
619
|
+
workspaceRoot: tmpDir,
|
|
620
|
+
type: 'action',
|
|
621
|
+
oldName: 'customClick',
|
|
622
|
+
newName: 'tapElement',
|
|
623
|
+
});
|
|
624
|
+
expect(res.status).toBe('failed');
|
|
625
|
+
expect(res.error).toContain('already exists');
|
|
626
|
+
// Also test root actions directory collision
|
|
627
|
+
const rootAction1 = path.join(tmpDir, 'actions', 'pressKey');
|
|
628
|
+
const rootAction2 = path.join(tmpDir, 'actions', 'releaseKey');
|
|
629
|
+
fs.mkdirSync(rootAction1, { recursive: true });
|
|
630
|
+
fs.mkdirSync(rootAction2, { recursive: true });
|
|
631
|
+
const res2 = await RefactorEngine.renameSymbol({
|
|
632
|
+
workspaceRoot: tmpDir,
|
|
633
|
+
type: 'action',
|
|
634
|
+
oldName: 'pressKey',
|
|
635
|
+
newName: 'releaseKey',
|
|
636
|
+
});
|
|
637
|
+
expect(res2.status).toBe('failed');
|
|
638
|
+
expect(res2.error).toContain('already exists');
|
|
639
|
+
});
|
|
640
|
+
it('rejects refactoring if target fixture exists with a different extension (cross-format collision)', async () => {
|
|
641
|
+
const fixturesDir = path.join(tmpDir, 'fixtures');
|
|
642
|
+
fs.mkdirSync(fixturesDir, { recursive: true });
|
|
643
|
+
fs.writeFileSync(path.join(fixturesDir, 'users.json'), '{}');
|
|
644
|
+
// Target accounts already exists as accounts.csv!
|
|
645
|
+
fs.writeFileSync(path.join(fixturesDir, 'accounts.csv'), 'id,name');
|
|
646
|
+
const res = await RefactorEngine.renameSymbol({
|
|
647
|
+
workspaceRoot: tmpDir,
|
|
648
|
+
type: 'fixture',
|
|
649
|
+
oldName: 'users',
|
|
650
|
+
newName: 'accounts',
|
|
651
|
+
});
|
|
652
|
+
expect(res.status).toBe('failed');
|
|
653
|
+
expect(res.error).toContain('already exists');
|
|
654
|
+
});
|
|
655
|
+
it('rejects invalid identifier formats across all entity types', async () => {
|
|
656
|
+
// 1. Page Object: Must be PascalCase
|
|
657
|
+
const invalidPoNames = ['loginPage', 'login-page', '1LoginPage', 'Login Page', 'Login@Page', '_LoginPage'];
|
|
658
|
+
for (const name of invalidPoNames) {
|
|
659
|
+
const res = await RefactorEngine.renameSymbol({
|
|
660
|
+
workspaceRoot: tmpDir,
|
|
661
|
+
type: 'page-object',
|
|
662
|
+
oldName: 'LoginPage',
|
|
663
|
+
newName: name,
|
|
664
|
+
});
|
|
665
|
+
expect(res.status).toBe('failed');
|
|
666
|
+
expect(res.error).toContain('PascalCase');
|
|
667
|
+
}
|
|
668
|
+
// 2. Step: Must be camelCase
|
|
669
|
+
const invalidStepNames = ['LoginUser', 'login_user', 'login-user', '1login', 'login User', 'login$user'];
|
|
670
|
+
for (const name of invalidStepNames) {
|
|
671
|
+
const res = await RefactorEngine.renameSymbol({
|
|
672
|
+
workspaceRoot: tmpDir,
|
|
673
|
+
type: 'step',
|
|
674
|
+
oldName: 'loginUser',
|
|
675
|
+
newName: name,
|
|
676
|
+
});
|
|
677
|
+
expect(res.status).toBe('failed');
|
|
678
|
+
expect(res.error).toContain('camelCase');
|
|
679
|
+
}
|
|
680
|
+
// 3. Action: Must be camelCase
|
|
681
|
+
const invalidActionNames = ['SwipeUp', 'swipe_up', 'swipe-up', '1swipe', 'swipe!'];
|
|
682
|
+
for (const name of invalidActionNames) {
|
|
683
|
+
const res = await RefactorEngine.renameSymbol({
|
|
684
|
+
workspaceRoot: tmpDir,
|
|
685
|
+
type: 'action',
|
|
686
|
+
oldName: 'swipeUp',
|
|
687
|
+
newName: name,
|
|
688
|
+
});
|
|
689
|
+
expect(res.status).toBe('failed');
|
|
690
|
+
expect(res.error).toContain('camelCase');
|
|
691
|
+
}
|
|
692
|
+
// 4. Fixture: Must be camelCase or snake_case starting with lowercase
|
|
693
|
+
const invalidFixtureNames = ['Users', 'UserAccounts', 'user-data', '1users', 'users.data', 'users data'];
|
|
694
|
+
for (const name of invalidFixtureNames) {
|
|
695
|
+
const res = await RefactorEngine.renameSymbol({
|
|
696
|
+
workspaceRoot: tmpDir,
|
|
697
|
+
type: 'fixture',
|
|
698
|
+
oldName: 'users',
|
|
699
|
+
newName: name,
|
|
700
|
+
});
|
|
701
|
+
expect(res.status).toBe('failed');
|
|
702
|
+
expect(res.error).toContain('camelCase or snake_case');
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
it('rejects identical oldName and newName or empty names', async () => {
|
|
706
|
+
const resIdentical = await RefactorEngine.renameSymbol({
|
|
707
|
+
workspaceRoot: tmpDir,
|
|
708
|
+
type: 'page-object',
|
|
709
|
+
oldName: 'LoginPage',
|
|
710
|
+
newName: 'LoginPage',
|
|
711
|
+
});
|
|
712
|
+
expect(resIdentical.status).toBe('failed');
|
|
713
|
+
expect(resIdentical.error).toContain('Invalid or identical');
|
|
714
|
+
const resEmpty = await RefactorEngine.renameSymbol({
|
|
715
|
+
workspaceRoot: tmpDir,
|
|
716
|
+
type: 'page-object',
|
|
717
|
+
oldName: '',
|
|
718
|
+
newName: 'AuthPage',
|
|
719
|
+
});
|
|
720
|
+
expect(resEmpty.status).toBe('failed');
|
|
721
|
+
expect(resEmpty.error).toContain('Invalid or identical');
|
|
722
|
+
});
|
|
723
|
+
it('supports fixtures located in support/fixtures directory and preserves nested subdirectories', async () => {
|
|
724
|
+
const supportFixDir = path.join(tmpDir, 'support', 'fixtures', 'billing');
|
|
725
|
+
fs.mkdirSync(supportFixDir, { recursive: true });
|
|
726
|
+
fs.writeFileSync(path.join(supportFixDir, 'invoices.json'), '{"id": 1}');
|
|
727
|
+
const specDir = path.join(tmpDir, 'specs', 'Billing');
|
|
728
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
729
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
730
|
+
fs.writeFileSync(specFile, 'const inv = Fixture.invoices;\nconst file = "support/fixtures/billing/invoices.json";');
|
|
731
|
+
const res = await RefactorEngine.renameSymbol({
|
|
732
|
+
workspaceRoot: tmpDir,
|
|
733
|
+
type: 'fixture',
|
|
734
|
+
oldName: 'invoices',
|
|
735
|
+
newName: 'receipts',
|
|
736
|
+
});
|
|
737
|
+
expect(res.status).toBe('success');
|
|
738
|
+
expect(fs.existsSync(path.join(supportFixDir, 'invoices.json'))).toBe(false);
|
|
739
|
+
expect(fs.existsSync(path.join(supportFixDir, 'receipts.json'))).toBe(true);
|
|
740
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
741
|
+
expect(updatedSpec).toContain('Fixture.receipts');
|
|
742
|
+
expect(updatedSpec).toContain('"support/fixtures/billing/receipts.json"');
|
|
743
|
+
expect(updatedSpec).not.toContain('Fixture.invoices');
|
|
744
|
+
});
|
|
745
|
+
it('handles refactoring when old entity directory does not exist on disk gracefully', async () => {
|
|
746
|
+
// If the old page-object directory does not exist on disk,
|
|
747
|
+
// it should still update references in spec files if any exist, without crashing
|
|
748
|
+
const specDir = path.join(tmpDir, 'specs', 'Orphan');
|
|
749
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
750
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
751
|
+
fs.writeFileSync(specFile, 'await GhostPage.click();');
|
|
752
|
+
const res = await RefactorEngine.renameSymbol({
|
|
753
|
+
workspaceRoot: tmpDir,
|
|
754
|
+
type: 'page-object',
|
|
755
|
+
oldName: 'GhostPage',
|
|
756
|
+
newName: 'ActivePage',
|
|
757
|
+
});
|
|
758
|
+
expect(res.status).toBe('success');
|
|
759
|
+
expect(fs.readFileSync(specFile, 'utf-8')).toContain('await ActivePage.click();');
|
|
760
|
+
});
|
|
761
|
+
it('propagates refactoring across multi-package Nx monorepo structure (packages/**/specs/**)', async () => {
|
|
762
|
+
// 1. Setup Nx monorepo indicator and root shared assets
|
|
763
|
+
fs.writeFileSync(path.join(tmpDir, 'nx.json'), JSON.stringify({}));
|
|
764
|
+
fs.writeFileSync(path.join(tmpDir, 'pnpm-workspace.yaml'), 'packages:\n - "packages/*"\n');
|
|
765
|
+
const rootPoDir = path.join(tmpDir, 'page-objects', 'AuthPage');
|
|
766
|
+
fs.mkdirSync(rootPoDir, { recursive: true });
|
|
767
|
+
fs.writeFileSync(path.join(rootPoDir, 'web.ts'), 'export default class AuthPage {}');
|
|
768
|
+
const rootFixDir = path.join(tmpDir, 'fixtures');
|
|
769
|
+
fs.mkdirSync(rootFixDir, { recursive: true });
|
|
770
|
+
fs.writeFileSync(path.join(rootFixDir, 'users.json'), '{"role": "admin"}');
|
|
771
|
+
// 2. Setup Sub-package 1: packages/portal-e2e
|
|
772
|
+
const portalSpecDir = path.join(tmpDir, 'packages', 'portal-e2e', 'specs');
|
|
773
|
+
fs.mkdirSync(portalSpecDir, { recursive: true });
|
|
774
|
+
const portalSpecFile = path.join(portalSpecDir, 'login.web.test.ts');
|
|
775
|
+
fs.writeFileSync(portalSpecFile, 'it("logs into portal", async () => {\n await AuthPage.login();\n const u = Fixture.users;\n});');
|
|
776
|
+
// 3. Setup Sub-package 2: packages/admin-e2e
|
|
777
|
+
const adminSpecDir = path.join(tmpDir, 'packages', 'admin-e2e', 'specs');
|
|
778
|
+
fs.mkdirSync(adminSpecDir, { recursive: true });
|
|
779
|
+
const adminSpecFile = path.join(adminSpecDir, 'dashboard.web.test.ts');
|
|
780
|
+
fs.writeFileSync(adminSpecFile, 'it("verifies admin dashboard", async () => {\n await AuthPage.login();\n const u = Fixture.users;\n});');
|
|
781
|
+
// 4. Refactor root Page Object: AuthPage -> IdentityPage
|
|
782
|
+
const poRes = await RefactorEngine.renameSymbol({
|
|
783
|
+
workspaceRoot: tmpDir,
|
|
784
|
+
type: 'page-object',
|
|
785
|
+
oldName: 'AuthPage',
|
|
786
|
+
newName: 'IdentityPage',
|
|
787
|
+
});
|
|
788
|
+
expect(poRes.status).toBe('success');
|
|
789
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'AuthPage'))).toBe(false);
|
|
790
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'IdentityPage'))).toBe(true);
|
|
791
|
+
// Verify propagation into both sub-packages in packages/**
|
|
792
|
+
expect(fs.readFileSync(portalSpecFile, 'utf-8')).toContain('IdentityPage.login()');
|
|
793
|
+
expect(fs.readFileSync(portalSpecFile, 'utf-8')).not.toContain('AuthPage.login()');
|
|
794
|
+
expect(fs.readFileSync(adminSpecFile, 'utf-8')).toContain('IdentityPage.login()');
|
|
795
|
+
expect(fs.readFileSync(adminSpecFile, 'utf-8')).not.toContain('AuthPage.login()');
|
|
796
|
+
// 5. Refactor root Fixture: users -> user_accounts
|
|
797
|
+
const fixRes = await RefactorEngine.renameSymbol({
|
|
798
|
+
workspaceRoot: tmpDir,
|
|
799
|
+
type: 'fixture',
|
|
800
|
+
oldName: 'users',
|
|
801
|
+
newName: 'user_accounts',
|
|
802
|
+
});
|
|
803
|
+
expect(fixRes.status).toBe('success');
|
|
804
|
+
expect(fs.existsSync(path.join(rootFixDir, 'users.json'))).toBe(false);
|
|
805
|
+
expect(fs.existsSync(path.join(rootFixDir, 'user_accounts.json'))).toBe(true);
|
|
806
|
+
expect(fs.readFileSync(portalSpecFile, 'utf-8')).toContain('Fixture.user_accounts');
|
|
807
|
+
expect(fs.readFileSync(portalSpecFile, 'utf-8')).not.toContain('Fixture.users');
|
|
808
|
+
expect(fs.readFileSync(adminSpecFile, 'utf-8')).toContain('Fixture.user_accounts');
|
|
809
|
+
expect(fs.readFileSync(adminSpecFile, 'utf-8')).not.toContain('Fixture.users');
|
|
810
|
+
});
|
|
811
|
+
it('supports scoped refactoring within a specific Nx sub-package via workspaceRoot / cwd', async () => {
|
|
812
|
+
const mobilePkgDir = path.join(tmpDir, 'packages', 'mobile-e2e');
|
|
813
|
+
const mobilePoDir = path.join(mobilePkgDir, 'page-objects', 'WelcomePage');
|
|
814
|
+
fs.mkdirSync(mobilePoDir, { recursive: true });
|
|
815
|
+
fs.writeFileSync(path.join(mobilePoDir, 'mobile.ts'), 'export default class WelcomePage {}');
|
|
816
|
+
const mobileSpecDir = path.join(mobilePkgDir, 'specs');
|
|
817
|
+
fs.mkdirSync(mobileSpecDir, { recursive: true });
|
|
818
|
+
const mobileSpecFile = path.join(mobileSpecDir, 'welcome.mobile.test.ts');
|
|
819
|
+
fs.writeFileSync(mobileSpecFile, 'await WelcomePage.start();');
|
|
820
|
+
const webPkgDir = path.join(tmpDir, 'packages', 'web-e2e', 'specs');
|
|
821
|
+
fs.mkdirSync(webPkgDir, { recursive: true });
|
|
822
|
+
const webSpecFile = path.join(webPkgDir, 'other.web.test.ts');
|
|
823
|
+
fs.writeFileSync(webSpecFile, 'console.log("unrelated");');
|
|
824
|
+
// Execute refactoring scoped strictly to packages/mobile-e2e
|
|
825
|
+
const res = await RefactorEngine.renameSymbol({
|
|
826
|
+
workspaceRoot: mobilePkgDir,
|
|
827
|
+
type: 'page-object',
|
|
828
|
+
oldName: 'WelcomePage',
|
|
829
|
+
newName: 'OnboardingPage',
|
|
830
|
+
});
|
|
831
|
+
expect(res.status).toBe('success');
|
|
832
|
+
expect(fs.existsSync(path.join(mobilePkgDir, 'page-objects', 'WelcomePage'))).toBe(false);
|
|
833
|
+
expect(fs.existsSync(path.join(mobilePkgDir, 'page-objects', 'OnboardingPage'))).toBe(true);
|
|
834
|
+
expect(fs.readFileSync(mobileSpecFile, 'utf-8')).toContain('OnboardingPage.start();');
|
|
835
|
+
expect(fs.readFileSync(webSpecFile, 'utf-8')).toBe('console.log("unrelated");');
|
|
836
|
+
});
|
|
837
|
+
it('supports skipPathRename when directory or file was already renamed on disk by external watcher / IDE explorer', async () => {
|
|
838
|
+
const poDir = path.join(tmpDir, 'page-objects', 'LoginPage');
|
|
839
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
840
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'export default class LoginPage {}');
|
|
841
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
842
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
843
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
844
|
+
fs.writeFileSync(specFile, 'LoginPage.submit();');
|
|
845
|
+
// Simulate VS Code Explorer already renaming the folder on disk
|
|
846
|
+
const targetPoDir = path.join(tmpDir, 'page-objects', 'AuthPage');
|
|
847
|
+
fs.renameSync(poDir, targetPoDir);
|
|
848
|
+
// Dry-run check with skipPathRename: true
|
|
849
|
+
const dryResult = await RefactorEngine.renameFromPaths({
|
|
850
|
+
workspaceRoot: tmpDir,
|
|
851
|
+
fromPath: 'page-objects/LoginPage',
|
|
852
|
+
toPath: 'page-objects/AuthPage',
|
|
853
|
+
dryRun: true,
|
|
854
|
+
skipPathRename: true,
|
|
855
|
+
});
|
|
856
|
+
expect(dryResult.status).toBe('success');
|
|
857
|
+
expect(dryResult.occurrencesReplaced).toBe(2);
|
|
858
|
+
expect(dryResult.modifiedFiles.length).toBe(2);
|
|
859
|
+
// Execution with skipPathRename: true
|
|
860
|
+
const realResult = await RefactorEngine.renameFromPaths({
|
|
861
|
+
workspaceRoot: tmpDir,
|
|
862
|
+
fromPath: 'page-objects/LoginPage',
|
|
863
|
+
toPath: 'page-objects/AuthPage',
|
|
864
|
+
dryRun: false,
|
|
865
|
+
skipPathRename: true,
|
|
866
|
+
});
|
|
867
|
+
expect(realResult.status).toBe('success');
|
|
868
|
+
expect(realResult.occurrencesReplaced).toBe(2);
|
|
869
|
+
expect(fs.readFileSync(path.join(targetPoDir, 'web.ts'), 'utf-8')).toContain('class AuthPage');
|
|
870
|
+
expect(fs.readFileSync(specFile, 'utf-8')).toContain('AuthPage.submit();');
|
|
871
|
+
});
|
|
872
|
+
});
|
|
175
873
|
});
|