@testspectra/cli 1.1.7 → 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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { SpecLinter } from '../spec-linter.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
describe('Step Parity & Symbol Integrity Linter (Stage 3 & Stage 4)', () => {
|
|
9
|
+
const tempDir = path.join(__dirname, '__temp_parity__');
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (fs.existsSync(tempDir)) {
|
|
15
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
it('passes validation when declared @step directives match Step.*() invocations in web.test.ts', () => {
|
|
19
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
20
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
21
|
+
fs.writeFileSync(specPath, `---
|
|
22
|
+
id: TC-0100-matching
|
|
23
|
+
title: Valid login step matching
|
|
24
|
+
priority: High
|
|
25
|
+
caseType: Positive
|
|
26
|
+
status: ready-for-automation
|
|
27
|
+
coverage:
|
|
28
|
+
web: automated
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Valid login step matching
|
|
32
|
+
|
|
33
|
+
## Test Steps
|
|
34
|
+
1. @step:login
|
|
35
|
+
2. Assert dashboard loaded
|
|
36
|
+
`);
|
|
37
|
+
fs.writeFileSync(testPath, `it("Valid login step matching", async () => {
|
|
38
|
+
await Step.login();
|
|
39
|
+
await LoginPage.submitButton.should("be.visible");
|
|
40
|
+
});
|
|
41
|
+
`);
|
|
42
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
43
|
+
expect(result.diagnostics).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
it('emits testspectra/missing-step-call when a declared @step is absent in web.test.ts', () => {
|
|
46
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
47
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
48
|
+
fs.writeFileSync(specPath, `---
|
|
49
|
+
id: TC-0101-missing-in-code
|
|
50
|
+
title: Step declared but missing in code
|
|
51
|
+
priority: High
|
|
52
|
+
caseType: Positive
|
|
53
|
+
status: ready-for-automation
|
|
54
|
+
coverage:
|
|
55
|
+
web: automated
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Step declared but missing in code
|
|
59
|
+
|
|
60
|
+
## Test Steps
|
|
61
|
+
1. @step:login
|
|
62
|
+
2. @step:logout
|
|
63
|
+
`);
|
|
64
|
+
fs.writeFileSync(testPath, `it("Step declared but missing in code", async () => {
|
|
65
|
+
await Step.login();
|
|
66
|
+
});
|
|
67
|
+
`);
|
|
68
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
69
|
+
const missingErrors = result.diagnostics.filter((d) => d.code === 'testspectra/missing-step-call');
|
|
70
|
+
expect(missingErrors).toHaveLength(1);
|
|
71
|
+
expect(missingErrors[0].severity).toBe('error');
|
|
72
|
+
expect(missingErrors[0].message).toContain('Step "@step:logout" declared in spec.md line 15 is missing in web.test.ts');
|
|
73
|
+
expect(missingErrors[0].line).toBe(15);
|
|
74
|
+
});
|
|
75
|
+
it('emits testspectra/undocumented-step warning when Step.*() called in test script is not documented in spec.md', () => {
|
|
76
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
77
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
78
|
+
fs.writeFileSync(specPath, `---
|
|
79
|
+
id: TC-0102-undocumented-step
|
|
80
|
+
title: Undocumented step call
|
|
81
|
+
priority: Medium
|
|
82
|
+
caseType: Positive
|
|
83
|
+
status: ready-for-automation
|
|
84
|
+
coverage:
|
|
85
|
+
web: automated
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
# Undocumented step call
|
|
89
|
+
|
|
90
|
+
## Test Steps
|
|
91
|
+
1. @step:login
|
|
92
|
+
`);
|
|
93
|
+
fs.writeFileSync(testPath, `it("Undocumented step call", async () => {
|
|
94
|
+
await Step.login();
|
|
95
|
+
await Step.logout();
|
|
96
|
+
});
|
|
97
|
+
`);
|
|
98
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
99
|
+
const warnings = result.diagnostics.filter((d) => d.code === 'testspectra/undocumented-step');
|
|
100
|
+
expect(warnings).toHaveLength(1);
|
|
101
|
+
expect(warnings[0].severity).toBe('warning');
|
|
102
|
+
expect(warnings[0].message).toContain('Step "logout" called in web.test.ts:L3 is not documented with @step:logout in spec.md');
|
|
103
|
+
expect(warnings[0].line).toBe(3);
|
|
104
|
+
});
|
|
105
|
+
it('emits testspectra/title-mismatch warning when it() title in test script differs from spec.md title', () => {
|
|
106
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
107
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
108
|
+
fs.writeFileSync(specPath, `---
|
|
109
|
+
id: TC-0103-title-mismatch
|
|
110
|
+
title: Verify user profile updates
|
|
111
|
+
priority: High
|
|
112
|
+
caseType: Positive
|
|
113
|
+
status: ready-for-automation
|
|
114
|
+
coverage:
|
|
115
|
+
web: automated
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# Verify user profile updates
|
|
119
|
+
|
|
120
|
+
## Test Steps
|
|
121
|
+
1. Open profile and change name
|
|
122
|
+
`);
|
|
123
|
+
fs.writeFileSync(testPath, `it("should process checkout cart items", async () => {
|
|
124
|
+
await CartPage.checkout();
|
|
125
|
+
});
|
|
126
|
+
`);
|
|
127
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
128
|
+
const titleWarnings = result.diagnostics.filter((d) => d.code === 'testspectra/title-mismatch');
|
|
129
|
+
expect(titleWarnings).toHaveLength(1);
|
|
130
|
+
expect(titleWarnings[0].severity).toBe('warning');
|
|
131
|
+
expect(titleWarnings[0].message).toContain('Test title "should process checkout cart items" in web.test.ts:L1 does not match spec.md title "Verify user profile updates"');
|
|
132
|
+
});
|
|
133
|
+
it('does not emit testspectra/title-mismatch warning when it() title is directly aligned with spec.md title', () => {
|
|
134
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
135
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
136
|
+
fs.writeFileSync(specPath, `---
|
|
137
|
+
id: TC-0104-title-aligned
|
|
138
|
+
title: Verify playground form inputs and default states
|
|
139
|
+
priority: High
|
|
140
|
+
caseType: Positive
|
|
141
|
+
status: ready-for-automation
|
|
142
|
+
coverage:
|
|
143
|
+
web: automated
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
# Verify playground form inputs and default states
|
|
147
|
+
|
|
148
|
+
## Test Steps
|
|
149
|
+
1. Check form inputs
|
|
150
|
+
`);
|
|
151
|
+
fs.writeFileSync(testPath, `it("Verify playground form inputs and default states", async () => {
|
|
152
|
+
await PlaygroundPage.open();
|
|
153
|
+
});
|
|
154
|
+
`);
|
|
155
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
156
|
+
const titleWarnings = result.diagnostics.filter((d) => d.code === 'testspectra/title-mismatch');
|
|
157
|
+
expect(titleWarnings).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
it('emits testspectra/no-manual-import error when manual import statements are detected in test script', () => {
|
|
160
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
161
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
162
|
+
fs.writeFileSync(specPath, `---
|
|
163
|
+
id: TC-0105-manual-imports
|
|
164
|
+
title: Forbidden manual imports
|
|
165
|
+
priority: High
|
|
166
|
+
caseType: Positive
|
|
167
|
+
status: ready-for-automation
|
|
168
|
+
coverage:
|
|
169
|
+
web: automated
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
# Forbidden manual imports
|
|
173
|
+
|
|
174
|
+
## Test Steps
|
|
175
|
+
1. Test without manual imports
|
|
176
|
+
`);
|
|
177
|
+
fs.writeFileSync(testPath, `import { expect } from 'vitest';
|
|
178
|
+
import path from 'path';
|
|
179
|
+
|
|
180
|
+
it("Forbidden manual imports", async () => {
|
|
181
|
+
expect(1).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
`);
|
|
184
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
185
|
+
const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
|
|
186
|
+
expect(importErrors).toHaveLength(2);
|
|
187
|
+
expect(importErrors[0].severity).toBe('error');
|
|
188
|
+
expect(importErrors[0].message).toContain('Manual imports forbidden');
|
|
189
|
+
expect(importErrors[0].line).toBe(1);
|
|
190
|
+
expect(importErrors[1].line).toBe(2);
|
|
191
|
+
});
|
|
192
|
+
it('emits testspectra/single-it-required error when test script contains 0 it() blocks', () => {
|
|
193
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
194
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
195
|
+
fs.writeFileSync(specPath, `---
|
|
196
|
+
id: TC-0106-zero-it
|
|
197
|
+
title: Zero it blocks test
|
|
198
|
+
priority: Medium
|
|
199
|
+
caseType: Positive
|
|
200
|
+
status: ready-for-automation
|
|
201
|
+
coverage:
|
|
202
|
+
web: automated
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
# Zero it blocks test
|
|
206
|
+
|
|
207
|
+
## Test Steps
|
|
208
|
+
1. Execute step
|
|
209
|
+
`);
|
|
210
|
+
fs.writeFileSync(testPath, `// Helper file with no test execution block
|
|
211
|
+
const dummy = 123;
|
|
212
|
+
`);
|
|
213
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
214
|
+
const itErrors = result.diagnostics.filter((d) => d.code === 'testspectra/single-it-required');
|
|
215
|
+
expect(itErrors).toHaveLength(1);
|
|
216
|
+
expect(itErrors[0].severity).toBe('error');
|
|
217
|
+
expect(itErrors[0].message).toContain('Test case script must contain exactly one it() block (found 0)');
|
|
218
|
+
});
|
|
219
|
+
it('emits testspectra/single-it-required error when test script contains multiple it() blocks', () => {
|
|
220
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
221
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
222
|
+
fs.writeFileSync(specPath, `---
|
|
223
|
+
id: TC-0107-multiple-it
|
|
224
|
+
title: Multiple it blocks test
|
|
225
|
+
priority: Medium
|
|
226
|
+
caseType: Positive
|
|
227
|
+
status: ready-for-automation
|
|
228
|
+
coverage:
|
|
229
|
+
web: automated
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
# Multiple it blocks test
|
|
233
|
+
|
|
234
|
+
## Test Steps
|
|
235
|
+
1. First step
|
|
236
|
+
2. Second step
|
|
237
|
+
`);
|
|
238
|
+
fs.writeFileSync(testPath, `it("Multiple it blocks test", async () => {
|
|
239
|
+
await Step.first();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should execute second step", async () => {
|
|
243
|
+
await Step.second();
|
|
244
|
+
});
|
|
245
|
+
`);
|
|
246
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
247
|
+
const itErrors = result.diagnostics.filter((d) => d.code === 'testspectra/single-it-required');
|
|
248
|
+
expect(itErrors).toHaveLength(1);
|
|
249
|
+
expect(itErrors[0].severity).toBe('error');
|
|
250
|
+
expect(itErrors[0].message).toContain('Multiple it() blocks detected (2); each test script must contain exactly one it() block');
|
|
251
|
+
expect(itErrors[0].line).toBe(5);
|
|
252
|
+
});
|
|
253
|
+
it('emits testspectra/no-test-block error when test() block is used instead of it()', () => {
|
|
254
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
255
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
256
|
+
fs.writeFileSync(specPath, `---
|
|
257
|
+
id: TC-0108-test-keyword
|
|
258
|
+
title: Test keyword forbidden
|
|
259
|
+
priority: Medium
|
|
260
|
+
caseType: Positive
|
|
261
|
+
status: ready-for-automation
|
|
262
|
+
coverage:
|
|
263
|
+
web: automated
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
# Test keyword forbidden
|
|
267
|
+
|
|
268
|
+
## Test Steps
|
|
269
|
+
1. Execute step
|
|
270
|
+
`);
|
|
271
|
+
fs.writeFileSync(testPath, `test("Test keyword forbidden", async () => {
|
|
272
|
+
await Step.first();
|
|
273
|
+
});
|
|
274
|
+
`);
|
|
275
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
276
|
+
const testErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-test-block');
|
|
277
|
+
expect(testErrors).toHaveLength(1);
|
|
278
|
+
expect(testErrors[0].severity).toBe('error');
|
|
279
|
+
expect(testErrors[0].message).toContain('test() is forbidden; TestSpectra test scripts must strictly use it() instead of test()');
|
|
280
|
+
expect(testErrors[0].line).toBe(1);
|
|
281
|
+
});
|
|
282
|
+
it('emits testspectra/no-describe-block error when describe() block is detected in test script', () => {
|
|
283
|
+
const specPath = path.join(tempDir, 'spec.md');
|
|
284
|
+
const testPath = path.join(tempDir, 'web.test.ts');
|
|
285
|
+
fs.writeFileSync(specPath, `---
|
|
286
|
+
id: TC-0108-describe-block
|
|
287
|
+
title: Describe block forbidden
|
|
288
|
+
priority: High
|
|
289
|
+
caseType: Positive
|
|
290
|
+
status: ready-for-automation
|
|
291
|
+
coverage:
|
|
292
|
+
web: automated
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
# Describe block forbidden
|
|
296
|
+
|
|
297
|
+
## Test Steps
|
|
298
|
+
1. Execute test inside describe
|
|
299
|
+
`);
|
|
300
|
+
fs.writeFileSync(testPath, `describe("Describe block forbidden suite", () => {
|
|
301
|
+
it("Describe block forbidden", async () => {
|
|
302
|
+
await LoginPage.open();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
`);
|
|
306
|
+
const result = SpecLinter.validateSpecFile(specPath);
|
|
307
|
+
const descErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-describe-block');
|
|
308
|
+
expect(descErrors).toHaveLength(1);
|
|
309
|
+
expect(descErrors[0].severity).toBe('error');
|
|
310
|
+
expect(descErrors[0].message).toContain('describe() blocks are forbidden; test suites are structured via directory hierarchy with suite.md');
|
|
311
|
+
expect(descErrors[0].line).toBe(1);
|
|
312
|
+
});
|
|
313
|
+
describe('testspectra/no-manual-import extended to non-test scripts', () => {
|
|
314
|
+
it('emits testspectra/no-manual-import when shared step file contains import statements', () => {
|
|
315
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'login');
|
|
316
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
317
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `import { expect } from 'vitest';
|
|
318
|
+
import path from 'path';
|
|
319
|
+
|
|
320
|
+
export default async function login() {
|
|
321
|
+
await LoginPage.open();
|
|
322
|
+
}
|
|
323
|
+
`);
|
|
324
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
325
|
+
const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
|
|
326
|
+
expect(importErrors).toHaveLength(2);
|
|
327
|
+
expect(importErrors[0].severity).toBe('error');
|
|
328
|
+
expect(importErrors[0].message).toContain('Manual imports forbidden');
|
|
329
|
+
expect(importErrors[0].filePath).toContain('web.step.ts');
|
|
330
|
+
});
|
|
331
|
+
it('emits testspectra/no-manual-import when shared action file contains import statements', () => {
|
|
332
|
+
const actionsDir = path.join(tempDir, 'shared', 'testing', 'support', 'actions', 'fillForm');
|
|
333
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
334
|
+
fs.writeFileSync(path.join(actionsDir, 'web.action.ts'), `import { Page } from 'playwright';
|
|
335
|
+
|
|
336
|
+
export default async function (email: string) {
|
|
337
|
+
await this.get("#email").type(email);
|
|
338
|
+
}
|
|
339
|
+
`);
|
|
340
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
341
|
+
const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
|
|
342
|
+
expect(importErrors).toHaveLength(1);
|
|
343
|
+
expect(importErrors[0].filePath).toContain('web.action.ts');
|
|
344
|
+
});
|
|
345
|
+
it('emits testspectra/no-manual-import when shared hook file contains import statements', () => {
|
|
346
|
+
const hooksDir = path.join(tempDir, 'shared', 'testing', 'global-hooks', 'before');
|
|
347
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
348
|
+
fs.writeFileSync(path.join(hooksDir, 'web.hook.ts'), `import { database } from './setup';
|
|
349
|
+
|
|
350
|
+
export default async function () {
|
|
351
|
+
await database.reset();
|
|
352
|
+
}
|
|
353
|
+
`);
|
|
354
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
355
|
+
const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
|
|
356
|
+
expect(importErrors).toHaveLength(1);
|
|
357
|
+
expect(importErrors[0].filePath).toContain('web.hook.ts');
|
|
358
|
+
});
|
|
359
|
+
it('does NOT emit testspectra/no-manual-import when shared step has no imports', () => {
|
|
360
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'verify');
|
|
361
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
362
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `export default async function verifyState() {
|
|
363
|
+
await Page.open();
|
|
364
|
+
await Page.element.should("be.visible");
|
|
365
|
+
}
|
|
366
|
+
`);
|
|
367
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
368
|
+
const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
|
|
369
|
+
expect(importErrors).toHaveLength(0);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface WorkspaceAssets {
|
|
2
|
+
sharedSteps: Set<string>;
|
|
3
|
+
pageObjects: Set<string>;
|
|
4
|
+
fixtures: Set<string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Scans a workspace directory for all `project.json` files that have both
|
|
8
|
+
* "scope:shared" and "type:shared" tags, and returns their parent folder paths.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findSharedLibraryRoots(workspaceDir: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the root directory of a shared testing library if the file path is inside one.
|
|
13
|
+
* Determines shared library status based on `project.json` containing
|
|
14
|
+
* both "scope:shared" and "type:shared" tags in ancestor directories,
|
|
15
|
+
* taking the parent folder of that `project.json` as the shared library root.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getSharedLibraryRoot(filePath: string, workspaceDir?: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a file path is inside a shared testing library directory.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isInsideSharedLibrary(filePath: string, workspaceDir?: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Traverses upwards to locate the project root containing spectra.config.ts or package.json.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findProjectRoot(startDir: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Discovers and indexes all available shared steps, page objects, and fixtures
|
|
28
|
+
* across the workspace (including root and shared directories).
|
|
29
|
+
*/
|
|
30
|
+
export declare function discoverWorkspaceAssets(workspaceDir: string): WorkspaceAssets;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Scans a workspace directory for all `project.json` files that have both
|
|
5
|
+
* "scope:shared" and "type:shared" tags, and returns their parent folder paths.
|
|
6
|
+
*/
|
|
7
|
+
export function findSharedLibraryRoots(workspaceDir) {
|
|
8
|
+
const sharedRoots = [];
|
|
9
|
+
const ignoredDirs = new Set(['node_modules', 'dist', '.git', '.testspectra', 'build', 'coverage']);
|
|
10
|
+
const scan = (dir) => {
|
|
11
|
+
if (!fs.existsSync(dir))
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (ignoredDirs.has(entry.name))
|
|
18
|
+
continue;
|
|
19
|
+
scan(path.join(dir, entry.name));
|
|
20
|
+
}
|
|
21
|
+
else if (entry.isFile() && entry.name === 'project.json') {
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
const tags = Array.isArray(parsed.tags) ? parsed.tags : [];
|
|
26
|
+
if (tags.includes('scope:shared') && tags.includes('type:shared')) {
|
|
27
|
+
sharedRoots.push(dir);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
};
|
|
36
|
+
scan(workspaceDir);
|
|
37
|
+
return sharedRoots;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolves the root directory of a shared testing library if the file path is inside one.
|
|
41
|
+
* Determines shared library status based on `project.json` containing
|
|
42
|
+
* both "scope:shared" and "type:shared" tags in ancestor directories,
|
|
43
|
+
* taking the parent folder of that `project.json` as the shared library root.
|
|
44
|
+
*/
|
|
45
|
+
export function getSharedLibraryRoot(filePath, workspaceDir) {
|
|
46
|
+
const resolved = workspaceDir && !path.isAbsolute(filePath) ? path.resolve(workspaceDir, filePath) : path.resolve(filePath);
|
|
47
|
+
let current = resolved;
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(current)) {
|
|
50
|
+
if (fs.statSync(current).isFile()) {
|
|
51
|
+
current = path.dirname(current);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (path.extname(current)) {
|
|
55
|
+
current = path.dirname(current);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
if (path.extname(current)) {
|
|
60
|
+
current = path.dirname(current);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
while (true) {
|
|
64
|
+
const projectJsonPath = path.join(current, 'project.json');
|
|
65
|
+
if (fs.existsSync(projectJsonPath)) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(projectJsonPath, 'utf-8');
|
|
68
|
+
const projectConfig = JSON.parse(raw);
|
|
69
|
+
const tags = Array.isArray(projectConfig.tags) ? projectConfig.tags : [];
|
|
70
|
+
if (tags.includes('scope:shared') && tags.includes('type:shared')) {
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const parent = path.dirname(current);
|
|
80
|
+
if (parent === current) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Checks if a file path is inside a shared testing library directory.
|
|
89
|
+
*/
|
|
90
|
+
export function isInsideSharedLibrary(filePath, workspaceDir) {
|
|
91
|
+
return getSharedLibraryRoot(filePath, workspaceDir) !== null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Traverses upwards to locate the project root containing spectra.config.ts or package.json.
|
|
95
|
+
*/
|
|
96
|
+
export function findProjectRoot(startDir) {
|
|
97
|
+
let current = path.resolve(startDir);
|
|
98
|
+
while (true) {
|
|
99
|
+
if (fs.existsSync(path.join(current, 'spectra.config.ts')) ||
|
|
100
|
+
fs.existsSync(path.join(current, 'spectra.config.js'))) {
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
const parent = path.dirname(current);
|
|
104
|
+
if (parent === current) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
current = parent;
|
|
108
|
+
}
|
|
109
|
+
// Fallback: search for package.json
|
|
110
|
+
current = path.resolve(startDir);
|
|
111
|
+
while (true) {
|
|
112
|
+
if (fs.existsSync(path.join(current, 'package.json'))) {
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
const parent = path.dirname(current);
|
|
116
|
+
if (parent === current) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
current = parent;
|
|
120
|
+
}
|
|
121
|
+
return startDir;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Discovers and indexes all available shared steps, page objects, and fixtures
|
|
125
|
+
* across the workspace (including root and shared directories).
|
|
126
|
+
*/
|
|
127
|
+
export function discoverWorkspaceAssets(workspaceDir) {
|
|
128
|
+
const assets = {
|
|
129
|
+
sharedSteps: new Set(),
|
|
130
|
+
pageObjects: new Set(),
|
|
131
|
+
fixtures: new Set(),
|
|
132
|
+
};
|
|
133
|
+
const candidateRoots = [
|
|
134
|
+
workspaceDir,
|
|
135
|
+
...findSharedLibraryRoots(workspaceDir),
|
|
136
|
+
path.join(workspaceDir, 'shared', 'testing'),
|
|
137
|
+
path.join(workspaceDir, 'shared'),
|
|
138
|
+
path.join(workspaceDir, 'testing'),
|
|
139
|
+
];
|
|
140
|
+
// Also check packages in monorepos if packages/ directory exists
|
|
141
|
+
const packagesDir = path.join(workspaceDir, 'packages');
|
|
142
|
+
if (fs.existsSync(packagesDir)) {
|
|
143
|
+
try {
|
|
144
|
+
const pkgs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
145
|
+
for (const pkg of pkgs) {
|
|
146
|
+
if (pkg.isDirectory()) {
|
|
147
|
+
candidateRoots.push(path.join(packagesDir, pkg.name));
|
|
148
|
+
candidateRoots.push(path.join(packagesDir, pkg.name, 'e2e'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
}
|
|
154
|
+
const scanFileForPageClasses = (filePath) => {
|
|
155
|
+
try {
|
|
156
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
157
|
+
const matches = content.matchAll(/(?:class|interface)\s+([A-Za-z0-9_]+Page)\b/g);
|
|
158
|
+
for (const match of matches) {
|
|
159
|
+
assets.pageObjects.add(match[1]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
};
|
|
164
|
+
for (const root of candidateRoots) {
|
|
165
|
+
if (!fs.existsSync(root))
|
|
166
|
+
continue;
|
|
167
|
+
// 1. Shared Steps (support/steps or steps)
|
|
168
|
+
const stepDirs = [path.join(root, 'support', 'steps'), path.join(root, 'steps')];
|
|
169
|
+
for (const sDir of stepDirs) {
|
|
170
|
+
if (fs.existsSync(sDir) && fs.statSync(sDir).isDirectory()) {
|
|
171
|
+
try {
|
|
172
|
+
const entries = fs.readdirSync(sDir, { withFileTypes: true });
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (entry.isDirectory()) {
|
|
175
|
+
assets.sharedSteps.add(entry.name);
|
|
176
|
+
}
|
|
177
|
+
else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
|
|
178
|
+
const nameWithoutExt = path.basename(entry.name, path.extname(entry.name));
|
|
179
|
+
if (nameWithoutExt !== 'index') {
|
|
180
|
+
assets.sharedSteps.add(nameWithoutExt);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 2. Page Objects (page-objects or pageobjects)
|
|
189
|
+
const poDirs = [path.join(root, 'page-objects'), path.join(root, 'pageobjects')];
|
|
190
|
+
for (const poDir of poDirs) {
|
|
191
|
+
if (fs.existsSync(poDir) && fs.statSync(poDir).isDirectory()) {
|
|
192
|
+
try {
|
|
193
|
+
const entries = fs.readdirSync(poDir, { withFileTypes: true });
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (entry.isDirectory()) {
|
|
196
|
+
assets.pageObjects.add(entry.name);
|
|
197
|
+
// Also inspect files in the subdirectory
|
|
198
|
+
try {
|
|
199
|
+
const subEntries = fs.readdirSync(path.join(poDir, entry.name), { withFileTypes: true });
|
|
200
|
+
for (const sub of subEntries) {
|
|
201
|
+
if (sub.isFile() && (sub.name.endsWith('.ts') || sub.name.endsWith('.js'))) {
|
|
202
|
+
scanFileForPageClasses(path.join(poDir, entry.name, sub.name));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
}
|
|
208
|
+
else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
|
|
209
|
+
const nameWithoutExt = path.basename(entry.name, path.extname(entry.name));
|
|
210
|
+
if (nameWithoutExt !== 'index') {
|
|
211
|
+
assets.pageObjects.add(nameWithoutExt);
|
|
212
|
+
}
|
|
213
|
+
scanFileForPageClasses(path.join(poDir, entry.name));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// 3. Fixtures (fixtures)
|
|
221
|
+
const fixDir = path.join(root, 'fixtures');
|
|
222
|
+
if (fs.existsSync(fixDir) && fs.statSync(fixDir).isDirectory()) {
|
|
223
|
+
try {
|
|
224
|
+
const entries = fs.readdirSync(fixDir, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
if (entry.isFile()) {
|
|
227
|
+
const nameWithoutExt = path.basename(entry.name, path.extname(entry.name));
|
|
228
|
+
assets.fixtures.add(nameWithoutExt);
|
|
229
|
+
}
|
|
230
|
+
else if (entry.isDirectory()) {
|
|
231
|
+
assets.fixtures.add(entry.name);
|
|
232
|
+
try {
|
|
233
|
+
const subEntries = fs.readdirSync(path.join(fixDir, entry.name), { withFileTypes: true });
|
|
234
|
+
for (const sub of subEntries) {
|
|
235
|
+
if (sub.isFile()) {
|
|
236
|
+
const subNameWithoutExt = path.basename(sub.name, path.extname(sub.name));
|
|
237
|
+
assets.fixtures.add(subNameWithoutExt);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return assets;
|
|
249
|
+
}
|