@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,34 @@
|
|
|
1
|
+
import { SuiteHooks, GlobalHooks } from './hook-discovery.js';
|
|
2
|
+
export type TestCaseStatus = 'PASSED' | 'FAILED' | 'SKIPPED' | 'ERROR';
|
|
3
|
+
export interface TestCaseResult {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
status: TestCaseStatus;
|
|
7
|
+
error?: Error;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SuiteExecutionResult {
|
|
11
|
+
suiteName: string;
|
|
12
|
+
results: TestCaseResult[];
|
|
13
|
+
status: 'PASSED' | 'FAILED' | 'ERROR';
|
|
14
|
+
error?: Error;
|
|
15
|
+
}
|
|
16
|
+
export type HookFn = () => Promise<void> | void;
|
|
17
|
+
export declare class HookExecutionError extends Error {
|
|
18
|
+
phase: 'before' | 'beforeEach' | 'afterEach' | 'after' | 'globalBefore' | 'globalAfter';
|
|
19
|
+
originalError: any;
|
|
20
|
+
constructor(phase: 'before' | 'beforeEach' | 'afterEach' | 'after' | 'globalBefore' | 'globalAfter', originalError: any);
|
|
21
|
+
}
|
|
22
|
+
export declare function runHook(hookPathOrFn: string | HookFn, timeoutMs?: number): Promise<void>;
|
|
23
|
+
export declare class LifecycleDispatcher {
|
|
24
|
+
mode: 'single' | 'global';
|
|
25
|
+
globalHooks: GlobalHooks;
|
|
26
|
+
constructor(mode?: 'single' | 'global', globalHooks?: GlobalHooks);
|
|
27
|
+
runGlobalBefore(): Promise<void>;
|
|
28
|
+
runGlobalAfter(): Promise<void>;
|
|
29
|
+
executeSuite(suiteName: string, suiteHooks: SuiteHooks, testCases: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
fn: () => Promise<void>;
|
|
33
|
+
}>): Promise<SuiteExecutionResult>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { pathToFileURL } from 'url';
|
|
2
|
+
export class HookExecutionError extends Error {
|
|
3
|
+
phase;
|
|
4
|
+
originalError;
|
|
5
|
+
constructor(phase, originalError) {
|
|
6
|
+
super(`Hook failure during [${phase}]: ${originalError?.message || String(originalError)}`);
|
|
7
|
+
this.phase = phase;
|
|
8
|
+
this.originalError = originalError;
|
|
9
|
+
this.name = 'HookExecutionError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function runHook(hookPathOrFn, timeoutMs = 10000) {
|
|
13
|
+
let fn;
|
|
14
|
+
if (typeof hookPathOrFn === 'function') {
|
|
15
|
+
fn = hookPathOrFn;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
try {
|
|
19
|
+
const fileUrl = pathToFileURL(hookPathOrFn).href;
|
|
20
|
+
const mod = await import(fileUrl);
|
|
21
|
+
fn = mod.default || mod;
|
|
22
|
+
if (typeof fn !== 'function') {
|
|
23
|
+
throw new Error(`Hook file ${hookPathOrFn} does not export a default function`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
throw new Error(`Failed to load hook from ${hookPathOrFn}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let timeoutId = null;
|
|
31
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
32
|
+
timeoutId = setTimeout(() => {
|
|
33
|
+
reject(new Error(`Hook execution timed out after ${timeoutMs}ms`));
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await Promise.race([Promise.resolve(fn()), timeoutPromise]);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (timeoutId)
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class LifecycleDispatcher {
|
|
45
|
+
mode;
|
|
46
|
+
globalHooks;
|
|
47
|
+
constructor(mode = 'single', globalHooks = { before: [], after: [] }) {
|
|
48
|
+
this.mode = mode;
|
|
49
|
+
this.globalHooks = globalHooks;
|
|
50
|
+
}
|
|
51
|
+
async runGlobalBefore() {
|
|
52
|
+
if (this.mode !== 'global')
|
|
53
|
+
return;
|
|
54
|
+
for (const hook of this.globalHooks.before) {
|
|
55
|
+
try {
|
|
56
|
+
await runHook(hook);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
throw new HookExecutionError('globalBefore', err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async runGlobalAfter() {
|
|
64
|
+
if (this.mode !== 'global')
|
|
65
|
+
return;
|
|
66
|
+
let firstError = null;
|
|
67
|
+
for (const hook of this.globalHooks.after) {
|
|
68
|
+
try {
|
|
69
|
+
await runHook(hook);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (!firstError)
|
|
73
|
+
firstError = err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (firstError) {
|
|
77
|
+
throw new HookExecutionError('globalAfter', firstError);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async executeSuite(suiteName, suiteHooks, testCases) {
|
|
81
|
+
const results = [];
|
|
82
|
+
let suiteLevelError;
|
|
83
|
+
// 1. Run suite before hook
|
|
84
|
+
let beforeSuiteFailed = false;
|
|
85
|
+
for (const hook of suiteHooks.before) {
|
|
86
|
+
try {
|
|
87
|
+
await runHook(hook);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
beforeSuiteFailed = true;
|
|
91
|
+
suiteLevelError = new HookExecutionError('before', err);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (beforeSuiteFailed) {
|
|
96
|
+
// All test cases in suite are skipped / marked ERROR
|
|
97
|
+
for (const tc of testCases) {
|
|
98
|
+
results.push({
|
|
99
|
+
id: tc.id,
|
|
100
|
+
title: tc.title,
|
|
101
|
+
status: 'ERROR',
|
|
102
|
+
error: suiteLevelError,
|
|
103
|
+
durationMs: 0,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Always execute suite after hook even on before failure
|
|
107
|
+
for (const hook of suiteHooks.after) {
|
|
108
|
+
try {
|
|
109
|
+
await runHook(hook);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
suiteName,
|
|
115
|
+
results,
|
|
116
|
+
status: 'ERROR',
|
|
117
|
+
error: suiteLevelError,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// 2. Run each test case with beforeEach and afterEach
|
|
121
|
+
for (const tc of testCases) {
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
let tcStatus = 'PASSED';
|
|
124
|
+
let tcError;
|
|
125
|
+
let beforeEachFailed = false;
|
|
126
|
+
// Run beforeEach hooks
|
|
127
|
+
for (const hook of suiteHooks.beforeEach) {
|
|
128
|
+
try {
|
|
129
|
+
await runHook(hook);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
beforeEachFailed = true;
|
|
133
|
+
tcStatus = 'ERROR';
|
|
134
|
+
tcError = new HookExecutionError('beforeEach', err);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Run test body if beforeEach passed
|
|
139
|
+
if (!beforeEachFailed) {
|
|
140
|
+
try {
|
|
141
|
+
await tc.fn();
|
|
142
|
+
tcStatus = 'PASSED';
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
tcStatus = 'FAILED';
|
|
146
|
+
tcError = err instanceof Error ? err : new Error(String(err));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// afterEach is GUARANTEED to execute even if beforeEach or test body failed
|
|
150
|
+
for (const hook of suiteHooks.afterEach) {
|
|
151
|
+
try {
|
|
152
|
+
await runHook(hook);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// If test passed or failed, an afterEach failure transitions it to ERROR (Teardown Failure)
|
|
156
|
+
tcStatus = 'ERROR';
|
|
157
|
+
if (!tcError) {
|
|
158
|
+
tcError = new HookExecutionError('afterEach', err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
results.push({
|
|
163
|
+
id: tc.id,
|
|
164
|
+
title: tc.title,
|
|
165
|
+
status: tcStatus,
|
|
166
|
+
error: tcError,
|
|
167
|
+
durationMs: Date.now() - startTime,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// 3. Run suite after hook
|
|
171
|
+
for (const hook of suiteHooks.after) {
|
|
172
|
+
try {
|
|
173
|
+
await runHook(hook);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (!suiteLevelError) {
|
|
177
|
+
suiteLevelError = new HookExecutionError('after', err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const hasErrors = results.some((r) => r.status === 'ERROR') || !!suiteLevelError;
|
|
182
|
+
const hasFailures = results.some((r) => r.status === 'FAILED');
|
|
183
|
+
return {
|
|
184
|
+
suiteName,
|
|
185
|
+
results,
|
|
186
|
+
status: hasErrors ? 'ERROR' : hasFailures ? 'FAILED' : 'PASSED',
|
|
187
|
+
error: suiteLevelError,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ParallelizationMode = 'suite' | 'testcase';
|
|
2
|
+
export interface WorkerSizingOptions {
|
|
3
|
+
maxWorkersCap?: number;
|
|
4
|
+
userConcurrency?: number;
|
|
5
|
+
cpuQuotaOverride?: number;
|
|
6
|
+
memoryBytesOverride?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Detects actual CPU quota available to the process, accounting for
|
|
10
|
+
* Docker and Kubernetes cgroup v1 and cgroup v2 container limits.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectContainerCpuQuota(): number;
|
|
13
|
+
/**
|
|
14
|
+
* Detects available memory in bytes, respecting container cgroup memory limits
|
|
15
|
+
* to prevent Linux OOM (Out of Memory) kills.
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectAvailableMemoryBytes(): number;
|
|
18
|
+
/**
|
|
19
|
+
* Dynamically computes optimal worker concurrency respecting:
|
|
20
|
+
* 1. Explicit user concurrency flag (--concurrency <N> or config)
|
|
21
|
+
* 2. Real container cgroups CPU quota & host CPU count
|
|
22
|
+
* 3. Low-CPU safety ratio (clamping <=2 CPUs to 1 worker, 3-4 CPUs to 2 workers)
|
|
23
|
+
* 4. Memory-aware guard (~600MB RAM budget per Chromium instance to prevent OOM kills)
|
|
24
|
+
* 5. Target items count & maximum worker cap
|
|
25
|
+
*/
|
|
26
|
+
export declare function calculateWorkerCount(targetItems: any[], options?: WorkerSizingOptions | number): number;
|
|
27
|
+
export declare function groupSpecs(specPaths: string[], mode?: ParallelizationMode): (string | string[])[];
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Detects actual CPU quota available to the process, accounting for
|
|
6
|
+
* Docker and Kubernetes cgroup v1 and cgroup v2 container limits.
|
|
7
|
+
*/
|
|
8
|
+
export function detectContainerCpuQuota() {
|
|
9
|
+
try {
|
|
10
|
+
// 1. Check cgroup v2 (Unified Hierarchy): /sys/fs/cgroup/cpu.max
|
|
11
|
+
// Format: "<quota> <period>" e.g. "200000 100000" or "max 100000"
|
|
12
|
+
if (fs.existsSync('/sys/fs/cgroup/cpu.max')) {
|
|
13
|
+
const content = fs.readFileSync('/sys/fs/cgroup/cpu.max', 'utf-8').trim();
|
|
14
|
+
const [quotaStr, periodStr] = content.split(/\s+/);
|
|
15
|
+
if (quotaStr && quotaStr !== 'max' && periodStr) {
|
|
16
|
+
const quota = parseFloat(quotaStr);
|
|
17
|
+
const period = parseFloat(periodStr);
|
|
18
|
+
if (quota > 0 && period > 0) {
|
|
19
|
+
return Math.max(0.5, quota / period);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 2. Check cgroup v1: /sys/fs/cgroup/cpu/cpu.cfs_quota_us and cpu.cfs_period_us
|
|
24
|
+
if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
|
|
25
|
+
const quota = parseFloat(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf-8').trim());
|
|
26
|
+
const period = parseFloat(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf-8').trim());
|
|
27
|
+
if (quota > 0 && period > 0) {
|
|
28
|
+
return Math.max(0.5, quota / period);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
// 3. Fallback to host CPU count
|
|
34
|
+
if (typeof os.availableParallelism === 'function') {
|
|
35
|
+
try {
|
|
36
|
+
return os.availableParallelism();
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
return os.cpus()?.length || 2;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Detects available memory in bytes, respecting container cgroup memory limits
|
|
44
|
+
* to prevent Linux OOM (Out of Memory) kills.
|
|
45
|
+
*/
|
|
46
|
+
export function detectAvailableMemoryBytes() {
|
|
47
|
+
try {
|
|
48
|
+
// 1. Check cgroup v2: /sys/fs/cgroup/memory.max and /sys/fs/cgroup/memory.current
|
|
49
|
+
if (fs.existsSync('/sys/fs/cgroup/memory.max')) {
|
|
50
|
+
const maxStr = fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf-8').trim();
|
|
51
|
+
if (maxStr !== 'max') {
|
|
52
|
+
const limit = parseFloat(maxStr);
|
|
53
|
+
let usage = 0;
|
|
54
|
+
if (fs.existsSync('/sys/fs/cgroup/memory.current')) {
|
|
55
|
+
usage = parseFloat(fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf-8').trim()) || 0;
|
|
56
|
+
}
|
|
57
|
+
if (limit > 0) {
|
|
58
|
+
return Math.max(0, limit - usage);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 2. Check cgroup v1: /sys/fs/cgroup/memory/memory.limit_in_bytes
|
|
63
|
+
if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
|
|
64
|
+
const limit = parseFloat(fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf-8').trim());
|
|
65
|
+
// cgroup v1 sets a huge number (e.g. 9223372036854771712) when no limit is enforced
|
|
66
|
+
if (limit > 0 && limit < 1e15) {
|
|
67
|
+
let usage = 0;
|
|
68
|
+
if (fs.existsSync('/sys/fs/cgroup/memory/memory.usage_in_bytes')) {
|
|
69
|
+
usage = parseFloat(fs.readFileSync('/sys/fs/cgroup/memory/memory.usage_in_bytes', 'utf-8').trim()) || 0;
|
|
70
|
+
}
|
|
71
|
+
return Math.max(0, limit - usage);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
// 3. Fallback to host available memory (estimating reclaimable cache/buffer on modern OS)
|
|
77
|
+
try {
|
|
78
|
+
const free = os.freemem();
|
|
79
|
+
const total = os.totalmem();
|
|
80
|
+
return Math.max(free, total * 0.4);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return 1024 * 1024 * 1024; // 1 GB fallback
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Dynamically computes optimal worker concurrency respecting:
|
|
88
|
+
* 1. Explicit user concurrency flag (--concurrency <N> or config)
|
|
89
|
+
* 2. Real container cgroups CPU quota & host CPU count
|
|
90
|
+
* 3. Low-CPU safety ratio (clamping <=2 CPUs to 1 worker, 3-4 CPUs to 2 workers)
|
|
91
|
+
* 4. Memory-aware guard (~600MB RAM budget per Chromium instance to prevent OOM kills)
|
|
92
|
+
* 5. Target items count & maximum worker cap
|
|
93
|
+
*/
|
|
94
|
+
export function calculateWorkerCount(targetItems, options) {
|
|
95
|
+
const opts = typeof options === 'number' ? { maxWorkersCap: options } : options || {};
|
|
96
|
+
const itemCount = targetItems.length;
|
|
97
|
+
if (itemCount === 0)
|
|
98
|
+
return 1;
|
|
99
|
+
// 1. Explicit User Concurrency has absolute priority
|
|
100
|
+
if (opts.userConcurrency && opts.userConcurrency > 0) {
|
|
101
|
+
return Math.min(opts.userConcurrency, itemCount);
|
|
102
|
+
}
|
|
103
|
+
// 2. Determine CPU Quota (cgroups aware)
|
|
104
|
+
const cpuCount = opts.cpuQuotaOverride !== undefined ? opts.cpuQuotaOverride : detectContainerCpuQuota();
|
|
105
|
+
// 3. Low-CPU Safety Ratio:
|
|
106
|
+
// - CPU <= 2: Max 1 worker (prevents severe thrashing on 2-vCPU CI runners)
|
|
107
|
+
// - CPU 3-4: Max 2 workers
|
|
108
|
+
// - CPU > 4: cpuCount - 2 (leaves 2 cores for OS/IDE/DevTools)
|
|
109
|
+
let cpuBasedWorkers = 1;
|
|
110
|
+
if (cpuCount <= 2) {
|
|
111
|
+
cpuBasedWorkers = 1;
|
|
112
|
+
}
|
|
113
|
+
else if (cpuCount <= 4) {
|
|
114
|
+
cpuBasedWorkers = 2;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
cpuBasedWorkers = Math.max(1, Math.floor(cpuCount - 2));
|
|
118
|
+
}
|
|
119
|
+
// 4. Memory-Aware Guard (Anti-OOM):
|
|
120
|
+
// Allocate ~600MB per Chromium browser instance
|
|
121
|
+
const MEMORY_PER_WORKER_BYTES = 600 * 1024 * 1024;
|
|
122
|
+
const availableMemBytes = opts.memoryBytesOverride !== undefined ? opts.memoryBytesOverride : detectAvailableMemoryBytes();
|
|
123
|
+
const memBasedWorkers = Math.max(1, Math.floor(availableMemBytes / MEMORY_PER_WORKER_BYTES));
|
|
124
|
+
// 5. Apply Maximum Cap and Item Count Clamping
|
|
125
|
+
const maxCap = opts.maxWorkersCap || 6;
|
|
126
|
+
return Math.max(1, Math.min(cpuBasedWorkers, memBasedWorkers, maxCap, itemCount));
|
|
127
|
+
}
|
|
128
|
+
export function groupSpecs(specPaths, mode = 'suite') {
|
|
129
|
+
if (mode === 'testcase') {
|
|
130
|
+
return [...specPaths];
|
|
131
|
+
}
|
|
132
|
+
// Group by suite directory
|
|
133
|
+
const suiteGroups = new Map();
|
|
134
|
+
for (const specPath of specPaths) {
|
|
135
|
+
const normalized = specPath.replace(/\\/g, '/');
|
|
136
|
+
const parts = normalized.split('/');
|
|
137
|
+
let suiteKey = 'default';
|
|
138
|
+
const specsIdx = parts.findIndex((p) => p === 'specs' || p === 'suites');
|
|
139
|
+
if (specsIdx !== -1 && specsIdx + 1 < parts.length) {
|
|
140
|
+
suiteKey = parts.slice(0, specsIdx + 2).join('/');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
suiteKey = path.dirname(normalized);
|
|
144
|
+
}
|
|
145
|
+
if (!suiteGroups.has(suiteKey)) {
|
|
146
|
+
suiteGroups.set(suiteKey, []);
|
|
147
|
+
}
|
|
148
|
+
suiteGroups.get(suiteKey).push(specPath);
|
|
149
|
+
}
|
|
150
|
+
return Array.from(suiteGroups.values());
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { extractModuleExports } from '../extractor.js';
|
|
6
|
+
import { SpecLinter } from '../spec-linter.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
describe('Anonymous Default Export (testspectra/require-anonymous-default-export)', () => {
|
|
10
|
+
const tempDir = path.join(__dirname, '__temp_anonymous_export__');
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (fs.existsSync(tempDir)) {
|
|
16
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
describe('extractModuleExports()', () => {
|
|
20
|
+
it('detects valid anonymous async default export in .step.ts', () => {
|
|
21
|
+
const code = `export default async function () {
|
|
22
|
+
await LoginPage.open();
|
|
23
|
+
}`;
|
|
24
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
25
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
26
|
+
expect(result.hasNamedExports).toBe(false);
|
|
27
|
+
expect(result.testBlocks).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
it('detects valid anonymous async arrow function default export in .step.ts', () => {
|
|
30
|
+
const code = `export default async () => {
|
|
31
|
+
await LoginPage.open();
|
|
32
|
+
};`;
|
|
33
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
34
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
35
|
+
expect(result.hasNamedExports).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('detects named function with export default (forbidden)', () => {
|
|
38
|
+
const code = `export default async function login() {
|
|
39
|
+
await LoginPage.open();
|
|
40
|
+
}`;
|
|
41
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
42
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
43
|
+
expect(result.hasNamedExports).toBe(true);
|
|
44
|
+
expect(result.namedExportLines).toHaveLength(1);
|
|
45
|
+
expect(result.namedExportLines[0].name).toBe('login');
|
|
46
|
+
expect(result.namedExportLines[0].line).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
it('detects named export function (forbidden)', () => {
|
|
49
|
+
const code = `export async function verifyUser() {
|
|
50
|
+
await UserPage.verify();
|
|
51
|
+
}`;
|
|
52
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
53
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
54
|
+
expect(result.hasNamedExports).toBe(true);
|
|
55
|
+
expect(result.namedExportLines[0].name).toBe('verifyUser');
|
|
56
|
+
});
|
|
57
|
+
it('detects named export class (forbidden)', () => {
|
|
58
|
+
const code = `export class Helper {}`;
|
|
59
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
60
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
61
|
+
expect(result.hasNamedExports).toBe(true);
|
|
62
|
+
expect(result.namedExportLines[0].name).toBe('Helper');
|
|
63
|
+
});
|
|
64
|
+
it('detects export { ... } clauses (forbidden)', () => {
|
|
65
|
+
const code = `const a = 1; export { a };`;
|
|
66
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
67
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
68
|
+
expect(result.hasNamedExports).toBe(true);
|
|
69
|
+
expect(result.namedExportLines[0].name).toBe('a');
|
|
70
|
+
});
|
|
71
|
+
it('detects named arrow function with export default (forbidden)', () => {
|
|
72
|
+
const code = `const fn = async () => {}; export default async function named() {}`;
|
|
73
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
74
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
75
|
+
expect(result.hasNamedExports).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('detects valid anonymous async default export in .action.ts', () => {
|
|
78
|
+
const code = `export default async function (email: string) {
|
|
79
|
+
await this.get("#email").type(email);
|
|
80
|
+
}`;
|
|
81
|
+
const result = extractModuleExports(code, 'web.action.ts');
|
|
82
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
83
|
+
expect(result.hasNamedExports).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('detects valid anonymous async default export in .hook.ts', () => {
|
|
86
|
+
const code = `export default async function () {}`;
|
|
87
|
+
const result = extractModuleExports(code, 'web.hook.ts');
|
|
88
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(true);
|
|
89
|
+
expect(result.hasNamedExports).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it('detects missing default export', () => {
|
|
92
|
+
const code = `function verifyLogin() {
|
|
93
|
+
await LoginPage.open();
|
|
94
|
+
}`;
|
|
95
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
96
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it('detects non-async default export as invalid', () => {
|
|
99
|
+
const code = `export default function verifyLogin() {
|
|
100
|
+
console.log("hello");
|
|
101
|
+
}`;
|
|
102
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
103
|
+
expect(result.hasAnonymousAsyncDefaultExport).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
it('detects forbidden it() and describe() blocks in shared module', () => {
|
|
106
|
+
const code = `export default async function () {}
|
|
107
|
+
it("test block", async () => {});
|
|
108
|
+
describe("suite block", () => {});
|
|
109
|
+
`;
|
|
110
|
+
const result = extractModuleExports(code, 'web.step.ts');
|
|
111
|
+
expect(result.testBlocks).toHaveLength(2);
|
|
112
|
+
expect(result.testBlocks[0].name).toBe('it');
|
|
113
|
+
expect(result.testBlocks[1].name).toBe('describe');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('testspectra/require-anonymous-default-export workspace validation', () => {
|
|
117
|
+
it('emits testspectra/require-anonymous-default-export when shared step lacks anonymous async default export', () => {
|
|
118
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'login');
|
|
119
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `function verifyLogin() {
|
|
121
|
+
console.log("no export");
|
|
122
|
+
}
|
|
123
|
+
`);
|
|
124
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
125
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
126
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
127
|
+
expect(errors[0].message).toContain('anonymous async function as default export');
|
|
128
|
+
});
|
|
129
|
+
it('passes validation when shared step has valid anonymous async default export', () => {
|
|
130
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'login');
|
|
131
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `export default async function () {
|
|
133
|
+
await LoginPage.open();
|
|
134
|
+
}
|
|
135
|
+
`);
|
|
136
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
137
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
138
|
+
expect(errors).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
it('emits testspectra/require-anonymous-default-export when shared action has named exports', () => {
|
|
141
|
+
const actionsDir = path.join(tempDir, 'shared', 'testing', 'support', 'actions', 'fillForm');
|
|
142
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
143
|
+
fs.writeFileSync(path.join(actionsDir, 'web.action.ts'), `export default async function () {}
|
|
144
|
+
export function helper() {}
|
|
145
|
+
`);
|
|
146
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
147
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
148
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
149
|
+
expect(errors.some((d) => d.message.includes('Named export'))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it('emits testspectra/require-anonymous-default-export when shared hook has test blocks', () => {
|
|
152
|
+
const hooksDir = path.join(tempDir, 'shared', 'testing', 'global-hooks', 'before');
|
|
153
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
154
|
+
fs.writeFileSync(path.join(hooksDir, 'web.hook.ts'), `export default async function () {}
|
|
155
|
+
it("should run", async () => {});
|
|
156
|
+
`);
|
|
157
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
158
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
159
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
160
|
+
expect(errors.some((d) => d.message.includes('it() block is forbidden'))).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it('does NOT emit testspectra/require-anonymous-default-export for valid shared step files', () => {
|
|
163
|
+
const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'verify');
|
|
164
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
165
|
+
fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `export default async function () {
|
|
166
|
+
await Page.open();
|
|
167
|
+
await Page.element.should("be.visible");
|
|
168
|
+
}
|
|
169
|
+
`);
|
|
170
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
171
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
172
|
+
expect(errors).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
it('does NOT emit testspectra/require-anonymous-default-export for files outside shared/ directory', () => {
|
|
175
|
+
const featureDir = path.join(tempDir, 'packages', 'playground', 'e2e');
|
|
176
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
177
|
+
fs.writeFileSync(path.join(featureDir, 'helper.ts'), `export function helper() {}
|
|
178
|
+
`);
|
|
179
|
+
const result = SpecLinter.validateWorkspace(tempDir);
|
|
180
|
+
const errors = result.diagnostics.filter((d) => d.code === 'testspectra/require-anonymous-default-export');
|
|
181
|
+
expect(errors).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|