@testspectra/cli 1.1.7 → 1.1.8-rc.2
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/spectra.js +0 -0
- package/bin/testspectra-runner.exe +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 +270 -29
- 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 +154 -149
- package/dist/commands/lint.d.ts +5 -0
- package/dist/commands/lint.js +55 -0
- package/dist/commands/refactor.d.ts +2 -1
- package/dist/commands/refactor.js +129 -35
- package/dist/commands/run.d.ts +2 -3
- package/dist/commands/run.js +93 -28
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.js +28 -0
- package/dist/commands/sync-types.js +1 -0
- package/dist/commands/test.d.ts +17 -0
- package/dist/commands/test.js +104 -0
- package/dist/commands/watch.js +9 -3
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +5 -114
- package/dist/config/schema.js +1 -17
- package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
- package/dist/generator/__tests__/type-generator.test.js +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 +76 -40
- package/dist/linter/__tests__/anonymous-export.test.js +184 -0
- package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
- package/dist/linter/__tests__/component-test-imports.test.js +115 -0
- package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
- package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
- package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
- package/dist/linter/__tests__/spec-schema.test.js +369 -0
- package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
- package/dist/linter/__tests__/step-parity.test.js +372 -0
- package/dist/linter/discovery.d.ts +30 -0
- package/dist/linter/discovery.js +249 -0
- package/dist/linter/extractor.d.ts +67 -0
- package/dist/linter/extractor.js +245 -0
- package/dist/linter/index.d.ts +7 -0
- package/dist/linter/index.js +7 -0
- package/dist/linter/parser.d.ts +24 -0
- package/dist/linter/parser.js +219 -0
- package/dist/linter/schemas/spec.schema.d.ts +83 -0
- package/dist/linter/schemas/spec.schema.js +150 -0
- package/dist/linter/schemas/suite.schema.d.ts +59 -0
- package/dist/linter/schemas/suite.schema.js +83 -0
- package/dist/linter/spec-linter.d.ts +23 -0
- package/dist/linter/spec-linter.js +630 -0
- package/dist/linter/types.d.ts +28 -0
- package/dist/linter/types.js +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
- package/dist/refactor/refactor-engine.d.ts +25 -0
- package/dist/refactor/refactor-engine.js +374 -42
- package/dist/reporter/semantic-formatter.js +22 -2
- package/dist/reporter/types.d.ts +1 -1
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +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 +16 -11
- package/templates/default/global-hooks/after/mobile.hook.ts +3 -3
- package/templates/default/global-hooks/after/web.hook.ts +3 -3
- package/templates/default/global-hooks/before/mobile.hook.ts +3 -3
- package/templates/default/global-hooks/before/web.hook.ts +3 -3
- 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 +70 -46
- package/templates/default/page-objects/MatchersPage/web.ts +43 -37
- package/templates/default/page-objects/NavigationPage/mobile.ts +14 -14
- package/templates/default/page-objects/NavigationPage/web.ts +8 -8
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +42 -36
- package/templates/default/page-objects/PlaygroundPage/web.ts +31 -31
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -11
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -12
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -27
- 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 +27 -16
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -12
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -24
- 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 +10 -9
- 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 +9 -9
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +17 -12
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
- 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 +19 -12
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +11 -11
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +17 -12
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
- 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 +11 -11
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +17 -12
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +29 -29
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +19 -12
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
- package/templates/default/specs/ElementMatchers/suite.md +10 -9
- 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 +6 -6
- package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +18 -12
- package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +6 -6
- package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +18 -12
- package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
- package/templates/default/spectra.config.ts +52 -68
- package/templates/default/support/actions/fillCredentials/mobile.action.ts +4 -4
- package/templates/default/support/actions/fillCredentials/web.action.ts +4 -4
- 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 +5 -5
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +5 -5
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +6 -6
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +6 -6
- package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db +0 -0
- package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +1577 -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/CLAUDE.md +10 -10
- package/templates/nx/package.json +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -3
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -27
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -3
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -24
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +17 -17
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
- 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 +3 -3
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -3
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
- package/templates/nx/pnpm-lock.yaml +6663 -6663
- package/templates/nx/pnpm-workspace.yaml +9 -9
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +33 -33
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +91 -37
- package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +5 -5
- package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +8 -8
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +26 -26
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +31 -31
- package/templates/nx/shared/testing/project.json +19 -19
- package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +4 -4
- package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +4 -4
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +3 -3
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +5 -5
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +4 -4
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +6 -6
- package/templates/nx/spectra.config.ts +52 -68
- package/templates/react-component/.cursorrules +17 -0
- package/templates/react-component/.vscode/extensions.json +5 -0
- package/templates/react-component/CLAUDE.md +14 -0
- package/templates/react-component/README.md +69 -0
- package/templates/react-component/fixtures/component-mock.json +12 -0
- package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
- package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
- package/templates/react-component/package.json +27 -0
- package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
- package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
- package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
- package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
- package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
- package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
- package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
- package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
- package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
- package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
- package/templates/react-component/specs/InputComponent/suite.md +12 -0
- package/templates/react-component/spectra.config.ts +27 -0
- package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
- package/templates/react-component/src/components/Button/Button.tsx +57 -0
- package/templates/react-component/src/components/Input/Input.tsx +41 -0
- package/templates/react-component/src/test-utils.tsx +23 -0
- package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
- package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
- package/templates/react-component/tsconfig.json +8 -0
- package/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-DPfv5_EF.js +0 -116
- package/demo-app/assets/index-gIwu2EMV.css +0 -1
- package/dist/commands/__tests__/cloud-run.test.js +0 -93
- package/dist/commands/cloud-run.d.ts +0 -13
- package/dist/commands/cloud-run.js +0 -120
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
- /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface DeclaredSpecStep {
|
|
2
|
+
name: string;
|
|
3
|
+
line: number;
|
|
4
|
+
raw: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ExtractedSymbolCall {
|
|
7
|
+
name: string;
|
|
8
|
+
line: number;
|
|
9
|
+
column: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ExtractedCodeSymbols {
|
|
12
|
+
stepCalls: ExtractedSymbolCall[];
|
|
13
|
+
itBlocks: Array<{
|
|
14
|
+
title: string;
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
}>;
|
|
18
|
+
testBlocks: Array<{
|
|
19
|
+
title: string;
|
|
20
|
+
line: number;
|
|
21
|
+
column: number;
|
|
22
|
+
}>;
|
|
23
|
+
describeBlocks: Array<{
|
|
24
|
+
title: string;
|
|
25
|
+
line: number;
|
|
26
|
+
column: number;
|
|
27
|
+
}>;
|
|
28
|
+
importStatements: Array<{
|
|
29
|
+
line: number;
|
|
30
|
+
column: number;
|
|
31
|
+
specifier: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extracts @step:<name> tokens from the "## Test Steps" section of spec.md.
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractDeclaredStepsFromSpec(body: string, bodyStartLine: number): DeclaredSpecStep[];
|
|
38
|
+
/**
|
|
39
|
+
* Parses a TypeScript test file into an AST and extracts Step.* calls,
|
|
40
|
+
* it() test blocks, describe() suite blocks, and import statements.
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractSymbolsFromTestCode(code: string, filePath: string): ExtractedCodeSymbols;
|
|
43
|
+
/**
|
|
44
|
+
* Determines whether an it("...") test title aligns with the spec.md title.
|
|
45
|
+
* In TestSpectra, the it() argument represents the complete test case workflow
|
|
46
|
+
* and directly corresponds to the title declared in spec.md.
|
|
47
|
+
*/
|
|
48
|
+
export declare function areTitlesAligned(specTitle: string, testTitle: string): boolean;
|
|
49
|
+
export interface ModuleExportResult {
|
|
50
|
+
hasAnonymousAsyncDefaultExport: boolean;
|
|
51
|
+
hasNamedExports: boolean;
|
|
52
|
+
namedExportLines: Array<{
|
|
53
|
+
line: number;
|
|
54
|
+
column: number;
|
|
55
|
+
name: string;
|
|
56
|
+
}>;
|
|
57
|
+
testBlocks: Array<{
|
|
58
|
+
line: number;
|
|
59
|
+
column: number;
|
|
60
|
+
name: string;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parses a shared module (.step.ts, .action.ts, .hook.ts) into AST
|
|
65
|
+
* and checks for anonymous async default export compliance.
|
|
66
|
+
*/
|
|
67
|
+
export declare function extractModuleExports(code: string, filePath: string): ModuleExportResult;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts @step:<name> tokens from the "## Test Steps" section of spec.md.
|
|
4
|
+
*/
|
|
5
|
+
export function extractDeclaredStepsFromSpec(body, bodyStartLine) {
|
|
6
|
+
const steps = [];
|
|
7
|
+
const lines = body.split('\n');
|
|
8
|
+
let inStepsSection = false;
|
|
9
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10
|
+
const lineNum = bodyStartLine + i;
|
|
11
|
+
const rawLine = lines[i];
|
|
12
|
+
const trimmed = rawLine.trim();
|
|
13
|
+
if (trimmed.startsWith('## ')) {
|
|
14
|
+
const heading = trimmed
|
|
15
|
+
.replace(/^##\s+/, '')
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
if (heading === 'test steps') {
|
|
19
|
+
inStepsSection = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
else if (inStepsSection) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (inStepsSection) {
|
|
27
|
+
const stepRegex = /@step:([a-zA-Z0-9_-]+)/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = stepRegex.exec(rawLine)) !== null) {
|
|
30
|
+
steps.push({
|
|
31
|
+
name: match[1],
|
|
32
|
+
line: lineNum,
|
|
33
|
+
raw: match[0],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return steps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parses a TypeScript test file into an AST and extracts Step.* calls,
|
|
42
|
+
* it() test blocks, describe() suite blocks, and import statements.
|
|
43
|
+
*/
|
|
44
|
+
export function extractSymbolsFromTestCode(code, filePath) {
|
|
45
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
46
|
+
const stepCalls = [];
|
|
47
|
+
const itBlocks = [];
|
|
48
|
+
const testBlocks = [];
|
|
49
|
+
const describeBlocks = [];
|
|
50
|
+
const importStatements = [];
|
|
51
|
+
function visit(node) {
|
|
52
|
+
// 1. Detect manual import statements (TS-LINT-006)
|
|
53
|
+
if (ts.isImportDeclaration(node)) {
|
|
54
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
55
|
+
const specifier = ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : '';
|
|
56
|
+
importStatements.push({
|
|
57
|
+
line: line + 1,
|
|
58
|
+
column: character + 1,
|
|
59
|
+
specifier,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else if (ts.isImportEqualsDeclaration(node)) {
|
|
63
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
64
|
+
importStatements.push({
|
|
65
|
+
line: line + 1,
|
|
66
|
+
column: character + 1,
|
|
67
|
+
specifier: '',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// 2. Detect it(...) / test(...) [forbidden] and describe(...) / suite(...) call expressions (TS-LINT-007, TS-LINT-008, TS-LINT-005)
|
|
71
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
72
|
+
const fnName = node.expression.text;
|
|
73
|
+
if (fnName === 'it') {
|
|
74
|
+
let title = '';
|
|
75
|
+
if (node.arguments.length > 0) {
|
|
76
|
+
const firstArg = node.arguments[0];
|
|
77
|
+
if (ts.isStringLiteral(firstArg) || ts.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
78
|
+
title = firstArg.text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
82
|
+
itBlocks.push({
|
|
83
|
+
title,
|
|
84
|
+
line: line + 1,
|
|
85
|
+
column: character + 1,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (fnName === 'test') {
|
|
89
|
+
let title = '';
|
|
90
|
+
if (node.arguments.length > 0) {
|
|
91
|
+
const firstArg = node.arguments[0];
|
|
92
|
+
if (ts.isStringLiteral(firstArg) || ts.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
93
|
+
title = firstArg.text;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
97
|
+
testBlocks.push({
|
|
98
|
+
title,
|
|
99
|
+
line: line + 1,
|
|
100
|
+
column: character + 1,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else if (fnName === 'describe' || fnName === 'suite') {
|
|
104
|
+
let title = '';
|
|
105
|
+
if (node.arguments.length > 0) {
|
|
106
|
+
const firstArg = node.arguments[0];
|
|
107
|
+
if (ts.isStringLiteral(firstArg) || ts.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
108
|
+
title = firstArg.text;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
112
|
+
describeBlocks.push({
|
|
113
|
+
title,
|
|
114
|
+
line: line + 1,
|
|
115
|
+
column: character + 1,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 3. Detect Step.<name>() calls
|
|
120
|
+
if (ts.isCallExpression(node)) {
|
|
121
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
122
|
+
const propAccess = node.expression;
|
|
123
|
+
if (ts.isIdentifier(propAccess.expression) && propAccess.expression.text === 'Step') {
|
|
124
|
+
const stepName = propAccess.name.text;
|
|
125
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
126
|
+
stepCalls.push({
|
|
127
|
+
name: stepName,
|
|
128
|
+
line: line + 1,
|
|
129
|
+
column: character + 1,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
ts.forEachChild(node, visit);
|
|
135
|
+
}
|
|
136
|
+
visit(sourceFile);
|
|
137
|
+
return {
|
|
138
|
+
stepCalls,
|
|
139
|
+
itBlocks,
|
|
140
|
+
testBlocks,
|
|
141
|
+
describeBlocks,
|
|
142
|
+
importStatements,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Determines whether an it("...") test title aligns with the spec.md title.
|
|
147
|
+
* In TestSpectra, the it() argument represents the complete test case workflow
|
|
148
|
+
* and directly corresponds to the title declared in spec.md.
|
|
149
|
+
*/
|
|
150
|
+
export function areTitlesAligned(specTitle, testTitle) {
|
|
151
|
+
if (!specTitle || !testTitle)
|
|
152
|
+
return true;
|
|
153
|
+
return specTitle.trim().toLowerCase() === testTitle.trim().toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
function hasModifier(node, kind) {
|
|
156
|
+
return ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === kind) === true;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Parses a shared module (.step.ts, .action.ts, .hook.ts) into AST
|
|
160
|
+
* and checks for anonymous async default export compliance.
|
|
161
|
+
*/
|
|
162
|
+
export function extractModuleExports(code, filePath) {
|
|
163
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
164
|
+
let hasAnonymousAsyncDefaultExport = false;
|
|
165
|
+
const namedExportLines = [];
|
|
166
|
+
const testBlocks = [];
|
|
167
|
+
function visit(node) {
|
|
168
|
+
// Check for export default async function () {} (FunctionDeclaration with DefaultKeyword)
|
|
169
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
170
|
+
const hasExport = hasModifier(node, ts.SyntaxKind.ExportKeyword);
|
|
171
|
+
const hasDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
|
|
172
|
+
const hasAsync = hasModifier(node, ts.SyntaxKind.AsyncKeyword);
|
|
173
|
+
if (hasExport && hasDefault && hasAsync) {
|
|
174
|
+
if (!node.name) {
|
|
175
|
+
hasAnonymousAsyncDefaultExport = true;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Named function used with export default: export default async function foo() {}
|
|
179
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
180
|
+
const name = node.name.text;
|
|
181
|
+
namedExportLines.push({ line: line + 1, column: character + 1, name });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (hasExport && !hasDefault) {
|
|
185
|
+
// export function foo() {} — named export (forbidden)
|
|
186
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
187
|
+
const name = node.name?.text || 'default';
|
|
188
|
+
namedExportLines.push({ line: line + 1, column: character + 1, name });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Check for export default async () => {} or export default async function () {} (ExportAssignment)
|
|
192
|
+
if (ts.isExportAssignment(node)) {
|
|
193
|
+
const expr = node.expression;
|
|
194
|
+
if (ts.isArrowFunction(expr)) {
|
|
195
|
+
const hasAsync = hasModifier(expr, ts.SyntaxKind.AsyncKeyword);
|
|
196
|
+
if (hasAsync) {
|
|
197
|
+
hasAnonymousAsyncDefaultExport = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (ts.isFunctionExpression(expr)) {
|
|
201
|
+
const hasAsync = hasModifier(expr, ts.SyntaxKind.AsyncKeyword);
|
|
202
|
+
if (hasAsync && !expr.name) {
|
|
203
|
+
hasAnonymousAsyncDefaultExport = true;
|
|
204
|
+
}
|
|
205
|
+
else if (hasAsync && expr.name) {
|
|
206
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(expr.getStart());
|
|
207
|
+
namedExportLines.push({ line: line + 1, column: character + 1, name: expr.name.text });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Check for export class (named exports)
|
|
212
|
+
if (ts.isClassDeclaration(node)) {
|
|
213
|
+
const hasExport = hasModifier(node, ts.SyntaxKind.ExportKeyword);
|
|
214
|
+
if (hasExport) {
|
|
215
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
216
|
+
const name = node.name?.text || 'default';
|
|
217
|
+
namedExportLines.push({ line: line + 1, column: character + 1, name });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Check for export { ... } or export * from '...' (named exports)
|
|
221
|
+
if (ts.isExportDeclaration(node)) {
|
|
222
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
223
|
+
const exportName = node.exportClause && ts.isNamedExports(node.exportClause)
|
|
224
|
+
? node.exportClause.elements.map((e) => e.name.text).join(', ')
|
|
225
|
+
: '*';
|
|
226
|
+
namedExportLines.push({ line: line + 1, column: character + 1, name: exportName });
|
|
227
|
+
}
|
|
228
|
+
// Check for forbidden test blocks
|
|
229
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
230
|
+
const fnName = node.expression.text;
|
|
231
|
+
if (fnName === 'it' || fnName === 'test' || fnName === 'describe') {
|
|
232
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
233
|
+
testBlocks.push({ line: line + 1, column: character + 1, name: fnName });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
ts.forEachChild(node, visit);
|
|
237
|
+
}
|
|
238
|
+
visit(sourceFile);
|
|
239
|
+
return {
|
|
240
|
+
hasAnonymousAsyncDefaultExport,
|
|
241
|
+
hasNamedExports: namedExportLines.length > 0,
|
|
242
|
+
namedExportLines,
|
|
243
|
+
testBlocks,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LintDiagnostic } from './types.js';
|
|
2
|
+
export interface ParsedFrontmatterResult<T = Record<string, unknown>> {
|
|
3
|
+
data: T | null;
|
|
4
|
+
raw: string;
|
|
5
|
+
hasFrontmatter: boolean;
|
|
6
|
+
startLine: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
bodyStartLine: number;
|
|
9
|
+
diagnostics: LintDiagnostic[];
|
|
10
|
+
keyLineMap: Map<string, number>;
|
|
11
|
+
body: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scans lines within a range to locate YAML frontmatter properties before any Markdown heading
|
|
15
|
+
* or code fence. Returns the 0-indexed line range [first, last] or null if no properties found.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findPropertyRange(lines: string[], startIdx?: number, maxIdx?: number): {
|
|
18
|
+
first: number;
|
|
19
|
+
last: number;
|
|
20
|
+
} | null;
|
|
21
|
+
/**
|
|
22
|
+
* Extracts and parses YAML frontmatter from a Markdown document with line mapping.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseMarkdownFrontmatter<T = Record<string, unknown>>(content: string, filePath: string): ParsedFrontmatterResult<T>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
/**
|
|
3
|
+
* Scans lines within a range to locate YAML frontmatter properties before any Markdown heading
|
|
4
|
+
* or code fence. Returns the 0-indexed line range [first, last] or null if no properties found.
|
|
5
|
+
*/
|
|
6
|
+
export function findPropertyRange(lines, startIdx = 0, maxIdx = lines.length) {
|
|
7
|
+
let first = -1;
|
|
8
|
+
let last = -1;
|
|
9
|
+
for (let i = startIdx; i < maxIdx; i++) {
|
|
10
|
+
const raw = lines[i];
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
if (first !== -1) {
|
|
14
|
+
let hasMorePropsAhead = false;
|
|
15
|
+
for (let j = i + 1; j < maxIdx; j++) {
|
|
16
|
+
const nextTrimmed = lines[j].trim();
|
|
17
|
+
if (!nextTrimmed)
|
|
18
|
+
continue;
|
|
19
|
+
if (nextTrimmed.startsWith('#') || nextTrimmed === '---' || nextTrimmed.startsWith('```')) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
if (/^[a-zA-Z0-9_-]+\s*:/.test(nextTrimmed)) {
|
|
23
|
+
hasMorePropsAhead = true;
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
if (!hasMorePropsAhead) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (trimmed.startsWith('#') || trimmed === '---' || trimmed.startsWith('```')) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (/^[a-zA-Z0-9_-]+\s*:/.test(trimmed) || (first !== -1 && (trimmed.startsWith('- ') || /^\s+/.test(raw)))) {
|
|
37
|
+
if (first === -1) {
|
|
38
|
+
first = i;
|
|
39
|
+
}
|
|
40
|
+
last = i;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return first !== -1 ? { first, last } : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extracts and parses YAML frontmatter from a Markdown document with line mapping.
|
|
50
|
+
*/
|
|
51
|
+
export function parseMarkdownFrontmatter(content, filePath) {
|
|
52
|
+
const diagnostics = [];
|
|
53
|
+
const keyLineMap = new Map();
|
|
54
|
+
// Normalize line endings
|
|
55
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
56
|
+
const lines = normalized.split('\n');
|
|
57
|
+
if (lines.length === 0 || lines[0].trim() !== '---') {
|
|
58
|
+
// Check if there is a closing delimiter somewhere in the file
|
|
59
|
+
let closingIndex = -1;
|
|
60
|
+
for (let i = 1; i < lines.length; i++) {
|
|
61
|
+
if (lines[i].trim() === '---') {
|
|
62
|
+
closingIndex = i;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (closingIndex !== -1) {
|
|
67
|
+
// Case 1: Opener delimiter is missing, but closing delimiter exists
|
|
68
|
+
return {
|
|
69
|
+
data: null,
|
|
70
|
+
raw: '',
|
|
71
|
+
hasFrontmatter: false,
|
|
72
|
+
startLine: 1,
|
|
73
|
+
endLine: closingIndex + 1,
|
|
74
|
+
bodyStartLine: closingIndex + 2,
|
|
75
|
+
diagnostics: [
|
|
76
|
+
{
|
|
77
|
+
code: 'testspectra/missing-frontmatter',
|
|
78
|
+
severity: 'error',
|
|
79
|
+
message: 'Missing opening YAML frontmatter delimiter (---) at line 1',
|
|
80
|
+
filePath,
|
|
81
|
+
line: 1,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
keyLineMap,
|
|
85
|
+
body: lines.slice(closingIndex + 1).join('\n'),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Neither opener nor closing delimiter exists
|
|
89
|
+
const propRange = findPropertyRange(lines, 0);
|
|
90
|
+
if (propRange) {
|
|
91
|
+
// Case 3: No delimiters at all, but frontmatter properties exist
|
|
92
|
+
return {
|
|
93
|
+
data: null,
|
|
94
|
+
raw: '',
|
|
95
|
+
hasFrontmatter: false,
|
|
96
|
+
startLine: 1,
|
|
97
|
+
endLine: propRange.last + 1,
|
|
98
|
+
bodyStartLine: propRange.last + 2,
|
|
99
|
+
diagnostics: [
|
|
100
|
+
{
|
|
101
|
+
code: 'testspectra/missing-frontmatter',
|
|
102
|
+
severity: 'error',
|
|
103
|
+
message: 'Missing YAML frontmatter delimiters (---) at line 1',
|
|
104
|
+
filePath,
|
|
105
|
+
line: 1,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
keyLineMap,
|
|
109
|
+
body: normalized,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Case 4: Delimiters missing and properties missing
|
|
113
|
+
return {
|
|
114
|
+
data: null,
|
|
115
|
+
raw: '',
|
|
116
|
+
hasFrontmatter: false,
|
|
117
|
+
startLine: 1,
|
|
118
|
+
endLine: 1,
|
|
119
|
+
bodyStartLine: 1,
|
|
120
|
+
diagnostics: [
|
|
121
|
+
{
|
|
122
|
+
code: 'testspectra/missing-frontmatter',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
message: 'Missing YAML frontmatter delimiter (---) at line 1',
|
|
125
|
+
filePath,
|
|
126
|
+
line: 1,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
keyLineMap,
|
|
130
|
+
body: normalized,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
let closingIndex = -1;
|
|
134
|
+
for (let i = 1; i < lines.length; i++) {
|
|
135
|
+
if (lines[i].trim() === '---') {
|
|
136
|
+
closingIndex = i;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (closingIndex === -1) {
|
|
141
|
+
// Case 2: Opening delimiter exists at line 1, but closing delimiter is missing
|
|
142
|
+
return {
|
|
143
|
+
data: null,
|
|
144
|
+
raw: '',
|
|
145
|
+
hasFrontmatter: false,
|
|
146
|
+
startLine: 1,
|
|
147
|
+
endLine: lines.length,
|
|
148
|
+
bodyStartLine: lines.length,
|
|
149
|
+
diagnostics: [
|
|
150
|
+
{
|
|
151
|
+
code: 'testspectra/missing-frontmatter',
|
|
152
|
+
severity: 'error',
|
|
153
|
+
message: 'Missing closing YAML frontmatter delimiter (---). Expected closing "---"',
|
|
154
|
+
filePath,
|
|
155
|
+
line: 1,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
keyLineMap,
|
|
159
|
+
body: '',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const frontmatterLines = lines.slice(1, closingIndex);
|
|
163
|
+
const rawFrontmatter = frontmatterLines.join('\n');
|
|
164
|
+
const body = lines.slice(closingIndex + 1).join('\n');
|
|
165
|
+
const lineCounter = new YAML.LineCounter();
|
|
166
|
+
const doc = YAML.parseDocument(rawFrontmatter, { lineCounter, keepSourceTokens: true });
|
|
167
|
+
if (doc.errors && doc.errors.length > 0) {
|
|
168
|
+
for (const err of doc.errors) {
|
|
169
|
+
const pos = err.linePos ? err.linePos[0] : { line: 1, col: 1 };
|
|
170
|
+
diagnostics.push({
|
|
171
|
+
code: 'testspectra/parse-error',
|
|
172
|
+
severity: 'error',
|
|
173
|
+
message: `Malformed YAML syntax in frontmatter: ${err.message}`,
|
|
174
|
+
filePath,
|
|
175
|
+
line: 1 + pos.line, // 1 for the leading '---'
|
|
176
|
+
column: pos.col,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
data: null,
|
|
181
|
+
raw: rawFrontmatter,
|
|
182
|
+
hasFrontmatter: true,
|
|
183
|
+
startLine: 1,
|
|
184
|
+
endLine: closingIndex + 1,
|
|
185
|
+
bodyStartLine: closingIndex + 2,
|
|
186
|
+
diagnostics,
|
|
187
|
+
keyLineMap,
|
|
188
|
+
body,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Populate keyLineMap for exact line numbers
|
|
192
|
+
if (YAML.isMap(doc.contents)) {
|
|
193
|
+
for (const item of doc.contents.items) {
|
|
194
|
+
if (item && item.key) {
|
|
195
|
+
const keyName = String(item.key);
|
|
196
|
+
if (item.key.range) {
|
|
197
|
+
const pos = lineCounter.linePos(item.key.range[0]);
|
|
198
|
+
// 1 (for opening delimiter line) + linePos.line
|
|
199
|
+
keyLineMap.set(keyName, 1 + pos.line);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
keyLineMap.set(keyName, 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const data = doc.toJSON();
|
|
208
|
+
return {
|
|
209
|
+
data,
|
|
210
|
+
raw: rawFrontmatter,
|
|
211
|
+
hasFrontmatter: true,
|
|
212
|
+
startLine: 1,
|
|
213
|
+
endLine: closingIndex + 1,
|
|
214
|
+
bodyStartLine: closingIndex + 2,
|
|
215
|
+
diagnostics,
|
|
216
|
+
keyLineMap,
|
|
217
|
+
body,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { LintDiagnostic } from '../types.js';
|
|
3
|
+
export declare const PRIORITY_VALUES: readonly ["Critical", "High", "Medium", "Low"];
|
|
4
|
+
export declare const CASE_TYPE_VALUES: readonly ["Positive", "Negative", "Edge"];
|
|
5
|
+
export declare const STATUS_VALUES: readonly ["draft", "ready-for-automation", "automated", "deprecated"];
|
|
6
|
+
export declare const COVERAGE_STATUS_VALUES: readonly ["automated", "manual", "unsupported"];
|
|
7
|
+
export declare const PriorityEnum: z.ZodEnum<["Critical", "High", "Medium", "Low"]>;
|
|
8
|
+
export declare const CaseTypeEnum: z.ZodEnum<["Positive", "Negative", "Edge"]>;
|
|
9
|
+
export declare const StatusEnum: z.ZodEnum<["draft", "ready-for-automation", "automated", "deprecated"]>;
|
|
10
|
+
export declare const CoverageSchema: z.ZodObject<{
|
|
11
|
+
web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
12
|
+
mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
15
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
18
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const SpecFrontmatterSchema: z.ZodObject<{
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
title: z.ZodString;
|
|
23
|
+
priority: z.ZodEnum<["Critical", "High", "Medium", "Low"]>;
|
|
24
|
+
caseType: z.ZodEnum<["Positive", "Negative", "Edge"]>;
|
|
25
|
+
status: z.ZodEnum<["draft", "ready-for-automation", "automated", "deprecated"]>;
|
|
26
|
+
coverage: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
28
|
+
mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
|
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
31
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
34
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
35
|
+
}>>;
|
|
36
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
id: string;
|
|
39
|
+
status: "draft" | "ready-for-automation" | "automated" | "deprecated";
|
|
40
|
+
title: string;
|
|
41
|
+
priority: "Critical" | "High" | "Medium" | "Low";
|
|
42
|
+
caseType: "Positive" | "Negative" | "Edge";
|
|
43
|
+
coverage?: {
|
|
44
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
45
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
46
|
+
} | undefined;
|
|
47
|
+
tags?: string[] | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
id: string;
|
|
50
|
+
status: "draft" | "ready-for-automation" | "automated" | "deprecated";
|
|
51
|
+
title: string;
|
|
52
|
+
priority: "Critical" | "High" | "Medium" | "Low";
|
|
53
|
+
caseType: "Positive" | "Negative" | "Edge";
|
|
54
|
+
coverage?: {
|
|
55
|
+
web?: "automated" | "manual" | "unsupported" | undefined;
|
|
56
|
+
mobile?: "automated" | "manual" | "unsupported" | undefined;
|
|
57
|
+
} | undefined;
|
|
58
|
+
tags?: string[] | undefined;
|
|
59
|
+
}>;
|
|
60
|
+
export type SpecFrontmatter = z.infer<typeof SpecFrontmatterSchema>;
|
|
61
|
+
export interface SectionDefinition {
|
|
62
|
+
name: string;
|
|
63
|
+
level: number;
|
|
64
|
+
required: boolean;
|
|
65
|
+
headerPattern: RegExp;
|
|
66
|
+
description: string;
|
|
67
|
+
}
|
|
68
|
+
export declare const REQUIRED_SPEC_SECTIONS: SectionDefinition[];
|
|
69
|
+
export declare const STANDARD_SPEC_SECTIONS: SectionDefinition[];
|
|
70
|
+
export interface ParsedMarkdownHeader {
|
|
71
|
+
level: number;
|
|
72
|
+
text: string;
|
|
73
|
+
line: number;
|
|
74
|
+
raw: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extracts markdown headers from document body.
|
|
78
|
+
*/
|
|
79
|
+
export declare function extractMarkdownHeaders(body: string, startLine: number): ParsedMarkdownHeader[];
|
|
80
|
+
/**
|
|
81
|
+
* Validates markdown body sections against spec rules.
|
|
82
|
+
*/
|
|
83
|
+
export declare function validateSpecSections(body: string, filePath: string, bodyStartLine: number): LintDiagnostic[];
|