@testspectra/cli 1.0.70 → 1.1.0-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/LICENSE.md +48 -0
- package/README.md +36 -270
- package/bin/.testspectra-runner.hash +1 -0
- package/bin/spectra.js +1 -1
- package/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-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 -0
- package/dist/commands/__tests__/doctor.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor.test.js +39 -0
- package/dist/commands/__tests__/init.test.d.ts +1 -0
- package/dist/commands/__tests__/init.test.js +148 -0
- package/dist/commands/__tests__/license.test.d.ts +1 -0
- package/dist/commands/__tests__/license.test.js +13 -0
- package/dist/commands/__tests__/refactor.test.d.ts +1 -0
- package/dist/commands/__tests__/refactor.test.js +64 -0
- package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
- package/dist/commands/__tests__/run-e2e.test.js +422 -0
- package/dist/commands/__tests__/run.test.d.ts +1 -0
- package/dist/commands/__tests__/run.test.js +273 -0
- package/dist/commands/__tests__/watch.test.d.ts +1 -0
- package/dist/commands/__tests__/watch.test.js +155 -0
- package/dist/commands/add.js +86 -85
- package/dist/commands/demo.js +21 -11
- package/dist/commands/devices.d.ts +14 -2
- package/dist/commands/devices.js +44 -34
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.js +43 -264
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +211 -196
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +59 -0
- package/dist/commands/license.d.ts +4 -0
- package/dist/commands/license.js +30 -0
- package/dist/commands/lint.d.ts +5 -0
- package/dist/commands/lint.js +55 -0
- package/dist/commands/refactor.d.ts +9 -0
- package/dist/commands/refactor.js +172 -0
- package/dist/commands/run.d.ts +23 -2
- package/dist/commands/run.js +322 -111
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.js +28 -0
- package/dist/commands/sync-types.d.ts +4 -0
- package/dist/commands/sync-types.js +15 -0
- package/dist/commands/test.d.ts +17 -0
- package/dist/commands/test.js +76 -0
- package/dist/commands/update.js +39 -43
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +55 -45
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +40 -25
- package/dist/config/schema.d.ts +104 -145
- package/dist/config/schema.js +36 -45
- package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
- package/dist/generator/__tests__/tsconfig-isolation.test.js +282 -0
- package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
- package/dist/generator/__tests__/type-generator.test.js +386 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/tsconfig-generator.d.ts +27 -0
- package/dist/generator/tsconfig-generator.js +419 -0
- package/dist/generator/type-generator.d.ts +59 -0
- package/dist/generator/type-generator.js +740 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +165 -51
- package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
- package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
- package/dist/lifecycle/hook-discovery.d.ts +24 -0
- package/dist/lifecycle/hook-discovery.js +60 -0
- package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
- package/dist/lifecycle/hook-dispatcher.js +190 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/parallel-grouping.d.ts +27 -0
- package/dist/lifecycle/parallel-grouping.js +151 -0
- package/dist/linter/__tests__/anonymous-export.test.d.ts +1 -0
- 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/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
- package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
- package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
- package/dist/mobile/__tests__/device-pool.test.js +97 -0
- package/dist/mobile/device-pool.d.ts +23 -0
- package/dist/mobile/device-pool.js +1 -0
- package/dist/mobile/index.d.ts +1 -0
- package/dist/mobile/index.js +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.js +873 -0
- package/dist/refactor/index.d.ts +1 -0
- package/dist/refactor/index.js +1 -0
- package/dist/refactor/refactor-engine.d.ts +50 -0
- package/dist/refactor/refactor-engine.js +587 -0
- package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
- package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
- package/dist/reporter/semantic-formatter.d.ts +6 -0
- package/dist/reporter/semantic-formatter.js +206 -0
- package/dist/reporter/summary-view.d.ts +12 -0
- package/dist/reporter/summary-view.js +19 -0
- package/dist/reporter/terminal-stream.d.ts +32 -0
- package/dist/reporter/terminal-stream.js +82 -0
- package/dist/reporter/types.d.ts +2 -0
- package/dist/reporter/types.js +1 -0
- package/dist/runner/bridge.d.ts +10 -5
- package/dist/runner/bridge.js +101 -45
- package/dist/runner/reporter.d.ts +32 -2
- package/dist/runner/reporter.js +427 -13
- package/dist/server/demo.js +43 -44
- package/dist/types/generator.d.ts +1 -17
- package/dist/types/generator.js +1 -443
- package/dist/utils/__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 +46 -36
- package/package.json +25 -13
- package/templates/default/CLAUDE.md +12 -5
- package/templates/default/fixtures/apiData.json +24 -0
- package/templates/default/package.json +12 -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 +66 -17
- package/templates/default/page-objects/MatchersPage/web.ts +22 -2
- package/templates/default/page-objects/NavigationPage/mobile.ts +11 -2
- package/templates/default/page-objects/NavigationPage/web.ts +1 -1
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +34 -18
- package/templates/default/page-objects/PlaygroundPage/web.ts +4 -2
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -0
- 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 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -0
- 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 -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 +7 -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 +17 -3
- 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 +9 -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 +9 -4
- 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 +9 -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 +9 -3
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
- 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 +4 -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 +4 -4
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -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 +3 -3
- package/templates/default/spectra.config.ts +22 -31
- 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 +3 -1
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +3 -3
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +4 -2
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +4 -4
- package/templates/default/tsconfig.json +3 -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 +5366 -0
- 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 +2 -2
- package/templates/nx/package.json +6 -4
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +15 -5
- 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 +9 -4
- 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 +9 -3
- 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 +27 -6
- 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 +4 -4
- 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 +3 -3
- package/templates/nx/pnpm-lock.yaml +6663 -0
- package/templates/nx/pnpm-workspace.yaml +2 -1
- package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +13 -1
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +85 -17
- 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 +4 -2
- package/templates/nx/shared/testing/project.json +19 -0
- 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 +3 -3
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +2 -2
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +4 -4
- package/templates/nx/spectra.config.ts +20 -29
- package/templates/nx/tsconfig.json +3 -1
- 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-DAUlrHTr.css +0 -1
- package/demo-app/assets/index-DN2Kb0A8.js +0 -100
- package/dist/index.cjs +0 -101
- package/dist/plugin.cjs +0 -575
- package/dist/plugin.d.ts +0 -16
- package/dist/plugin.js +0 -184
- package/dist/step/index.d.ts +0 -6
- package/dist/step/index.js +0 -6
- package/dist/step/matchers.d.ts +0 -14
- package/dist/step/matchers.js +0 -114
- package/dist/step/proto.d.ts +0 -46
- package/dist/step/proto.js +0 -114
- package/dist/step/runner/collection.d.ts +0 -13
- package/dist/step/runner/collection.js +0 -51
- package/dist/step/runner/single.d.ts +0 -21
- package/dist/step/runner/single.js +0 -138
- package/dist/step/spectra.d.ts +0 -34
- package/dist/step/spectra.js +0 -156
- package/dist/step/types.d.ts +0 -32
- package/dist/step/types.js +0 -5
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
- package/templates/default/tsconfig.spectra.android.json +0 -46
- package/templates/default/tsconfig.spectra.ios.json +0 -46
- package/templates/default/tsconfig.spectra.json +0 -22
- package/templates/default/tsconfig.spectra.web.json +0 -53
- package/templates/nx/fixtures/playgroundData.json +0 -7
- package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
- package/templates/nx/global-hooks/after/web.hook.ts +0 -3
- package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
- package/templates/nx/global-hooks/before/web.hook.ts +0 -3
- package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
- package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
- package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
- package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
- package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
- package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
- package/templates/nx/specs/BrowserContext/suite.md +0 -9
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
- package/templates/nx/specs/ContentMatchers/suite.md +0 -9
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
- package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/suite.md +0 -9
- package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
- package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
- package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
- package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/suite.md +0 -9
- package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
- package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
- package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
- package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
- package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
- package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
- package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
- package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
- package/templates/nx/tsconfig.spectra.android.json +0 -39
- package/templates/nx/tsconfig.spectra.ios.json +0 -39
- package/templates/nx/tsconfig.spectra.json +0 -25
- package/templates/nx/tsconfig.spectra.shared.json +0 -25
- package/templates/nx/tsconfig.spectra.web.json +0 -45
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ZodError } from 'zod';
|
|
4
|
+
import { areTitlesAligned, extractDeclaredStepsFromSpec, extractModuleExports, extractSymbolsFromTestCode, } from './extractor.js';
|
|
5
|
+
import { isInsideSharedLibrary } from './discovery.js';
|
|
6
|
+
import { parseMarkdownFrontmatter } from './parser.js';
|
|
7
|
+
import { SpecFrontmatterSchema, validateSpecSections } from './schemas/spec.schema.js';
|
|
8
|
+
import { SuiteFrontmatterSchema, validateSuiteSections } from './schemas/suite.schema.js';
|
|
9
|
+
/**
|
|
10
|
+
* Resolves an import specifier against the importing file's directory and checks whether it
|
|
11
|
+
* reaches into an ambient-global-backed directory (Page/Component Objects, Steps, Fixtures) —
|
|
12
|
+
* the only imports still forbidden in `.tsx` component test files. Everything else (React,
|
|
13
|
+
* component source under test, npm packages, test-utils helpers) is allowed in `.tsx`, unlike
|
|
14
|
+
* the fully zero-import `.ts` E2E spec contract.
|
|
15
|
+
*
|
|
16
|
+
* See docs/v2/features/component-testing/react/mount-convention.md §"Static Imports Are Allowed
|
|
17
|
+
* Here" for the rationale.
|
|
18
|
+
*/
|
|
19
|
+
function resolvesToAmbientGlobalDir(specifier, testFilePath) {
|
|
20
|
+
if (!specifier.startsWith('.'))
|
|
21
|
+
return false; // bare package specifiers (react, etc.) are fine
|
|
22
|
+
const resolved = path.normalize(path.join(path.dirname(testFilePath), specifier));
|
|
23
|
+
const segments = resolved.split(path.sep);
|
|
24
|
+
return (segments.includes('page-objects') ||
|
|
25
|
+
segments.includes('fixtures') ||
|
|
26
|
+
(segments.includes('support') && segments.includes('steps')));
|
|
27
|
+
}
|
|
28
|
+
export class SpecLinter {
|
|
29
|
+
/**
|
|
30
|
+
* Validates a single spec.md file.
|
|
31
|
+
*/
|
|
32
|
+
static validateSpecFile(filePath, contentOverride, companionCodeOverrides) {
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
if (contentOverride === undefined && !fs.existsSync(filePath)) {
|
|
35
|
+
return {
|
|
36
|
+
diagnostics: [
|
|
37
|
+
{
|
|
38
|
+
code: 'testspectra/parse-error',
|
|
39
|
+
severity: 'error',
|
|
40
|
+
message: `File does not exist: ${filePath}`,
|
|
41
|
+
filePath,
|
|
42
|
+
line: 1,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const content = contentOverride !== undefined ? contentOverride : fs.readFileSync(filePath, 'utf-8');
|
|
48
|
+
const parsed = parseMarkdownFrontmatter(content, filePath);
|
|
49
|
+
diagnostics.push(...parsed.diagnostics);
|
|
50
|
+
if (!parsed.hasFrontmatter || !parsed.data) {
|
|
51
|
+
return { diagnostics };
|
|
52
|
+
}
|
|
53
|
+
const data = parsed.data;
|
|
54
|
+
const id = typeof data.id === 'string' ? data.id : undefined;
|
|
55
|
+
const idLine = parsed.keyLineMap.get('id') || 1;
|
|
56
|
+
try {
|
|
57
|
+
SpecFrontmatterSchema.parse(data);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof ZodError) {
|
|
61
|
+
for (const issue of err.issues) {
|
|
62
|
+
const field = issue.path[0] ? String(issue.path[0]) : '';
|
|
63
|
+
const line = parsed.keyLineMap.get(field) || 1;
|
|
64
|
+
if (issue.code === 'invalid_enum_value') {
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
code: 'testspectra/invalid-frontmatter-enum',
|
|
67
|
+
severity: 'error',
|
|
68
|
+
message: issue.message,
|
|
69
|
+
filePath,
|
|
70
|
+
line,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (issue.code === 'invalid_type' &&
|
|
74
|
+
(issue.received === 'undefined' || issue.message.toLowerCase().includes('missing required'))) {
|
|
75
|
+
diagnostics.push({
|
|
76
|
+
code: 'testspectra/missing-frontmatter',
|
|
77
|
+
severity: 'error',
|
|
78
|
+
message: issue.message,
|
|
79
|
+
filePath,
|
|
80
|
+
line: 1,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else if (['priority', 'caseType', 'status'].includes(field)) {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
code: 'testspectra/invalid-frontmatter-enum',
|
|
86
|
+
severity: 'error',
|
|
87
|
+
message: issue.message,
|
|
88
|
+
filePath,
|
|
89
|
+
line,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
diagnostics.push({
|
|
94
|
+
code: 'testspectra/missing-frontmatter',
|
|
95
|
+
severity: 'error',
|
|
96
|
+
message: issue.message,
|
|
97
|
+
filePath,
|
|
98
|
+
line,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
diagnostics.push({
|
|
105
|
+
code: 'testspectra/parse-error',
|
|
106
|
+
severity: 'error',
|
|
107
|
+
message: err.message,
|
|
108
|
+
filePath,
|
|
109
|
+
line: 1,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Validate markdown body sections
|
|
114
|
+
const sectionDiagnostics = validateSpecSections(parsed.body, filePath, parsed.bodyStartLine);
|
|
115
|
+
diagnostics.push(...sectionDiagnostics);
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Stage 3 & 4: AST Step Parity & Test Structure Enforcement
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
119
|
+
const declaredSteps = extractDeclaredStepsFromSpec(parsed.body, parsed.bodyStartLine);
|
|
120
|
+
const specDir = path.dirname(filePath);
|
|
121
|
+
let companionTestFiles = [];
|
|
122
|
+
try {
|
|
123
|
+
const entries = fs.readdirSync(specDir, { withFileTypes: true });
|
|
124
|
+
companionTestFiles = entries
|
|
125
|
+
.filter((e) => e.isFile() &&
|
|
126
|
+
(e.name.endsWith('.test.ts') ||
|
|
127
|
+
e.name.endsWith('.test.tsx') ||
|
|
128
|
+
e.name.endsWith('.web.ts') ||
|
|
129
|
+
e.name.endsWith('.web.tsx') ||
|
|
130
|
+
e.name.endsWith('.mobile.ts') ||
|
|
131
|
+
e.name.endsWith('.mobile.tsx') ||
|
|
132
|
+
e.name.endsWith('.android.ts') ||
|
|
133
|
+
e.name.endsWith('.android.tsx') ||
|
|
134
|
+
e.name.endsWith('.ios.ts') ||
|
|
135
|
+
e.name.endsWith('.ios.tsx') ||
|
|
136
|
+
e.name.endsWith('.common.ts') ||
|
|
137
|
+
e.name.endsWith('.common.tsx')))
|
|
138
|
+
.map((e) => path.join(specDir, e.name));
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
if (companionCodeOverrides) {
|
|
142
|
+
const overrideKeys = companionCodeOverrides instanceof Map
|
|
143
|
+
? Array.from(companionCodeOverrides.keys())
|
|
144
|
+
: Object.keys(companionCodeOverrides);
|
|
145
|
+
for (const overridePath of overrideKeys) {
|
|
146
|
+
if (path.dirname(overridePath) === specDir && !companionTestFiles.includes(overridePath)) {
|
|
147
|
+
companionTestFiles.push(overridePath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (companionTestFiles.length === 0) {
|
|
152
|
+
if (declaredSteps.length > 0) {
|
|
153
|
+
for (const step of declaredSteps) {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
code: 'testspectra/missing-step-call',
|
|
156
|
+
severity: 'error',
|
|
157
|
+
message: `Parity Error: Step "@step:${step.name}" declared in spec.md line ${step.line} is missing in test code (no companion test files found)`,
|
|
158
|
+
filePath,
|
|
159
|
+
line: step.line,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const coverage = data.coverage;
|
|
166
|
+
for (const testFilePath of companionTestFiles) {
|
|
167
|
+
const testFileName = path.basename(testFilePath);
|
|
168
|
+
let testCode = '';
|
|
169
|
+
if (companionCodeOverrides) {
|
|
170
|
+
if (companionCodeOverrides instanceof Map) {
|
|
171
|
+
testCode = companionCodeOverrides.get(testFilePath) || '';
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
testCode = companionCodeOverrides[testFilePath] || '';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!testCode) {
|
|
178
|
+
try {
|
|
179
|
+
testCode = fs.readFileSync(testFilePath, 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const symbols = extractSymbolsFromTestCode(testCode, testFilePath);
|
|
186
|
+
const isComponentTestFile = testFilePath.endsWith('.tsx');
|
|
187
|
+
// testspectra/no-manual-import: Manual import statement detected (must be zero-import).
|
|
188
|
+
// `.tsx` component test files (docs/v2/features/component-testing/react/) are exempt
|
|
189
|
+
// EXCEPT for imports that reach into ambient-global-backed directories (Page/Component
|
|
190
|
+
// Objects, Steps, Fixtures) — those stay forbidden so the ambient global is the single
|
|
191
|
+
// source of truth there too, exactly as in zero-import `.ts` E2E specs.
|
|
192
|
+
for (const imp of symbols.importStatements) {
|
|
193
|
+
if (isComponentTestFile && !resolvesToAmbientGlobalDir(imp.specifier, testFilePath)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
diagnostics.push({
|
|
197
|
+
code: 'testspectra/no-manual-import',
|
|
198
|
+
severity: 'error',
|
|
199
|
+
message: isComponentTestFile
|
|
200
|
+
? `Manual import of "${imp.specifier}" is forbidden; Page/Component Objects, Steps, and Fixtures are ambient globals even in component test files`
|
|
201
|
+
: 'Manual imports forbidden; all TestSpectra APIs, Page Objects, Steps, and Fixtures are ambient globals',
|
|
202
|
+
filePath: testFilePath,
|
|
203
|
+
line: imp.line,
|
|
204
|
+
column: imp.column,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// testspectra/no-test-block: forbidden test() calls detected
|
|
208
|
+
for (const tb of symbols.testBlocks) {
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
code: 'testspectra/no-test-block',
|
|
211
|
+
severity: 'error',
|
|
212
|
+
message: 'test() is forbidden; TestSpectra test scripts must strictly use it() instead of test()',
|
|
213
|
+
filePath: testFilePath,
|
|
214
|
+
line: tb.line,
|
|
215
|
+
column: tb.column,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// testspectra/single-it-required: Multiple it() blocks (or 0 it blocks) detected in a single test case script
|
|
219
|
+
if (symbols.itBlocks.length === 0) {
|
|
220
|
+
if (symbols.testBlocks.length === 0) {
|
|
221
|
+
diagnostics.push({
|
|
222
|
+
code: 'testspectra/single-it-required',
|
|
223
|
+
severity: 'error',
|
|
224
|
+
message: 'Test case script must contain exactly one it() block (found 0)',
|
|
225
|
+
filePath: testFilePath,
|
|
226
|
+
line: 1,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (symbols.itBlocks.length > 1) {
|
|
231
|
+
for (let i = 1; i < symbols.itBlocks.length; i++) {
|
|
232
|
+
const extra = symbols.itBlocks[i];
|
|
233
|
+
diagnostics.push({
|
|
234
|
+
code: 'testspectra/single-it-required',
|
|
235
|
+
severity: 'error',
|
|
236
|
+
message: `Multiple it() blocks detected (${symbols.itBlocks.length}); each test script must contain exactly one it() block`,
|
|
237
|
+
filePath: testFilePath,
|
|
238
|
+
line: extra.line,
|
|
239
|
+
column: extra.column,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// testspectra/no-describe-block: describe(...) block detected (suites are strictly structured via directory hierarchy)
|
|
244
|
+
for (const desc of symbols.describeBlocks) {
|
|
245
|
+
diagnostics.push({
|
|
246
|
+
code: 'testspectra/no-describe-block',
|
|
247
|
+
severity: 'error',
|
|
248
|
+
message: 'describe() blocks are forbidden; test suites are structured via directory hierarchy with suite.md',
|
|
249
|
+
filePath: testFilePath,
|
|
250
|
+
line: desc.line,
|
|
251
|
+
column: desc.column,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// testspectra/title-mismatch: it("...") title in test script differs from title declared in spec.md frontmatter
|
|
255
|
+
if (symbols.itBlocks.length > 0 && typeof data.title === 'string') {
|
|
256
|
+
const itTitle = symbols.itBlocks[0].title;
|
|
257
|
+
if (!areTitlesAligned(data.title, itTitle)) {
|
|
258
|
+
diagnostics.push({
|
|
259
|
+
code: 'testspectra/title-mismatch',
|
|
260
|
+
severity: 'warning',
|
|
261
|
+
message: `Test title "${itTitle}" in ${testFileName}:L${symbols.itBlocks[0].line} does not match spec.md title "${data.title}"`,
|
|
262
|
+
filePath: testFilePath,
|
|
263
|
+
line: symbols.itBlocks[0].line,
|
|
264
|
+
column: symbols.itBlocks[0].column,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Determine if platform coverage is explicitly marked manual or unsupported
|
|
269
|
+
let isPlatformAutomated = true;
|
|
270
|
+
if (coverage) {
|
|
271
|
+
if (testFileName.startsWith('web.') && (coverage.web === 'manual' || coverage.web === 'unsupported')) {
|
|
272
|
+
isPlatformAutomated = false;
|
|
273
|
+
}
|
|
274
|
+
else if ((testFileName.startsWith('mobile.') ||
|
|
275
|
+
testFileName.startsWith('android.') ||
|
|
276
|
+
testFileName.startsWith('ios.')) &&
|
|
277
|
+
(coverage.mobile === 'manual' || coverage.mobile === 'unsupported')) {
|
|
278
|
+
isPlatformAutomated = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const codeStepNames = new Set(symbols.stepCalls.map((s) => s.name));
|
|
282
|
+
// testspectra/missing-step-call: Step declared as @step:<name> in spec.md is absent in companion test script
|
|
283
|
+
if (isPlatformAutomated) {
|
|
284
|
+
for (const declaredStep of declaredSteps) {
|
|
285
|
+
if (!codeStepNames.has(declaredStep.name)) {
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
code: 'testspectra/missing-step-call',
|
|
288
|
+
severity: 'error',
|
|
289
|
+
message: `Parity Error: Step "@step:${declaredStep.name}" declared in spec.md line ${declaredStep.line} is missing in ${testFileName}`,
|
|
290
|
+
filePath,
|
|
291
|
+
line: declaredStep.line,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// testspectra/undocumented-step: Step.<name>() called in test script is not documented with @step:<name> in spec.md
|
|
297
|
+
const declaredStepNames = new Set(declaredSteps.map((s) => s.name));
|
|
298
|
+
for (const stepCall of symbols.stepCalls) {
|
|
299
|
+
if (!declaredStepNames.has(stepCall.name)) {
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
code: 'testspectra/undocumented-step',
|
|
302
|
+
severity: 'warning',
|
|
303
|
+
message: `Step "${stepCall.name}" called in ${testFileName}:L${stepCall.line} is not documented with @step:${stepCall.name} in spec.md`,
|
|
304
|
+
filePath: testFilePath,
|
|
305
|
+
line: stepCall.line,
|
|
306
|
+
column: stepCall.column,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { diagnostics, id, idLine };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Validates a single suite.md file.
|
|
316
|
+
*/
|
|
317
|
+
static validateSuiteFile(filePath, contentOverride) {
|
|
318
|
+
const diagnostics = [];
|
|
319
|
+
if (contentOverride === undefined && !fs.existsSync(filePath)) {
|
|
320
|
+
return {
|
|
321
|
+
diagnostics: [
|
|
322
|
+
{
|
|
323
|
+
code: 'testspectra/parse-error',
|
|
324
|
+
severity: 'error',
|
|
325
|
+
message: `File does not exist: ${filePath}`,
|
|
326
|
+
filePath,
|
|
327
|
+
line: 1,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const content = contentOverride !== undefined ? contentOverride : fs.readFileSync(filePath, 'utf-8');
|
|
333
|
+
const parsed = parseMarkdownFrontmatter(content, filePath);
|
|
334
|
+
diagnostics.push(...parsed.diagnostics);
|
|
335
|
+
if (!parsed.hasFrontmatter || !parsed.data) {
|
|
336
|
+
return { diagnostics };
|
|
337
|
+
}
|
|
338
|
+
const data = parsed.data;
|
|
339
|
+
const id = typeof data.id === 'string' ? data.id : undefined;
|
|
340
|
+
const idLine = parsed.keyLineMap.get('id') || 1;
|
|
341
|
+
try {
|
|
342
|
+
SuiteFrontmatterSchema.parse(data);
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
if (err instanceof ZodError) {
|
|
346
|
+
for (const issue of err.issues) {
|
|
347
|
+
const field = issue.path[0] ? String(issue.path[0]) : '';
|
|
348
|
+
const line = parsed.keyLineMap.get(field) || 1;
|
|
349
|
+
if (issue.code === 'invalid_enum_value') {
|
|
350
|
+
diagnostics.push({
|
|
351
|
+
code: 'testspectra/invalid-frontmatter-enum',
|
|
352
|
+
severity: 'error',
|
|
353
|
+
message: issue.message,
|
|
354
|
+
filePath,
|
|
355
|
+
line,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else if (issue.code === 'invalid_type' &&
|
|
359
|
+
(issue.received === 'undefined' || issue.message.toLowerCase().includes('missing required'))) {
|
|
360
|
+
diagnostics.push({
|
|
361
|
+
code: 'testspectra/missing-frontmatter',
|
|
362
|
+
severity: 'error',
|
|
363
|
+
message: issue.message,
|
|
364
|
+
filePath,
|
|
365
|
+
line: 1,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
else if (field === 'status') {
|
|
369
|
+
diagnostics.push({
|
|
370
|
+
code: 'testspectra/invalid-frontmatter-enum',
|
|
371
|
+
severity: 'error',
|
|
372
|
+
message: issue.message,
|
|
373
|
+
filePath,
|
|
374
|
+
line,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
diagnostics.push({
|
|
379
|
+
code: 'testspectra/missing-frontmatter',
|
|
380
|
+
severity: 'error',
|
|
381
|
+
message: issue.message,
|
|
382
|
+
filePath,
|
|
383
|
+
line,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
diagnostics.push({
|
|
390
|
+
code: 'testspectra/parse-error',
|
|
391
|
+
severity: 'error',
|
|
392
|
+
message: err.message,
|
|
393
|
+
filePath,
|
|
394
|
+
line: 1,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Validate markdown body sections
|
|
399
|
+
const sectionDiagnostics = validateSuiteSections(parsed.body, filePath, parsed.bodyStartLine);
|
|
400
|
+
diagnostics.push(...sectionDiagnostics);
|
|
401
|
+
return { diagnostics, id, idLine };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Validates all spec.md and suite.md files in a workspace or specific target path.
|
|
405
|
+
*/
|
|
406
|
+
static validateWorkspace(workspaceDir, targetPath) {
|
|
407
|
+
const rootScan = targetPath ? path.resolve(workspaceDir, targetPath) : workspaceDir;
|
|
408
|
+
const specFiles = [];
|
|
409
|
+
const suiteFiles = [];
|
|
410
|
+
// Collect files
|
|
411
|
+
if (fs.existsSync(rootScan)) {
|
|
412
|
+
const stat = fs.statSync(rootScan);
|
|
413
|
+
if (stat.isFile()) {
|
|
414
|
+
const base = path.basename(rootScan);
|
|
415
|
+
if (base === 'spec.md')
|
|
416
|
+
specFiles.push(rootScan);
|
|
417
|
+
else if (base === 'suite.md')
|
|
418
|
+
suiteFiles.push(rootScan);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Directory traversal
|
|
422
|
+
const traverse = (dir) => {
|
|
423
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
const fullPath = path.join(dir, entry.name);
|
|
426
|
+
if (entry.isDirectory()) {
|
|
427
|
+
if (entry.name === 'node_modules' ||
|
|
428
|
+
entry.name === 'dist' ||
|
|
429
|
+
entry.name === '.git' ||
|
|
430
|
+
entry.name === '.testspectra') {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
traverse(fullPath);
|
|
434
|
+
}
|
|
435
|
+
else if (entry.isFile()) {
|
|
436
|
+
if (entry.name === 'spec.md') {
|
|
437
|
+
specFiles.push(fullPath);
|
|
438
|
+
}
|
|
439
|
+
else if (entry.name === 'suite.md') {
|
|
440
|
+
suiteFiles.push(fullPath);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
// If target not specified and `specs/` exists, scan `specs/` by default
|
|
446
|
+
const defaultSpecsDir = path.join(rootScan, 'specs');
|
|
447
|
+
if (!targetPath && fs.existsSync(defaultSpecsDir)) {
|
|
448
|
+
traverse(defaultSpecsDir);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
traverse(rootScan);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const allDiagnostics = [];
|
|
456
|
+
const idTracker = new Map();
|
|
457
|
+
let passedSpecs = 0;
|
|
458
|
+
let passedSuites = 0;
|
|
459
|
+
// Validate suites first
|
|
460
|
+
for (const suitePath of suiteFiles) {
|
|
461
|
+
const { diagnostics, id, idLine } = this.validateSuiteFile(suitePath);
|
|
462
|
+
let fileValid = diagnostics.filter((d) => d.severity === 'error').length === 0;
|
|
463
|
+
if (id) {
|
|
464
|
+
if (idTracker.has(id)) {
|
|
465
|
+
const prev = idTracker.get(id);
|
|
466
|
+
diagnostics.push({
|
|
467
|
+
code: 'testspectra/duplicate-spec-id',
|
|
468
|
+
severity: 'error',
|
|
469
|
+
message: `Duplicate suite id "${id}" detected across the workspace (first seen in ${path.relative(workspaceDir, prev.filePath)})`,
|
|
470
|
+
filePath: suitePath,
|
|
471
|
+
line: idLine || 1,
|
|
472
|
+
});
|
|
473
|
+
fileValid = false;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
idTracker.set(id, { filePath: suitePath, line: idLine || 1 });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (fileValid) {
|
|
480
|
+
passedSuites++;
|
|
481
|
+
}
|
|
482
|
+
allDiagnostics.push(...diagnostics);
|
|
483
|
+
}
|
|
484
|
+
// Validate specs
|
|
485
|
+
for (const specPath of specFiles) {
|
|
486
|
+
const { diagnostics, id, idLine } = this.validateSpecFile(specPath);
|
|
487
|
+
let fileValid = diagnostics.filter((d) => d.severity === 'error').length === 0;
|
|
488
|
+
if (id) {
|
|
489
|
+
if (idTracker.has(id)) {
|
|
490
|
+
const prev = idTracker.get(id);
|
|
491
|
+
diagnostics.push({
|
|
492
|
+
code: 'testspectra/duplicate-spec-id',
|
|
493
|
+
severity: 'error',
|
|
494
|
+
message: `Duplicate test case id "${id}" detected across the workspace (first seen in ${path.relative(workspaceDir, prev.filePath)})`,
|
|
495
|
+
filePath: specPath,
|
|
496
|
+
line: idLine || 1,
|
|
497
|
+
});
|
|
498
|
+
fileValid = false;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
idTracker.set(id, { filePath: specPath, line: idLine || 1 });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (fileValid) {
|
|
505
|
+
passedSpecs++;
|
|
506
|
+
}
|
|
507
|
+
allDiagnostics.push(...diagnostics);
|
|
508
|
+
}
|
|
509
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
510
|
+
// testspectra/no-specs-in-shared-lib: Specs or test scripts detected inside shared testing library
|
|
511
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
512
|
+
for (const specPath of specFiles) {
|
|
513
|
+
if (isInsideSharedLibrary(specPath)) {
|
|
514
|
+
allDiagnostics.push({
|
|
515
|
+
code: 'testspectra/no-specs-in-shared-lib',
|
|
516
|
+
severity: 'error',
|
|
517
|
+
message: `Test specification detected inside shared testing library; specs must be placed in feature package directories`,
|
|
518
|
+
filePath: specPath,
|
|
519
|
+
line: 1,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const suitePath of suiteFiles) {
|
|
524
|
+
if (isInsideSharedLibrary(suitePath)) {
|
|
525
|
+
allDiagnostics.push({
|
|
526
|
+
code: 'testspectra/no-specs-in-shared-lib',
|
|
527
|
+
severity: 'error',
|
|
528
|
+
message: `Suite specification detected inside shared testing library; suites must be placed in feature package directories`,
|
|
529
|
+
filePath: suitePath,
|
|
530
|
+
line: 1,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
535
|
+
// testspectra/no-manual-import (extended) & testspectra/require-anonymous-default-export: Scan shared scripts workspace-wide
|
|
536
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
537
|
+
const sharedScriptExtensions = ['.step.ts', '.action.ts', '.hook.ts'];
|
|
538
|
+
const collectSharedScripts = (dir) => {
|
|
539
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
540
|
+
for (const entry of entries) {
|
|
541
|
+
const fullPath = path.join(dir, entry.name);
|
|
542
|
+
if (entry.isDirectory()) {
|
|
543
|
+
if (entry.name === 'node_modules' ||
|
|
544
|
+
entry.name === 'dist' ||
|
|
545
|
+
entry.name === '.git' ||
|
|
546
|
+
entry.name === '.testspectra') {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
collectSharedScripts(fullPath);
|
|
550
|
+
}
|
|
551
|
+
else if (entry.isFile()) {
|
|
552
|
+
const isSharedScript = sharedScriptExtensions.some((ext) => entry.name.endsWith(ext));
|
|
553
|
+
if (!isSharedScript)
|
|
554
|
+
continue;
|
|
555
|
+
let content = '';
|
|
556
|
+
try {
|
|
557
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
// testspectra/no-manual-import: Check for manual imports in non-test scripts
|
|
563
|
+
const symbols = extractSymbolsFromTestCode(content, fullPath);
|
|
564
|
+
for (const imp of symbols.importStatements) {
|
|
565
|
+
allDiagnostics.push({
|
|
566
|
+
code: 'testspectra/no-manual-import',
|
|
567
|
+
severity: 'error',
|
|
568
|
+
message: 'Manual imports forbidden; all TestSpectra APIs, Page Objects, Steps, and Fixtures are ambient globals',
|
|
569
|
+
filePath: fullPath,
|
|
570
|
+
line: imp.line,
|
|
571
|
+
column: imp.column,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// testspectra/require-anonymous-default-export: Check for anonymous async default export
|
|
575
|
+
const moduleExports = extractModuleExports(content, fullPath);
|
|
576
|
+
// testspectra/require-anonymous-default-export: Named exports are forbidden
|
|
577
|
+
for (const ne of moduleExports.namedExportLines) {
|
|
578
|
+
allDiagnostics.push({
|
|
579
|
+
code: 'testspectra/require-anonymous-default-export',
|
|
580
|
+
severity: 'error',
|
|
581
|
+
message: `Named export "${ne.name}" is forbidden; shared modules must use anonymous default export only`,
|
|
582
|
+
filePath: fullPath,
|
|
583
|
+
line: ne.line,
|
|
584
|
+
column: ne.column,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (!moduleExports.hasAnonymousAsyncDefaultExport && moduleExports.namedExportLines.length === 0) {
|
|
588
|
+
allDiagnostics.push({
|
|
589
|
+
code: 'testspectra/require-anonymous-default-export',
|
|
590
|
+
severity: 'error',
|
|
591
|
+
message: 'Shared step/action/hook must export an anonymous async function as default export',
|
|
592
|
+
filePath: fullPath,
|
|
593
|
+
line: 1,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// testspectra/require-anonymous-default-export: Test blocks are forbidden in shared modules
|
|
597
|
+
for (const tb of moduleExports.testBlocks) {
|
|
598
|
+
allDiagnostics.push({
|
|
599
|
+
code: 'testspectra/require-anonymous-default-export',
|
|
600
|
+
severity: 'error',
|
|
601
|
+
message: `${tb.name}() block is forbidden inside shared step/action/hook modules`,
|
|
602
|
+
filePath: fullPath,
|
|
603
|
+
line: tb.line,
|
|
604
|
+
column: tb.column,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
try {
|
|
611
|
+
collectSharedScripts(rootScan);
|
|
612
|
+
}
|
|
613
|
+
catch { }
|
|
614
|
+
const errorCount = allDiagnostics.filter((d) => d.severity === 'error').length;
|
|
615
|
+
const warningCount = allDiagnostics.filter((d) => d.severity === 'warning').length;
|
|
616
|
+
const stats = {
|
|
617
|
+
totalSpecs: specFiles.length,
|
|
618
|
+
totalSuites: suiteFiles.length,
|
|
619
|
+
passedSpecs,
|
|
620
|
+
passedSuites,
|
|
621
|
+
errorCount,
|
|
622
|
+
warningCount,
|
|
623
|
+
};
|
|
624
|
+
return {
|
|
625
|
+
valid: errorCount === 0,
|
|
626
|
+
diagnostics: allDiagnostics,
|
|
627
|
+
stats,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type DiagnosticSeverity = 'error' | 'warning';
|
|
2
|
+
export type LintDiagnosticCode = 'testspectra/parse-error' | 'testspectra/missing-frontmatter' | 'testspectra/invalid-frontmatter-enum' | 'testspectra/missing-step-call' | 'testspectra/undocumented-step' | 'testspectra/title-mismatch' | 'testspectra/no-manual-import' | 'testspectra/single-it-required' | 'testspectra/no-test-block' | 'testspectra/no-describe-block' | 'testspectra/duplicate-spec-id' | 'testspectra/no-specs-in-shared-lib' | 'testspectra/require-anonymous-default-export';
|
|
3
|
+
export interface LintDiagnostic {
|
|
4
|
+
code: LintDiagnosticCode;
|
|
5
|
+
severity: DiagnosticSeverity;
|
|
6
|
+
message: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
line: number;
|
|
9
|
+
column?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SpecLintStats {
|
|
12
|
+
totalSpecs: number;
|
|
13
|
+
totalSuites: number;
|
|
14
|
+
passedSpecs: number;
|
|
15
|
+
passedSuites: number;
|
|
16
|
+
errorCount: number;
|
|
17
|
+
warningCount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SpecLintResult {
|
|
20
|
+
valid: boolean;
|
|
21
|
+
diagnostics: LintDiagnostic[];
|
|
22
|
+
stats: SpecLintStats;
|
|
23
|
+
}
|
|
24
|
+
export interface LintOptions {
|
|
25
|
+
strict?: boolean;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|