@testspectra/cli 1.0.70 → 1.1.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/LICENSE.md +48 -0
- package/README.md +36 -270
- package/bin/.testspectra-runner.hash +1 -0
- package/bin/spectra.js +1 -1
- package/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-DPfv5_EF.js +116 -0
- package/demo-app/assets/index-gIwu2EMV.css +1 -0
- package/demo-app/index.html +2 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
- package/dist/commands/__tests__/cloud-run.test.js +93 -0
- package/dist/commands/__tests__/doctor.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor.test.js +39 -0
- package/dist/commands/__tests__/init.test.d.ts +1 -0
- package/dist/commands/__tests__/init.test.js +148 -0
- package/dist/commands/__tests__/license.test.d.ts +1 -0
- package/dist/commands/__tests__/license.test.js +13 -0
- package/dist/commands/__tests__/refactor.test.d.ts +1 -0
- package/dist/commands/__tests__/refactor.test.js +64 -0
- package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
- package/dist/commands/__tests__/run-e2e.test.js +181 -0
- package/dist/commands/__tests__/run.test.d.ts +1 -0
- package/dist/commands/__tests__/run.test.js +197 -0
- package/dist/commands/__tests__/watch.test.d.ts +1 -0
- package/dist/commands/__tests__/watch.test.js +155 -0
- package/dist/commands/add.js +86 -85
- package/dist/commands/cloud-run.d.ts +11 -0
- package/dist/commands/cloud-run.js +118 -0
- package/dist/commands/demo.js +7 -7
- package/dist/commands/devices.d.ts +14 -2
- package/dist/commands/devices.js +44 -34
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.js +43 -264
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +206 -196
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +59 -0
- package/dist/commands/license.d.ts +4 -0
- package/dist/commands/license.js +30 -0
- package/dist/commands/refactor.d.ts +8 -0
- package/dist/commands/refactor.js +78 -0
- package/dist/commands/run.d.ts +22 -1
- package/dist/commands/run.js +252 -109
- package/dist/commands/sync-types.d.ts +4 -0
- package/dist/commands/sync-types.js +14 -0
- package/dist/commands/update.js +39 -43
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +47 -43
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +40 -25
- package/dist/config/schema.d.ts +117 -49
- package/dist/config/schema.js +41 -34
- package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
- package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
- package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
- package/dist/generator/__tests__/type-generator.test.js +386 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/tsconfig-generator.d.ts +27 -0
- package/dist/generator/tsconfig-generator.js +391 -0
- package/dist/generator/type-generator.d.ts +36 -0
- package/dist/generator/type-generator.js +644 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +124 -51
- package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
- package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
- package/dist/lifecycle/hook-discovery.d.ts +24 -0
- package/dist/lifecycle/hook-discovery.js +60 -0
- package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
- package/dist/lifecycle/hook-dispatcher.js +190 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/parallel-grouping.d.ts +27 -0
- package/dist/lifecycle/parallel-grouping.js +151 -0
- package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
- package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
- package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
- package/dist/mobile/__tests__/device-pool.test.js +97 -0
- package/dist/mobile/device-pool.d.ts +23 -0
- package/dist/mobile/device-pool.js +1 -0
- package/dist/mobile/index.d.ts +1 -0
- package/dist/mobile/index.js +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.js +175 -0
- package/dist/refactor/index.d.ts +1 -0
- package/dist/refactor/index.js +1 -0
- package/dist/refactor/refactor-engine.d.ts +25 -0
- package/dist/refactor/refactor-engine.js +255 -0
- package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
- package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
- package/dist/reporter/semantic-formatter.d.ts +6 -0
- package/dist/reporter/semantic-formatter.js +191 -0
- package/dist/reporter/summary-view.d.ts +12 -0
- package/dist/reporter/summary-view.js +19 -0
- package/dist/reporter/terminal-stream.d.ts +32 -0
- package/dist/reporter/terminal-stream.js +82 -0
- package/dist/reporter/types.d.ts +2 -0
- package/dist/reporter/types.js +1 -0
- package/dist/runner/bridge.d.ts +10 -5
- package/dist/runner/bridge.js +69 -44
- package/dist/runner/reporter.d.ts +30 -2
- package/dist/runner/reporter.js +419 -13
- package/dist/server/demo.js +43 -44
- package/dist/types/generator.d.ts +1 -17
- package/dist/types/generator.js +1 -443
- package/dist/utils/demo-server.js +38 -35
- package/package.json +20 -12
- package/templates/default/CLAUDE.md +7 -5
- package/templates/default/fixtures/apiData.json +24 -0
- package/templates/default/package.json +13 -4
- package/templates/default/page-objects/MatchersPage/mobile.ts +41 -16
- package/templates/default/page-objects/MatchersPage/web.ts +14 -0
- package/templates/default/page-objects/NavigationPage/mobile.ts +10 -1
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +27 -17
- package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +11 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +16 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
- package/templates/default/specs/ApiInterception/suite.md +9 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +15 -1
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
- package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
- package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
- package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
- package/templates/default/spectra.config.ts +21 -14
- package/templates/default/support/actions/fillCredentials/mobile.action.ts +2 -2
- package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +2 -0
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +2 -2
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +3 -1
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +3 -3
- package/templates/default/tsconfig.json +3 -1
- package/templates/nx/.nx/workspace-data/d/daemon.log +168 -0
- package/templates/nx/CLAUDE.md +2 -2
- package/templates/nx/package.json +7 -5
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +3 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +3 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
- package/templates/nx/pnpm-lock.yaml +6663 -0
- package/templates/nx/pnpm-workspace.yaml +2 -1
- package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +12 -0
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +14 -0
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
- package/templates/nx/shared/testing/project.json +19 -0
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +2 -2
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +3 -3
- package/templates/nx/spectra.config.ts +19 -12
- package/templates/nx/tsconfig.json +3 -1
- package/demo-app/assets/index-DAUlrHTr.css +0 -1
- package/demo-app/assets/index-DN2Kb0A8.js +0 -100
- package/dist/index.cjs +0 -101
- package/dist/plugin.cjs +0 -575
- package/dist/plugin.d.ts +0 -16
- package/dist/plugin.js +0 -184
- package/dist/step/index.d.ts +0 -6
- package/dist/step/index.js +0 -6
- package/dist/step/matchers.d.ts +0 -14
- package/dist/step/matchers.js +0 -114
- package/dist/step/proto.d.ts +0 -46
- package/dist/step/proto.js +0 -114
- package/dist/step/runner/collection.d.ts +0 -13
- package/dist/step/runner/collection.js +0 -51
- package/dist/step/runner/single.d.ts +0 -21
- package/dist/step/runner/single.js +0 -138
- package/dist/step/spectra.d.ts +0 -34
- package/dist/step/spectra.js +0 -156
- package/dist/step/types.d.ts +0 -32
- package/dist/step/types.js +0 -5
- package/templates/default/tsconfig.spectra.android.json +0 -46
- package/templates/default/tsconfig.spectra.ios.json +0 -46
- package/templates/default/tsconfig.spectra.json +0 -22
- package/templates/default/tsconfig.spectra.web.json +0 -53
- package/templates/nx/fixtures/playgroundData.json +0 -7
- package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
- package/templates/nx/global-hooks/after/web.hook.ts +0 -3
- package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
- package/templates/nx/global-hooks/before/web.hook.ts +0 -3
- package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
- package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
- package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
- package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
- package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
- package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
- package/templates/nx/specs/BrowserContext/suite.md +0 -9
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
- package/templates/nx/specs/ContentMatchers/suite.md +0 -9
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
- package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/suite.md +0 -9
- package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
- package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
- package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
- package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/suite.md +0 -9
- package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
- package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
- package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
- package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
- package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
- package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
- package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
- package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
- package/templates/nx/tsconfig.spectra.android.json +0 -39
- package/templates/nx/tsconfig.spectra.ios.json +0 -39
- package/templates/nx/tsconfig.spectra.json +0 -25
- package/templates/nx/tsconfig.spectra.shared.json +0 -25
- package/templates/nx/tsconfig.spectra.web.json +0 -45
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { RefactorEngine } from '../refactor-engine.js';
|
|
6
|
+
describe('CLI Workspace-Wide Symbol Refactoring Engine (docs/v2/cli/synchronized-symbol-refactoring.md)', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-refactor-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
try {
|
|
13
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
});
|
|
17
|
+
it('renames page-object class declaration, directory, and test call sites', async () => {
|
|
18
|
+
// 1. Setup LoginPage directory and files
|
|
19
|
+
const poDir = path.join(tmpDir, 'page-objects', 'LoginPage');
|
|
20
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'export default class LoginPage {\n static username = "#user";\n}');
|
|
22
|
+
// 2. Setup spec referencing LoginPage
|
|
23
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
24
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
25
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
26
|
+
fs.writeFileSync(specFile, 'it("logs in", async () => {\n await LoginPage.username;\n});');
|
|
27
|
+
const result = await RefactorEngine.renameSymbol({
|
|
28
|
+
workspaceRoot: tmpDir,
|
|
29
|
+
type: 'page-object',
|
|
30
|
+
oldName: 'LoginPage',
|
|
31
|
+
newName: 'AuthPage',
|
|
32
|
+
});
|
|
33
|
+
expect(result.status).toBe('success');
|
|
34
|
+
expect(result.oldSymbol).toBe('LoginPage');
|
|
35
|
+
expect(result.newSymbol).toBe('AuthPage');
|
|
36
|
+
// Verify folder was renamed
|
|
37
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'LoginPage'))).toBe(false);
|
|
38
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'AuthPage', 'web.ts'))).toBe(true);
|
|
39
|
+
// Verify class renamed inside page-object
|
|
40
|
+
const updatedPo = fs.readFileSync(path.join(tmpDir, 'page-objects', 'AuthPage', 'web.ts'), 'utf-8');
|
|
41
|
+
expect(updatedPo).toContain('class AuthPage');
|
|
42
|
+
expect(updatedPo).not.toContain('class LoginPage');
|
|
43
|
+
// Verify spec updated
|
|
44
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
45
|
+
expect(updatedSpec).toContain('AuthPage.username');
|
|
46
|
+
expect(updatedSpec).not.toContain('LoginPage.username');
|
|
47
|
+
});
|
|
48
|
+
it('renames shared step folder, spec directives, and Step call sites', async () => {
|
|
49
|
+
const stepDir = path.join(tmpDir, 'support', 'steps', 'loginUser');
|
|
50
|
+
fs.mkdirSync(stepDir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(path.join(stepDir, 'web.step.ts'), 'export default async function loginUser() {}');
|
|
52
|
+
fs.writeFileSync(path.join(stepDir, 'step.md'), '# Step loginUser');
|
|
53
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
54
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
55
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
56
|
+
fs.writeFileSync(specFile, 'await Step.loginUser();');
|
|
57
|
+
const specDoc = path.join(specDir, 'spec.md');
|
|
58
|
+
fs.writeFileSync(specDoc, 'Execute @step:loginUser before test.');
|
|
59
|
+
const result = await RefactorEngine.renameSymbol({
|
|
60
|
+
workspaceRoot: tmpDir,
|
|
61
|
+
type: 'step',
|
|
62
|
+
oldName: 'loginUser',
|
|
63
|
+
newName: 'loginAsAdmin',
|
|
64
|
+
});
|
|
65
|
+
expect(result.status).toBe('success');
|
|
66
|
+
expect(fs.existsSync(path.join(tmpDir, 'support', 'steps', 'loginUser'))).toBe(false);
|
|
67
|
+
expect(fs.existsSync(path.join(tmpDir, 'support', 'steps', 'loginAsAdmin'))).toBe(true);
|
|
68
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
69
|
+
expect(updatedSpec).toContain('Step.loginAsAdmin()');
|
|
70
|
+
expect(updatedSpec).not.toContain('Step.loginUser()');
|
|
71
|
+
const updatedDoc = fs.readFileSync(specDoc, 'utf-8');
|
|
72
|
+
expect(updatedDoc).toContain('@step:loginAsAdmin');
|
|
73
|
+
expect(updatedDoc).not.toContain('@step:loginUser');
|
|
74
|
+
});
|
|
75
|
+
it('renames custom action and updates Spectra.<name>() calls', async () => {
|
|
76
|
+
const actionDir = path.join(tmpDir, 'support', 'actions', 'customSwipe');
|
|
77
|
+
fs.mkdirSync(actionDir, { recursive: true });
|
|
78
|
+
fs.writeFileSync(path.join(actionDir, 'mobile.action.ts'), 'export default async function customSwipe() {}');
|
|
79
|
+
const specDir = path.join(tmpDir, 'specs', 'Feed');
|
|
80
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
81
|
+
const specFile = path.join(specDir, 'mobile.test.ts');
|
|
82
|
+
fs.writeFileSync(specFile, 'await Spectra.customSwipe();');
|
|
83
|
+
const result = await RefactorEngine.renameSymbol({
|
|
84
|
+
workspaceRoot: tmpDir,
|
|
85
|
+
type: 'action',
|
|
86
|
+
oldName: 'customSwipe',
|
|
87
|
+
newName: 'swipeTop',
|
|
88
|
+
});
|
|
89
|
+
expect(result.status).toBe('success');
|
|
90
|
+
expect(fs.existsSync(path.join(tmpDir, 'support', 'actions', 'customSwipe'))).toBe(false);
|
|
91
|
+
expect(fs.existsSync(path.join(tmpDir, 'support', 'actions', 'swipeTop'))).toBe(true);
|
|
92
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
93
|
+
expect(updatedSpec).toContain('Spectra.swipeTop()');
|
|
94
|
+
expect(updatedSpec).not.toContain('Spectra.customSwipe()');
|
|
95
|
+
});
|
|
96
|
+
it('renames fixture file and propagates Fixture.<name> across call sites', async () => {
|
|
97
|
+
const fixturesDir = path.join(tmpDir, 'fixtures');
|
|
98
|
+
fs.mkdirSync(fixturesDir, { recursive: true });
|
|
99
|
+
fs.writeFileSync(path.join(fixturesDir, 'users.json'), JSON.stringify({ admin: true }));
|
|
100
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
101
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
102
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
103
|
+
fs.writeFileSync(specFile, 'const u = Fixture.users.admin;');
|
|
104
|
+
const result = await RefactorEngine.renameSymbol({
|
|
105
|
+
workspaceRoot: tmpDir,
|
|
106
|
+
type: 'fixture',
|
|
107
|
+
oldName: 'users',
|
|
108
|
+
newName: 'accounts',
|
|
109
|
+
});
|
|
110
|
+
expect(result.status).toBe('success');
|
|
111
|
+
expect(fs.existsSync(path.join(fixturesDir, 'users.json'))).toBe(false);
|
|
112
|
+
expect(fs.existsSync(path.join(fixturesDir, 'accounts.json'))).toBe(true);
|
|
113
|
+
const updatedSpec = fs.readFileSync(specFile, 'utf-8');
|
|
114
|
+
expect(updatedSpec).toContain('Fixture.accounts.admin');
|
|
115
|
+
expect(updatedSpec).not.toContain('Fixture.users.admin');
|
|
116
|
+
});
|
|
117
|
+
it('dry-run mode previews changes without modifying files or directory structures', async () => {
|
|
118
|
+
const poDir = path.join(tmpDir, 'page-objects', 'LoginPage');
|
|
119
|
+
fs.mkdirSync(poDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(poDir, 'web.ts'), 'class LoginPage {}');
|
|
121
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
122
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
123
|
+
const specFile = path.join(specDir, 'web.test.ts');
|
|
124
|
+
fs.writeFileSync(specFile, 'LoginPage.click();');
|
|
125
|
+
const result = await RefactorEngine.renameSymbol({
|
|
126
|
+
workspaceRoot: tmpDir,
|
|
127
|
+
type: 'page-object',
|
|
128
|
+
oldName: 'LoginPage',
|
|
129
|
+
newName: 'AuthPage',
|
|
130
|
+
dryRun: true,
|
|
131
|
+
});
|
|
132
|
+
expect(result.status).toBe('success');
|
|
133
|
+
expect(result.occurrencesReplaced).toBeGreaterThan(0);
|
|
134
|
+
// Ensure disk was NOT modified
|
|
135
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'LoginPage'))).toBe(true);
|
|
136
|
+
expect(fs.existsSync(path.join(tmpDir, 'page-objects', 'AuthPage'))).toBe(false);
|
|
137
|
+
expect(fs.readFileSync(specFile, 'utf-8')).toBe('LoginPage.click();');
|
|
138
|
+
});
|
|
139
|
+
it('atomic rollback guard restores original files if an error occurs midway', async () => {
|
|
140
|
+
const specDir = path.join(tmpDir, 'specs', 'Auth');
|
|
141
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
142
|
+
// File 1
|
|
143
|
+
const file1 = path.join(specDir, 'test1.ts');
|
|
144
|
+
fs.writeFileSync(file1, 'LoginPage.step1();');
|
|
145
|
+
// File 2
|
|
146
|
+
const file2 = path.join(specDir, 'test2.ts');
|
|
147
|
+
fs.writeFileSync(file2, 'LoginPage.step2();');
|
|
148
|
+
// Simulate an I/O write failure on file2 by spying on writeFileSync
|
|
149
|
+
let _writeCallCount = 0;
|
|
150
|
+
const realWriteFileSync = fs.writeFileSync;
|
|
151
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation((filePath, data, opts) => {
|
|
152
|
+
_writeCallCount++;
|
|
153
|
+
if (typeof filePath === 'string' && filePath.endsWith('test2.ts')) {
|
|
154
|
+
throw new Error('Disk write simulated failure');
|
|
155
|
+
}
|
|
156
|
+
return realWriteFileSync(filePath, data, opts);
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
const result = await RefactorEngine.renameSymbol({
|
|
160
|
+
workspaceRoot: tmpDir,
|
|
161
|
+
type: 'page-object',
|
|
162
|
+
oldName: 'LoginPage',
|
|
163
|
+
newName: 'AuthPage',
|
|
164
|
+
});
|
|
165
|
+
expect(result.status).toBe('failed');
|
|
166
|
+
expect(result.error).toContain('Atomic rollback executed');
|
|
167
|
+
// Verify file1 was restored to original content
|
|
168
|
+
const f1Content = fs.readFileSync(file1, 'utf-8');
|
|
169
|
+
expect(f1Content).toBe('LoginPage.step1();');
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
writeSpy.mockRestore();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './refactor-engine.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './refactor-engine.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type RefactorType = 'page-object' | 'step' | 'action' | 'fixture';
|
|
2
|
+
export interface RenameOptions {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
type: RefactorType;
|
|
5
|
+
oldName: string;
|
|
6
|
+
newName: string;
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RefactorResult {
|
|
10
|
+
status: 'success' | 'failed';
|
|
11
|
+
entityType: RefactorType;
|
|
12
|
+
oldSymbol: string;
|
|
13
|
+
newSymbol: string;
|
|
14
|
+
modifiedFiles: string[];
|
|
15
|
+
occurrencesReplaced: number;
|
|
16
|
+
renamedPaths?: Array<{
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
}>;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class RefactorEngine {
|
|
23
|
+
private static findFiles;
|
|
24
|
+
static renameSymbol(options: RenameOptions): Promise<RefactorResult>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { TypeGenerator } from '../generator/type-generator.js';
|
|
4
|
+
export class RefactorEngine {
|
|
5
|
+
static findFiles(dir, extensions) {
|
|
6
|
+
const results = [];
|
|
7
|
+
if (!fs.existsSync(dir))
|
|
8
|
+
return results;
|
|
9
|
+
function walk(currentDir) {
|
|
10
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
if (entry.name === 'node_modules' ||
|
|
13
|
+
entry.name === 'dist' ||
|
|
14
|
+
entry.name === '.git' ||
|
|
15
|
+
entry.name === '.testspectra') {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
walk(fullPath);
|
|
21
|
+
}
|
|
22
|
+
else if (entry.isFile()) {
|
|
23
|
+
if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
24
|
+
results.push(fullPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
walk(dir);
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
static async renameSymbol(options) {
|
|
33
|
+
const { workspaceRoot, type, oldName, newName, dryRun = false } = options;
|
|
34
|
+
if (!oldName || !newName || oldName === newName) {
|
|
35
|
+
return {
|
|
36
|
+
status: 'failed',
|
|
37
|
+
entityType: type,
|
|
38
|
+
oldSymbol: oldName,
|
|
39
|
+
newSymbol: newName,
|
|
40
|
+
modifiedFiles: [],
|
|
41
|
+
occurrencesReplaced: 0,
|
|
42
|
+
error: 'Invalid or identical old/new symbol names',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Phase 1: Determine file operations and replacement patterns
|
|
46
|
+
const plannedRenames = [];
|
|
47
|
+
const plannedEdits = [];
|
|
48
|
+
let oldSymbol = oldName;
|
|
49
|
+
let newSymbol = newName;
|
|
50
|
+
// 1. Target Directory / File Discovery
|
|
51
|
+
if (type === 'page-object') {
|
|
52
|
+
oldSymbol = oldName;
|
|
53
|
+
newSymbol = newName;
|
|
54
|
+
const poDir = path.join(workspaceRoot, 'page-objects', oldName);
|
|
55
|
+
if (fs.existsSync(poDir)) {
|
|
56
|
+
plannedRenames.push({ from: poDir, to: path.join(workspaceRoot, 'page-objects', newName) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (type === 'step') {
|
|
60
|
+
oldSymbol = `Step.${oldName}`;
|
|
61
|
+
newSymbol = `Step.${newName}`;
|
|
62
|
+
const stepCandidates = [
|
|
63
|
+
path.join(workspaceRoot, 'support', 'steps', oldName),
|
|
64
|
+
path.join(workspaceRoot, 'steps', oldName),
|
|
65
|
+
];
|
|
66
|
+
for (const cand of stepCandidates) {
|
|
67
|
+
if (fs.existsSync(cand)) {
|
|
68
|
+
const parent = path.dirname(cand);
|
|
69
|
+
plannedRenames.push({ from: cand, to: path.join(parent, newName) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (type === 'action') {
|
|
74
|
+
oldSymbol = `Spectra.${oldName}`;
|
|
75
|
+
newSymbol = `Spectra.${newName}`;
|
|
76
|
+
const actionCandidates = [
|
|
77
|
+
path.join(workspaceRoot, 'support', 'actions', oldName),
|
|
78
|
+
path.join(workspaceRoot, 'actions', oldName),
|
|
79
|
+
];
|
|
80
|
+
for (const cand of actionCandidates) {
|
|
81
|
+
if (fs.existsSync(cand)) {
|
|
82
|
+
const parent = path.dirname(cand);
|
|
83
|
+
plannedRenames.push({ from: cand, to: path.join(parent, newName) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (type === 'fixture') {
|
|
88
|
+
oldSymbol = `Fixture.${oldName}`;
|
|
89
|
+
newSymbol = `Fixture.${newName}`;
|
|
90
|
+
const fixturesDir = path.join(workspaceRoot, 'fixtures');
|
|
91
|
+
if (fs.existsSync(fixturesDir)) {
|
|
92
|
+
const entries = fs.readdirSync(fixturesDir);
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const parsed = path.parse(entry);
|
|
95
|
+
if (parsed.name === oldName) {
|
|
96
|
+
plannedRenames.push({
|
|
97
|
+
from: path.join(fixturesDir, entry),
|
|
98
|
+
to: path.join(fixturesDir, `${newName}${parsed.ext}`),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 2. Scan Codebases and AST replacements in memory
|
|
105
|
+
const searchDirs = [
|
|
106
|
+
path.join(workspaceRoot, 'specs'),
|
|
107
|
+
path.join(workspaceRoot, 'suites'),
|
|
108
|
+
path.join(workspaceRoot, 'page-objects'),
|
|
109
|
+
path.join(workspaceRoot, 'support'),
|
|
110
|
+
path.join(workspaceRoot, 'steps'),
|
|
111
|
+
path.join(workspaceRoot, 'actions'),
|
|
112
|
+
path.join(workspaceRoot, 'global-hooks'),
|
|
113
|
+
];
|
|
114
|
+
const allCodeFiles = searchDirs.flatMap((dir) => this.findFiles(dir, ['.ts', '.js', '.md']));
|
|
115
|
+
const uniqueFiles = Array.from(new Set(allCodeFiles));
|
|
116
|
+
let totalOccurrences = 0;
|
|
117
|
+
for (const filePath of uniqueFiles) {
|
|
118
|
+
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
119
|
+
let modified = originalContent;
|
|
120
|
+
let count = 0;
|
|
121
|
+
if (type === 'page-object') {
|
|
122
|
+
// Replace `class OldName` -> `class NewName`
|
|
123
|
+
const classReg = new RegExp(`\\bclass\\s+${oldName}\\b`, 'g');
|
|
124
|
+
const classMatches = (modified.match(classReg) || []).length;
|
|
125
|
+
if (classMatches > 0) {
|
|
126
|
+
modified = modified.replace(classReg, `class ${newName}`);
|
|
127
|
+
count += classMatches;
|
|
128
|
+
}
|
|
129
|
+
// Replace `OldName.` -> `NewName.`
|
|
130
|
+
const refReg = new RegExp(`\\b${oldName}\\.`, 'g');
|
|
131
|
+
const refMatches = (modified.match(refReg) || []).length;
|
|
132
|
+
if (refMatches > 0) {
|
|
133
|
+
modified = modified.replace(refReg, `${newName}.`);
|
|
134
|
+
count += refMatches;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (type === 'step') {
|
|
138
|
+
// Replace `Step.oldStep(` -> `Step.newStep(`
|
|
139
|
+
const stepCallReg = new RegExp(`\\bStep\\.${oldName}\\b`, 'g');
|
|
140
|
+
const stepCallMatches = (modified.match(stepCallReg) || []).length;
|
|
141
|
+
if (stepCallMatches > 0) {
|
|
142
|
+
modified = modified.replace(stepCallReg, `Step.${newName}`);
|
|
143
|
+
count += stepCallMatches;
|
|
144
|
+
}
|
|
145
|
+
// Replace `@step:oldStep` -> `@step:newStep` in markdown specs
|
|
146
|
+
const directiveReg = new RegExp(`@step:${oldName}\\b`, 'g');
|
|
147
|
+
const dirMatches = (modified.match(directiveReg) || []).length;
|
|
148
|
+
if (dirMatches > 0) {
|
|
149
|
+
modified = modified.replace(directiveReg, `@step:${newName}`);
|
|
150
|
+
count += dirMatches;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (type === 'action') {
|
|
154
|
+
// Replace `Spectra.oldAction(` -> `Spectra.newAction(`
|
|
155
|
+
const actionReg = new RegExp(`\\bSpectra\\.${oldName}\\b`, 'g');
|
|
156
|
+
const actionMatches = (modified.match(actionReg) || []).length;
|
|
157
|
+
if (actionMatches > 0) {
|
|
158
|
+
modified = modified.replace(actionReg, `Spectra.${newName}`);
|
|
159
|
+
count += actionMatches;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (type === 'fixture') {
|
|
163
|
+
// Replace `Fixture.oldFixture` -> `Fixture.newFixture`
|
|
164
|
+
const fixReg = new RegExp(`\\bFixture\\.${oldName}\\b`, 'g');
|
|
165
|
+
const fixMatches = (modified.match(fixReg) || []).length;
|
|
166
|
+
if (fixMatches > 0) {
|
|
167
|
+
modified = modified.replace(fixReg, `Fixture.${newName}`);
|
|
168
|
+
count += fixMatches;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (count > 0) {
|
|
172
|
+
totalOccurrences += count;
|
|
173
|
+
plannedEdits.push({
|
|
174
|
+
filePath,
|
|
175
|
+
originalContent,
|
|
176
|
+
newContent: modified,
|
|
177
|
+
count,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (dryRun) {
|
|
182
|
+
return {
|
|
183
|
+
status: 'success',
|
|
184
|
+
entityType: type,
|
|
185
|
+
oldSymbol,
|
|
186
|
+
newSymbol,
|
|
187
|
+
modifiedFiles: plannedEdits.map((e) => path.relative(workspaceRoot, e.filePath)),
|
|
188
|
+
occurrencesReplaced: totalOccurrences,
|
|
189
|
+
renamedPaths: plannedRenames,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Phase 2: Atomic Flush & Rollback Guard
|
|
193
|
+
const modifiedFilesWritten = [];
|
|
194
|
+
const originalSnapshots = new Map();
|
|
195
|
+
try {
|
|
196
|
+
// 1. Write file edits
|
|
197
|
+
for (const edit of plannedEdits) {
|
|
198
|
+
originalSnapshots.set(edit.filePath, edit.originalContent);
|
|
199
|
+
fs.writeFileSync(edit.filePath, edit.newContent, 'utf-8');
|
|
200
|
+
modifiedFilesWritten.push(edit.filePath);
|
|
201
|
+
}
|
|
202
|
+
// 2. Perform path renames (directories and files)
|
|
203
|
+
for (const rn of plannedRenames) {
|
|
204
|
+
if (fs.existsSync(rn.from)) {
|
|
205
|
+
const parent = path.dirname(rn.to);
|
|
206
|
+
if (!fs.existsSync(parent))
|
|
207
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
208
|
+
fs.renameSync(rn.from, rn.to);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 3. Automatically regenerate ambient declarations post-refactor
|
|
212
|
+
TypeGenerator.generateAll(workspaceRoot);
|
|
213
|
+
const typesDir = path.join(workspaceRoot, '.testspectra', 'types');
|
|
214
|
+
if (fs.existsSync(typesDir)) {
|
|
215
|
+
modifiedFilesWritten.push(path.join(typesDir, 'web.d.ts'));
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
status: 'success',
|
|
219
|
+
entityType: type,
|
|
220
|
+
oldSymbol,
|
|
221
|
+
newSymbol,
|
|
222
|
+
modifiedFiles: Array.from(new Set(modifiedFilesWritten.map((p) => path.relative(workspaceRoot, p)))),
|
|
223
|
+
occurrencesReplaced: totalOccurrences,
|
|
224
|
+
renamedPaths: plannedRenames,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (ioErr) {
|
|
228
|
+
// ROLLBACK GUARD: Restore all modified files from original snapshots
|
|
229
|
+
for (const [filePath, orig] of originalSnapshots.entries()) {
|
|
230
|
+
try {
|
|
231
|
+
fs.writeFileSync(filePath, orig, 'utf-8');
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
}
|
|
235
|
+
// Reverse path renames
|
|
236
|
+
for (const rn of plannedRenames) {
|
|
237
|
+
if (fs.existsSync(rn.to)) {
|
|
238
|
+
try {
|
|
239
|
+
fs.renameSync(rn.to, rn.from);
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
status: 'failed',
|
|
246
|
+
entityType: type,
|
|
247
|
+
oldSymbol,
|
|
248
|
+
newSymbol,
|
|
249
|
+
modifiedFiles: [],
|
|
250
|
+
occurrencesReplaced: 0,
|
|
251
|
+
error: `Atomic rollback executed due to I/O error: ${ioErr.message}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatAction, formatAssertion } from '../semantic-formatter.js';
|
|
3
|
+
describe('semantic-formatter', () => {
|
|
4
|
+
describe('formatAction', () => {
|
|
5
|
+
it('formats click', () => expect(formatAction('click', 'LoginPage.submitButton')).toBe('Click on element "LoginPage.submitButton"'));
|
|
6
|
+
it('formats doubleClick', () => expect(formatAction('doubleClick', 'targetBtn')).toBe('Double-click on element "targetBtn"'));
|
|
7
|
+
it('formats rightClick', () => expect(formatAction('rightClick', 'targetBtn')).toBe('Right-click on element "targetBtn"'));
|
|
8
|
+
it('formats type', () => expect(formatAction('type', 'LoginPage.emailInput', { text: 'admin@testspectra.dev' })).toBe('Type "admin@testspectra.dev" into element "LoginPage.emailInput"'));
|
|
9
|
+
it('formats clear', () => expect(formatAction('clear', 'input')).toBe('Clear text in element "input"'));
|
|
10
|
+
it('formats select', () => expect(formatAction('select', 'dropdown', { option: 'Option A' })).toBe('Select option "Option A" in element "dropdown"'));
|
|
11
|
+
it('formats hover', () => expect(formatAction('hover', 'menu')).toBe('Hover over element "menu"'));
|
|
12
|
+
it('formats dragDrop', () => expect(formatAction('dragDrop', 'source', { dest: 'dest' })).toBe('Drag element "source" and drop onto element "dest"'));
|
|
13
|
+
it('formats scrollIntoView', () => expect(formatAction('scrollIntoView', 'footer')).toBe('Scroll element "footer" into view'));
|
|
14
|
+
it('formats longPress', () => expect(formatAction('longPress', 'btn', { durationMs: 1500 })).toBe('Long-press on element "btn" for 1500ms'));
|
|
15
|
+
it('formats waitForElement', () => expect(formatAction('waitForElement', 'modal')).toBe('Wait for element "modal" to be displayed'));
|
|
16
|
+
it('formats navigate', () => expect(formatAction('navigate', undefined, { url: '/login' })).toBe('Navigate to "/login"'));
|
|
17
|
+
it('formats back', () => expect(formatAction('back')).toBe('Navigate back in browser'));
|
|
18
|
+
it('formats forward', () => expect(formatAction('forward')).toBe('Navigate forward in browser'));
|
|
19
|
+
it('formats refresh', () => expect(formatAction('refresh')).toBe('Refresh current page'));
|
|
20
|
+
it('formats setViewport', () => expect(formatAction('setViewport', undefined, { width: 1920, height: 1080 })).toBe('Set viewport to 1920x1080'));
|
|
21
|
+
it('formats wait', () => expect(formatAction('wait', undefined, { ms: 1000 })).toBe('Wait for 1000ms'));
|
|
22
|
+
it('formats pressKey', () => expect(formatAction('pressKey', undefined, { key: 'Enter' })).toBe('Press keyboard key "Enter"'));
|
|
23
|
+
it('formats scroll', () => expect(formatAction('scroll', undefined, { direction: 'down', pixels: 400 })).toBe('Scroll down by 400px'));
|
|
24
|
+
it('formats swipe', () => expect(formatAction('swipe', undefined, { direction: 'left', distance: 300 })).toBe('Swipe left by 300px'));
|
|
25
|
+
it('formats intercept', () => expect(formatAction('intercept', undefined, { method: 'GET', url: '/api/data' })).toBe('Intercept GET "/api/data" mock rule registered'));
|
|
26
|
+
it('formats clearCookies', () => expect(formatAction('clearCookies')).toBe('Clear all browser cookies'));
|
|
27
|
+
it('formats clearLocalStorage', () => expect(formatAction('clearLocalStorage')).toBe('Clear browser localStorage'));
|
|
28
|
+
});
|
|
29
|
+
describe('formatAssertion', () => {
|
|
30
|
+
// 38 assertions
|
|
31
|
+
it('formats be.visible', () => expect(formatAssertion('be.visible', 'DashboardPage.header')).toBe('Expect element "DashboardPage.header" to be visible'));
|
|
32
|
+
it('formats not.be.visible', () => expect(formatAssertion('not.be.visible', 'DashboardPage.header')).toBe('Expect element "DashboardPage.header" not to be visible'));
|
|
33
|
+
it('formats exist', () => expect(formatAssertion('exist', 'DashboardPage.header')).toBe('Expect element "DashboardPage.header" to exist in DOM'));
|
|
34
|
+
it('formats not.exist', () => expect(formatAssertion('not.exist', 'DashboardPage.header')).toBe('Expect element "DashboardPage.header" not to exist in DOM'));
|
|
35
|
+
it('formats be.clickable', () => expect(formatAssertion('be.clickable', 'btn')).toBe('Expect element "btn" to be clickable'));
|
|
36
|
+
it('formats not.be.clickable', () => expect(formatAssertion('not.be.clickable', 'btn')).toBe('Expect element "btn" not to be clickable'));
|
|
37
|
+
it('formats be.enabled', () => expect(formatAssertion('be.enabled', 'btn')).toBe('Expect element "btn" to be enabled'));
|
|
38
|
+
it('formats be.disabled', () => expect(formatAssertion('be.disabled', 'btn')).toBe('Expect element "btn" to be disabled'));
|
|
39
|
+
it('formats be.checked', () => expect(formatAssertion('be.checked', 'checkbox')).toBe('Expect element "checkbox" to be checked'));
|
|
40
|
+
it('formats not.be.checked', () => expect(formatAssertion('not.be.checked', 'checkbox')).toBe('Expect element "checkbox" not to be checked'));
|
|
41
|
+
it('formats be.selected', () => expect(formatAssertion('be.selected', 'radio')).toBe('Expect element "radio" to be selected'));
|
|
42
|
+
it('formats not.be.selected', () => expect(formatAssertion('not.be.selected', 'radio')).toBe('Expect element "radio" not to be selected'));
|
|
43
|
+
it('formats be.focused', () => expect(formatAssertion('be.focused', 'input')).toBe('Expect element "input" to be focused'));
|
|
44
|
+
it('formats not.be.focused', () => expect(formatAssertion('not.be.focused', 'input')).toBe('Expect element "input" not to be focused'));
|
|
45
|
+
it('formats have.text', () => expect(formatAssertion('have.text', 'title', { expected: 'Hello' })).toBe('Expect element "title" to have text "Hello"'));
|
|
46
|
+
it('formats not.have.text', () => expect(formatAssertion('not.have.text', 'title', { expected: 'Hello' })).toBe('Expect element "title" not to have text "Hello"'));
|
|
47
|
+
it('formats contain.text', () => expect(formatAssertion('contain.text', 'title', { expected: 'Hello' })).toBe('Expect element "title" to contain text "Hello"'));
|
|
48
|
+
it('formats not.contain.text', () => expect(formatAssertion('not.contain.text', 'title', { expected: 'Hello' })).toBe('Expect element "title" not to contain text "Hello"'));
|
|
49
|
+
it('formats have.value', () => expect(formatAssertion('have.value', 'input', { expected: 'admin' })).toBe('Expect element "input" to have value "admin"'));
|
|
50
|
+
it('formats not.have.value', () => expect(formatAssertion('not.have.value', 'input', { expected: 'admin' })).toBe('Expect element "input" not to have value "admin"'));
|
|
51
|
+
it('formats contain.value', () => expect(formatAssertion('contain.value', 'input', { expected: 'min' })).toBe('Expect element "input" to contain value "min"'));
|
|
52
|
+
it('formats not.contain.value', () => expect(formatAssertion('not.contain.value', 'input', { expected: 'min' })).toBe('Expect element "input" not to contain value "min"'));
|
|
53
|
+
it('formats have.attr', () => expect(formatAssertion('have.attr', 'img', { name: 'alt', value: 'logo' })).toBe('Expect element "img" to have attribute "alt" = "logo"'));
|
|
54
|
+
it('formats not.have.attr', () => expect(formatAssertion('not.have.attr', 'img', { name: 'alt' })).toBe('Expect element "img" not to have attribute "alt"'));
|
|
55
|
+
it('formats have.class', () => expect(formatAssertion('have.class', 'div', { expected: 'active' })).toBe('Expect element "div" to have class "active"'));
|
|
56
|
+
it('formats not.have.class', () => expect(formatAssertion('not.have.class', 'div', { expected: 'active' })).toBe('Expect element "div" not to have class "active"'));
|
|
57
|
+
it('formats have.css', () => expect(formatAssertion('have.css', 'div', { name: 'color', value: 'red' })).toBe('Expect element "div" to have CSS "color" = "red"'));
|
|
58
|
+
it('formats not.have.css', () => expect(formatAssertion('not.have.css', 'div', { name: 'color', value: 'red' })).toBe('Expect element "div" not to have CSS "color" = "red"'));
|
|
59
|
+
it('formats have.length', () => expect(formatAssertion('have.length', 'DashboardPage.statCards', { expected: 4 })).toBe('Expect collection "DashboardPage.statCards" count to equal 4'));
|
|
60
|
+
it('formats not.have.length', () => expect(formatAssertion('not.have.length', 'cards', { expected: 4 })).toBe('Expect collection "cards" count not to equal 4'));
|
|
61
|
+
it('formats have.length.greaterThan', () => expect(formatAssertion('have.length.greaterThan', 'cards', { expected: 2 })).toBe('Expect collection "cards" count to be greater than 2'));
|
|
62
|
+
it('formats have.length.lessThan', () => expect(formatAssertion('have.length.lessThan', 'cards', { expected: 10 })).toBe('Expect collection "cards" count to be less than 10'));
|
|
63
|
+
it('formats be.empty', () => expect(formatAssertion('be.empty', 'cards')).toBe('Expect collection "cards" to be empty'));
|
|
64
|
+
it('formats not.be.empty', () => expect(formatAssertion('not.be.empty', 'cards')).toBe('Expect collection "cards" not to be empty'));
|
|
65
|
+
it('formats shouldHaveUrl', () => expect(formatAssertion('shouldHaveUrl', undefined, { expected: '/home' })).toBe('Expect browser URL to be "/home"'));
|
|
66
|
+
it('formats shouldContainUrl', () => expect(formatAssertion('shouldContainUrl', undefined, { expected: '/home' })).toBe('Expect browser URL to contain "/home"'));
|
|
67
|
+
it('formats shouldHaveTitle', () => expect(formatAssertion('shouldHaveTitle', undefined, { expected: 'Home' })).toBe('Expect browser title to be "Home"'));
|
|
68
|
+
it('formats shouldContainTitle', () => expect(formatAssertion('shouldContainTitle', undefined, { expected: 'Home' })).toBe('Expect browser title to contain "Home"'));
|
|
69
|
+
it('formats shouldBeLoaded', () => expect(formatAssertion('shouldBeLoaded', undefined)).toBe('Expect page to be fully loaded'));
|
|
70
|
+
it('formats shouldBePageLoaded', () => expect(formatAssertion('shouldBePageLoaded', undefined)).toBe('Expect page to be fully loaded'));
|
|
71
|
+
it('formats shouldHaveNoConsoleErrors', () => expect(formatAssertion('shouldHaveNoConsoleErrors', undefined)).toBe('Expect browser console to have no errors'));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ActionKey, AssertionKey } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Transforms raw matcher assertions and element commands into grammatically correct English sentences.
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatAction(actionKey: ActionKey | string, target?: any, args?: Record<string, any>): string;
|
|
6
|
+
export declare function formatAssertion(assertionKey: AssertionKey | string, target?: any, args?: Record<string, any>): string;
|