@testspectra/cli 1.0.70 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +48 -0
- package/README.md +36 -270
- package/bin/.testspectra-runner.hash +1 -0
- package/bin/spectra.js +1 -1
- package/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-DPfv5_EF.js +116 -0
- package/demo-app/assets/index-gIwu2EMV.css +1 -0
- package/demo-app/index.html +2 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
- package/dist/commands/__tests__/cloud-run.test.js +93 -0
- package/dist/commands/__tests__/doctor.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor.test.js +39 -0
- package/dist/commands/__tests__/init.test.d.ts +1 -0
- package/dist/commands/__tests__/init.test.js +148 -0
- package/dist/commands/__tests__/license.test.d.ts +1 -0
- package/dist/commands/__tests__/license.test.js +13 -0
- package/dist/commands/__tests__/refactor.test.d.ts +1 -0
- package/dist/commands/__tests__/refactor.test.js +64 -0
- package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
- package/dist/commands/__tests__/run-e2e.test.js +181 -0
- package/dist/commands/__tests__/run.test.d.ts +1 -0
- package/dist/commands/__tests__/run.test.js +197 -0
- package/dist/commands/__tests__/watch.test.d.ts +1 -0
- package/dist/commands/__tests__/watch.test.js +155 -0
- package/dist/commands/add.js +86 -85
- package/dist/commands/cloud-run.d.ts +11 -0
- package/dist/commands/cloud-run.js +118 -0
- package/dist/commands/demo.js +7 -7
- package/dist/commands/devices.d.ts +14 -2
- package/dist/commands/devices.js +44 -34
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.js +43 -264
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +206 -196
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +59 -0
- package/dist/commands/license.d.ts +4 -0
- package/dist/commands/license.js +30 -0
- package/dist/commands/refactor.d.ts +8 -0
- package/dist/commands/refactor.js +78 -0
- package/dist/commands/run.d.ts +22 -1
- package/dist/commands/run.js +252 -109
- package/dist/commands/sync-types.d.ts +4 -0
- package/dist/commands/sync-types.js +14 -0
- package/dist/commands/update.js +39 -43
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +47 -43
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +40 -25
- package/dist/config/schema.d.ts +117 -49
- package/dist/config/schema.js +41 -34
- package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
- package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
- package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
- package/dist/generator/__tests__/type-generator.test.js +386 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/tsconfig-generator.d.ts +27 -0
- package/dist/generator/tsconfig-generator.js +391 -0
- package/dist/generator/type-generator.d.ts +36 -0
- package/dist/generator/type-generator.js +644 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +124 -51
- package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
- package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
- package/dist/lifecycle/hook-discovery.d.ts +24 -0
- package/dist/lifecycle/hook-discovery.js +60 -0
- package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
- package/dist/lifecycle/hook-dispatcher.js +190 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/parallel-grouping.d.ts +27 -0
- package/dist/lifecycle/parallel-grouping.js +151 -0
- package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
- package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
- package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
- package/dist/mobile/__tests__/device-pool.test.js +97 -0
- package/dist/mobile/device-pool.d.ts +23 -0
- package/dist/mobile/device-pool.js +1 -0
- package/dist/mobile/index.d.ts +1 -0
- package/dist/mobile/index.js +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.js +175 -0
- package/dist/refactor/index.d.ts +1 -0
- package/dist/refactor/index.js +1 -0
- package/dist/refactor/refactor-engine.d.ts +25 -0
- package/dist/refactor/refactor-engine.js +255 -0
- package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
- package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
- package/dist/reporter/semantic-formatter.d.ts +6 -0
- package/dist/reporter/semantic-formatter.js +191 -0
- package/dist/reporter/summary-view.d.ts +12 -0
- package/dist/reporter/summary-view.js +19 -0
- package/dist/reporter/terminal-stream.d.ts +32 -0
- package/dist/reporter/terminal-stream.js +82 -0
- package/dist/reporter/types.d.ts +2 -0
- package/dist/reporter/types.js +1 -0
- package/dist/runner/bridge.d.ts +10 -5
- package/dist/runner/bridge.js +69 -44
- package/dist/runner/reporter.d.ts +30 -2
- package/dist/runner/reporter.js +419 -13
- package/dist/server/demo.js +43 -44
- package/dist/types/generator.d.ts +1 -17
- package/dist/types/generator.js +1 -443
- package/dist/utils/demo-server.js +38 -35
- package/package.json +20 -12
- package/templates/default/CLAUDE.md +7 -5
- package/templates/default/fixtures/apiData.json +24 -0
- package/templates/default/package.json +13 -4
- package/templates/default/page-objects/MatchersPage/mobile.ts +41 -16
- package/templates/default/page-objects/MatchersPage/web.ts +14 -0
- package/templates/default/page-objects/NavigationPage/mobile.ts +10 -1
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +27 -17
- package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +11 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +16 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
- package/templates/default/specs/ApiInterception/suite.md +9 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +15 -1
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
- package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
- package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
- package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
- package/templates/default/spectra.config.ts +21 -14
- package/templates/default/support/actions/fillCredentials/mobile.action.ts +2 -2
- package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +2 -0
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +2 -2
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +3 -1
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +3 -3
- package/templates/default/tsconfig.json +3 -1
- package/templates/nx/.nx/workspace-data/d/daemon.log +168 -0
- package/templates/nx/CLAUDE.md +2 -2
- package/templates/nx/package.json +7 -5
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +3 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +3 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
- package/templates/nx/pnpm-lock.yaml +6663 -0
- package/templates/nx/pnpm-workspace.yaml +2 -1
- package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +12 -0
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +14 -0
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
- package/templates/nx/shared/testing/project.json +19 -0
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +2 -2
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +3 -3
- package/templates/nx/spectra.config.ts +19 -12
- package/templates/nx/tsconfig.json +3 -1
- package/demo-app/assets/index-DAUlrHTr.css +0 -1
- package/demo-app/assets/index-DN2Kb0A8.js +0 -100
- package/dist/index.cjs +0 -101
- package/dist/plugin.cjs +0 -575
- package/dist/plugin.d.ts +0 -16
- package/dist/plugin.js +0 -184
- package/dist/step/index.d.ts +0 -6
- package/dist/step/index.js +0 -6
- package/dist/step/matchers.d.ts +0 -14
- package/dist/step/matchers.js +0 -114
- package/dist/step/proto.d.ts +0 -46
- package/dist/step/proto.js +0 -114
- package/dist/step/runner/collection.d.ts +0 -13
- package/dist/step/runner/collection.js +0 -51
- package/dist/step/runner/single.d.ts +0 -21
- package/dist/step/runner/single.js +0 -138
- package/dist/step/spectra.d.ts +0 -34
- package/dist/step/spectra.js +0 -156
- package/dist/step/types.d.ts +0 -32
- package/dist/step/types.js +0 -5
- package/templates/default/tsconfig.spectra.android.json +0 -46
- package/templates/default/tsconfig.spectra.ios.json +0 -46
- package/templates/default/tsconfig.spectra.json +0 -22
- package/templates/default/tsconfig.spectra.web.json +0 -53
- package/templates/nx/fixtures/playgroundData.json +0 -7
- package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
- package/templates/nx/global-hooks/after/web.hook.ts +0 -3
- package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
- package/templates/nx/global-hooks/before/web.hook.ts +0 -3
- package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
- package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
- package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
- package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
- package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
- package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
- package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
- package/templates/nx/specs/BrowserContext/suite.md +0 -9
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
- package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
- package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
- package/templates/nx/specs/ContentMatchers/suite.md +0 -9
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
- package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
- package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
- package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
- package/templates/nx/specs/ElementMatchers/suite.md +0 -9
- package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
- package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
- package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
- package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
- package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
- package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
- package/templates/nx/specs/Playground/suite.md +0 -9
- package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
- package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
- package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
- package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
- package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
- package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
- package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
- package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
- package/templates/nx/tsconfig.spectra.android.json +0 -39
- package/templates/nx/tsconfig.spectra.ios.json +0 -39
- package/templates/nx/tsconfig.spectra.json +0 -25
- package/templates/nx/tsconfig.spectra.shared.json +0 -25
- package/templates/nx/tsconfig.spectra.web.json +0 -45
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
- /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
- /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -1,72 +1,145 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { addCommand } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { addCommand } from './commands/add.js';
|
|
3
|
+
import { cloudRunCommand } from './commands/cloud-run.js';
|
|
4
|
+
import { demoCommand } from './commands/demo.js';
|
|
5
|
+
import { devicesCommand } from './commands/devices.js';
|
|
6
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
7
|
+
import { initCommand } from './commands/init.js';
|
|
8
|
+
import { installCommand } from './commands/install.js';
|
|
9
|
+
import { runCommand } from './commands/run.js';
|
|
10
|
+
import { updateCommand } from './commands/update.js';
|
|
11
|
+
import { watchCommand } from './commands/watch.js';
|
|
12
|
+
import { syncTypesCommand } from './commands/sync-types.js';
|
|
13
|
+
import { refactorCommand } from './commands/refactor.js';
|
|
14
|
+
import { licenseCommand } from './commands/license.js';
|
|
15
|
+
export * from './config/schema.js';
|
|
16
|
+
export * from './config/loader.js';
|
|
17
|
+
export * from './generator/index.js';
|
|
18
|
+
export * from './lifecycle/index.js';
|
|
19
|
+
export * from './refactor/index.js';
|
|
20
|
+
export * from './mobile/index.js';
|
|
21
|
+
export * from './types/generator.js';
|
|
22
|
+
export * from '@testspectra/matchers';
|
|
14
23
|
export function createCliProgram() {
|
|
15
24
|
const program = new Command();
|
|
25
|
+
program.name('spectra').description('TestSpectra Zero-Config Cross-Platform Test Runner CLI').version('1.0.0');
|
|
16
26
|
program
|
|
17
|
-
.
|
|
18
|
-
.description(
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
.command("init")
|
|
22
|
-
.description("Initialize testspectra.config.json and project directories")
|
|
23
|
-
.option("-t, --template <template>", "Project template: default, nx")
|
|
24
|
-
.option("-f, --force", "Overwrite existing configuration if present")
|
|
27
|
+
.command('init')
|
|
28
|
+
.description('Initialize testspectra.config.json and project directories')
|
|
29
|
+
.option('-t, --template <template>', 'Project template: default, nx')
|
|
30
|
+
.option('-f, --force', 'Overwrite existing configuration if present')
|
|
25
31
|
.action(initCommand);
|
|
26
32
|
program
|
|
27
|
-
.command(
|
|
28
|
-
.description(
|
|
29
|
-
.option(
|
|
30
|
-
.option(
|
|
33
|
+
.command('add [module]')
|
|
34
|
+
.description('Add a new TestSpectra E2E testing module to workspace or standalone project')
|
|
35
|
+
.option('-e, --e2e-folder <name>', 'Name of E2E sub-folder (default: e2e)')
|
|
36
|
+
.option('-f, --force', 'Overwrite existing module files if present')
|
|
31
37
|
.action(addCommand);
|
|
32
38
|
program
|
|
33
|
-
.command(
|
|
34
|
-
.description(
|
|
35
|
-
.option(
|
|
39
|
+
.command('install [dependency]')
|
|
40
|
+
.description('Install missing testing dependencies, browser drivers, and workspace packages')
|
|
41
|
+
.option('-f, --force', 'Force re-installation of dependencies')
|
|
42
|
+
.addHelpText('after', `
|
|
43
|
+
Examples:
|
|
44
|
+
$ spectra install Install all missing required dependencies automatically
|
|
45
|
+
$ spectra install chrome Install Google Chrome for Testing & matching Chromedriver
|
|
46
|
+
$ spectra install adb Install Google Android SDK Platform Tools (ADB)
|
|
47
|
+
$ spectra install bun Install Bun runtime
|
|
48
|
+
$ spectra install android-driver Install Spectra Android Native Driver & WDIO Service
|
|
49
|
+
$ spectra install wdio Install WebDriverIO core packages and matchers`)
|
|
50
|
+
.action(installCommand);
|
|
51
|
+
program
|
|
52
|
+
.command('demo')
|
|
53
|
+
.description('Launch the built-in TestSpectra Demo Web App server on localhost:5173')
|
|
54
|
+
.option('-p, --port <port>', 'Port to bind server (default: 5173)')
|
|
36
55
|
.action(demoCommand);
|
|
37
56
|
program
|
|
38
|
-
.command(
|
|
39
|
-
.description(
|
|
40
|
-
.option(
|
|
57
|
+
.command('watch')
|
|
58
|
+
.description('Watch workspace and continuously auto-generate TestSpectra ambient declarations')
|
|
59
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
41
60
|
.action(watchCommand);
|
|
42
61
|
program
|
|
43
|
-
.command(
|
|
44
|
-
.description(
|
|
45
|
-
.option(
|
|
46
|
-
.
|
|
62
|
+
.command('sync-types')
|
|
63
|
+
.description('Scan workspace and regenerate .testspectra/types/*.d.ts ambient declarations')
|
|
64
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
65
|
+
.action(syncTypesCommand);
|
|
66
|
+
program
|
|
67
|
+
.command('refactor [entityType] [oldName] [newName]')
|
|
68
|
+
.description('Atomically rename and propagate symbols (page-object, step, action, fixture)')
|
|
69
|
+
.option('-w, --cwd <dir>', 'Custom workspace directory')
|
|
70
|
+
.option('--from <path>', 'Source file or directory to refactor from')
|
|
71
|
+
.option('--to <path>', 'Destination file or directory to refactor to')
|
|
72
|
+
.option('--dry-run', 'Preview changes without modifying files')
|
|
73
|
+
.option('--json', 'Output results as JSON')
|
|
74
|
+
.action(refactorCommand);
|
|
75
|
+
program
|
|
76
|
+
.command('doctor')
|
|
77
|
+
.description('Verify local environment prerequisites (ADB, Spectra Android Driver, Chrome, Bun)')
|
|
78
|
+
.option('-v, --verbose', 'Show full installation paths for all detected runtimes and packages')
|
|
79
|
+
.option('--fix', 'Attempt automatic fix / download of missing tools')
|
|
80
|
+
.option('--json', 'Output diagnostic results as JSON')
|
|
47
81
|
.action(doctorCommand);
|
|
48
82
|
program
|
|
49
|
-
.command(
|
|
50
|
-
.description(
|
|
51
|
-
.option(
|
|
83
|
+
.command('update')
|
|
84
|
+
.description('Update @testspectra/cli and @testspectra/matchers to the latest version from npm')
|
|
85
|
+
.option('--check', 'Only check for available updates without installing')
|
|
52
86
|
.action(updateCommand);
|
|
53
87
|
program
|
|
54
|
-
.command(
|
|
55
|
-
.description(
|
|
56
|
-
.option(
|
|
88
|
+
.command('devices')
|
|
89
|
+
.description('List connected Android/iOS devices and local browsers')
|
|
90
|
+
.option('-t, --target <target>', 'Filter scope: android, ios, web, all', 'all')
|
|
91
|
+
.option('--json', 'Output results as JSON')
|
|
57
92
|
.action(devicesCommand);
|
|
58
93
|
program
|
|
59
|
-
.command(
|
|
60
|
-
.description(
|
|
61
|
-
.option(
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
94
|
+
.command('license [action] [key]')
|
|
95
|
+
.description('Manage TestSpectra commercial license (activate, status, remove)')
|
|
96
|
+
.option('--json', 'Output license details as JSON')
|
|
97
|
+
.addHelpText('after', `
|
|
98
|
+
Examples:
|
|
99
|
+
$ spectra license activate <KEY> Activate license key and auto-configure private driver registry
|
|
100
|
+
$ spectra license status Display active license organization, plan, and expiration
|
|
101
|
+
$ spectra license remove Remove locally activated license credentials`)
|
|
102
|
+
.action(licenseCommand);
|
|
103
|
+
program
|
|
104
|
+
.command('run [path]')
|
|
105
|
+
.description('Execute test suite or individual test case')
|
|
106
|
+
.option('-s, --spec <path>', 'Spec file path to execute')
|
|
107
|
+
.option('-t, --target <target>', 'Target platform: web, android, ios, common', 'web')
|
|
108
|
+
.option('-d, --device <device>', 'Target device name or UDID')
|
|
109
|
+
.option('--all-devices', 'Distribute test execution across all connected devices')
|
|
110
|
+
.option('--multi-device', 'Distribute test execution across all connected devices (alias for --all-devices)')
|
|
111
|
+
.option('--devices <list>', 'Comma-separated list of device UDIDs to run on')
|
|
112
|
+
.option('--license-key <key>', 'Explicit commercial license key override')
|
|
113
|
+
.option('--demo', 'Automatically launch built-in demo web app server during test execution')
|
|
114
|
+
.option('--headless', 'Run in headless mode')
|
|
115
|
+
.option('--no-headless', 'Run in headed mode')
|
|
116
|
+
.option('--headed', 'Run browser in visible GUI mode (alias for --no-headless)')
|
|
117
|
+
.option('--step-delay <ms>', 'Delay between consecutive actions and commands in milliseconds (slow motion)')
|
|
118
|
+
.option('--delay <ms>', 'Alias for --step-delay')
|
|
119
|
+
.option('--failed [targets]', 'Re-run failed tests from previous run (accepts comma-separated test IDs or spec paths)')
|
|
120
|
+
.option('-c, --concurrency <number>', 'Maximum parallel worker concurrency (auto-detected if omitted)')
|
|
121
|
+
.option('-w, --workdir <dir>', 'Custom app data / temp workdir')
|
|
122
|
+
.option('-o, --output <path>', 'Custom test report JSON output path')
|
|
69
123
|
.action(runCommand);
|
|
124
|
+
const cloud = program.command('cloud').description('TestSpectra Cloud commands');
|
|
125
|
+
cloud
|
|
126
|
+
.command('run')
|
|
127
|
+
.description('Trigger a TestSpectra Cloud run (spawns testspectra-runner on the cloud server) and optionally wait for the result')
|
|
128
|
+
.requiredOption('--project <id>', 'Project identifier as registered in TestSpectra')
|
|
129
|
+
.option('--project-name <name>', 'Human-readable project name (defaults to --project)')
|
|
130
|
+
.option('--branch <branch>', 'Branch to run against (defaults to the current git branch)')
|
|
131
|
+
.option('--commit <hash>', 'Commit hash (defaults to the current git HEAD)')
|
|
132
|
+
.option('--platform <platform>', 'Target platform: web | android', 'web')
|
|
133
|
+
.option('--no-wait', 'Return immediately after triggering, without waiting for the result')
|
|
134
|
+
.option('--timeout <duration>', 'Max time to wait for the run to finish (e.g. 30m, 10m)', '30m')
|
|
135
|
+
.addHelpText('after', `
|
|
136
|
+
Requires SPECTRA_URL and SPECTRA_TOKEN (a Gitea Personal Access Token) as
|
|
137
|
+
environment variables — see docs/v2/cloud/git-spec-standards-and-workflow.md.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
$ spectra cloud run --project org/repo --branch main --platform web --wait
|
|
141
|
+
$ spectra cloud run --project org/repo --no-wait`)
|
|
142
|
+
.action(cloudRunCommand);
|
|
70
143
|
return program;
|
|
71
144
|
}
|
|
72
145
|
export async function runCli(args = process.argv) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { discoverGlobalHooks, discoverSuiteHooks } from '../hook-discovery.js';
|
|
6
|
+
import { LifecycleDispatcher, HookExecutionError } from '../hook-dispatcher.js';
|
|
7
|
+
import { calculateWorkerCount, groupSpecs } from '../parallel-grouping.js';
|
|
8
|
+
describe('Core Lifecycle Execution Engine (docs/v2/cli/lifecycle-execution-engine.md)', () => {
|
|
9
|
+
let tmpDir;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-lifecycle-test-'));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
try {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
});
|
|
19
|
+
describe('Hook Discovery', () => {
|
|
20
|
+
it('discovers global-hooks and filters by platform hierarchy', () => {
|
|
21
|
+
const gBefore = path.join(tmpDir, 'global-hooks', 'before');
|
|
22
|
+
const gAfter = path.join(tmpDir, 'global-hooks', 'after');
|
|
23
|
+
fs.mkdirSync(gBefore, { recursive: true });
|
|
24
|
+
fs.mkdirSync(gAfter, { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(gBefore, 'web.hook.ts'), 'export default async () => {}');
|
|
26
|
+
fs.writeFileSync(path.join(gBefore, 'mobile.hook.ts'), 'export default async () => {}');
|
|
27
|
+
fs.writeFileSync(path.join(gBefore, 'common.hook.ts'), 'export default async () => {}');
|
|
28
|
+
fs.writeFileSync(path.join(gAfter, 'web.hook.ts'), 'export default async () => {}');
|
|
29
|
+
const webGlobal = discoverGlobalHooks(tmpDir, 'web');
|
|
30
|
+
expect(webGlobal.before.some((h) => h.endsWith('web.hook.ts'))).toBe(true);
|
|
31
|
+
expect(webGlobal.before.some((h) => h.endsWith('common.hook.ts'))).toBe(true);
|
|
32
|
+
expect(webGlobal.before.some((h) => h.endsWith('mobile.hook.ts'))).toBe(false);
|
|
33
|
+
const mobileGlobal = discoverGlobalHooks(tmpDir, 'mobile');
|
|
34
|
+
expect(mobileGlobal.before.some((h) => h.endsWith('mobile.hook.ts'))).toBe(true);
|
|
35
|
+
expect(mobileGlobal.before.some((h) => h.endsWith('common.hook.ts'))).toBe(true);
|
|
36
|
+
expect(mobileGlobal.before.some((h) => h.endsWith('web.hook.ts'))).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it('discovers suite hooks (before, beforeEach, afterEach, after)', () => {
|
|
39
|
+
const suiteDir = path.join(tmpDir, 'specs', 'Auth');
|
|
40
|
+
for (const hookType of ['before', 'beforeEach', 'afterEach', 'after']) {
|
|
41
|
+
const hDir = path.join(suiteDir, 'hooks', hookType);
|
|
42
|
+
fs.mkdirSync(hDir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(hDir, 'web.hook.ts'), 'export default async () => {}');
|
|
44
|
+
}
|
|
45
|
+
const suiteHooks = discoverSuiteHooks(suiteDir, 'web');
|
|
46
|
+
expect(suiteHooks.before.length).toBe(1);
|
|
47
|
+
expect(suiteHooks.beforeEach.length).toBe(1);
|
|
48
|
+
expect(suiteHooks.afterEach.length).toBe(1);
|
|
49
|
+
expect(suiteHooks.after.length).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('Lifecycle Dispatcher & Execution Flow', () => {
|
|
53
|
+
it('single run executes suite hooks without triggering global hooks', async () => {
|
|
54
|
+
const executionOrder = [];
|
|
55
|
+
const globalBeforeFn = vi.fn(async () => {
|
|
56
|
+
executionOrder.push('globalBefore');
|
|
57
|
+
});
|
|
58
|
+
const globalAfterFn = vi.fn(async () => {
|
|
59
|
+
executionOrder.push('globalAfter');
|
|
60
|
+
});
|
|
61
|
+
const suiteHooks = {
|
|
62
|
+
before: [
|
|
63
|
+
async () => {
|
|
64
|
+
executionOrder.push('suiteBefore');
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
beforeEach: [
|
|
68
|
+
async () => {
|
|
69
|
+
executionOrder.push('beforeEach');
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
afterEach: [
|
|
73
|
+
async () => {
|
|
74
|
+
executionOrder.push('afterEach');
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
after: [
|
|
78
|
+
async () => {
|
|
79
|
+
executionOrder.push('suiteAfter');
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
const dispatcher = new LifecycleDispatcher('single', {
|
|
84
|
+
before: [globalBeforeFn],
|
|
85
|
+
after: [globalAfterFn],
|
|
86
|
+
});
|
|
87
|
+
// Single run bypasses global before & global after
|
|
88
|
+
await dispatcher.runGlobalBefore();
|
|
89
|
+
expect(globalBeforeFn).not.toHaveBeenCalled();
|
|
90
|
+
const result = await dispatcher.executeSuite('AuthSuite', suiteHooks, [
|
|
91
|
+
{
|
|
92
|
+
id: 'TC-01',
|
|
93
|
+
title: 'Valid Login',
|
|
94
|
+
fn: async () => {
|
|
95
|
+
executionOrder.push('testBody');
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
await dispatcher.runGlobalAfter();
|
|
100
|
+
expect(globalAfterFn).not.toHaveBeenCalled();
|
|
101
|
+
expect(result.status).toBe('PASSED');
|
|
102
|
+
expect(executionOrder).toEqual(['suiteBefore', 'beforeEach', 'testBody', 'afterEach', 'suiteAfter']);
|
|
103
|
+
});
|
|
104
|
+
it('global batch run executes global-hooks/before once at start and global-hooks/after once at finish', async () => {
|
|
105
|
+
const executionOrder = [];
|
|
106
|
+
const globalBeforeFn = vi.fn(async () => {
|
|
107
|
+
executionOrder.push('globalBefore');
|
|
108
|
+
});
|
|
109
|
+
const globalAfterFn = vi.fn(async () => {
|
|
110
|
+
executionOrder.push('globalAfter');
|
|
111
|
+
});
|
|
112
|
+
const dispatcher = new LifecycleDispatcher('global', {
|
|
113
|
+
before: [globalBeforeFn],
|
|
114
|
+
after: [globalAfterFn],
|
|
115
|
+
});
|
|
116
|
+
await dispatcher.runGlobalBefore();
|
|
117
|
+
expect(globalBeforeFn).toHaveBeenCalledTimes(1);
|
|
118
|
+
const suiteHooks = {
|
|
119
|
+
before: [
|
|
120
|
+
async () => {
|
|
121
|
+
executionOrder.push('suiteBefore');
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
beforeEach: [
|
|
125
|
+
async () => {
|
|
126
|
+
executionOrder.push('beforeEach');
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
afterEach: [
|
|
130
|
+
async () => {
|
|
131
|
+
executionOrder.push('afterEach');
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
after: [
|
|
135
|
+
async () => {
|
|
136
|
+
executionOrder.push('suiteAfter');
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
const result = await dispatcher.executeSuite('AuthSuite', suiteHooks, [
|
|
141
|
+
{
|
|
142
|
+
id: 'TC-01',
|
|
143
|
+
title: 'Test 1',
|
|
144
|
+
fn: async () => {
|
|
145
|
+
executionOrder.push('tc1');
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'TC-02',
|
|
150
|
+
title: 'Test 2',
|
|
151
|
+
fn: async () => {
|
|
152
|
+
executionOrder.push('tc2');
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
await dispatcher.runGlobalAfter();
|
|
157
|
+
expect(globalAfterFn).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(result.status).toBe('PASSED');
|
|
159
|
+
expect(executionOrder).toEqual([
|
|
160
|
+
'globalBefore',
|
|
161
|
+
'suiteBefore',
|
|
162
|
+
'beforeEach',
|
|
163
|
+
'tc1',
|
|
164
|
+
'afterEach',
|
|
165
|
+
'beforeEach',
|
|
166
|
+
'tc2',
|
|
167
|
+
'afterEach',
|
|
168
|
+
'suiteAfter',
|
|
169
|
+
'globalAfter',
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
it('when beforeEach fails, test body is skipped, afterEach is guaranteed to execute, and test is marked as ERROR', async () => {
|
|
173
|
+
const testBodyFn = vi.fn();
|
|
174
|
+
let afterEachRan = false;
|
|
175
|
+
const suiteHooks = {
|
|
176
|
+
before: [],
|
|
177
|
+
beforeEach: [
|
|
178
|
+
async () => {
|
|
179
|
+
throw new Error('Database connection failed in beforeEach');
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
afterEach: [
|
|
183
|
+
async () => {
|
|
184
|
+
afterEachRan = true;
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
after: [],
|
|
188
|
+
};
|
|
189
|
+
const dispatcher = new LifecycleDispatcher('single');
|
|
190
|
+
const result = await dispatcher.executeSuite('FailingBeforeEachSuite', suiteHooks, [
|
|
191
|
+
{
|
|
192
|
+
id: 'TC-FAIL-01',
|
|
193
|
+
title: 'Should be skipped',
|
|
194
|
+
fn: testBodyFn,
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
expect(testBodyFn).not.toHaveBeenCalled();
|
|
198
|
+
expect(afterEachRan).toBe(true);
|
|
199
|
+
expect(result.results[0].status).toBe('ERROR');
|
|
200
|
+
expect(result.results[0].error?.message).toContain('Database connection failed in beforeEach');
|
|
201
|
+
expect(result.status).toBe('ERROR');
|
|
202
|
+
});
|
|
203
|
+
it('when afterEach fails, test transitions to ERROR status', async () => {
|
|
204
|
+
const suiteHooks = {
|
|
205
|
+
before: [],
|
|
206
|
+
beforeEach: [],
|
|
207
|
+
afterEach: [
|
|
208
|
+
async () => {
|
|
209
|
+
throw new Error('Teardown resource leak in afterEach');
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
after: [],
|
|
213
|
+
};
|
|
214
|
+
const dispatcher = new LifecycleDispatcher('single');
|
|
215
|
+
const result = await dispatcher.executeSuite('TeardownFailSuite', suiteHooks, [
|
|
216
|
+
{
|
|
217
|
+
id: 'TC-01',
|
|
218
|
+
title: 'Passing Body but failing afterEach',
|
|
219
|
+
fn: async () => {
|
|
220
|
+
// Passing body
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
expect(result.results[0].status).toBe('ERROR');
|
|
225
|
+
expect(result.results[0].error?.message).toContain('Teardown resource leak in afterEach');
|
|
226
|
+
expect(result.status).toBe('ERROR');
|
|
227
|
+
});
|
|
228
|
+
it('when global-hooks/before fails, throws HookExecutionError immediately', async () => {
|
|
229
|
+
const dispatcher = new LifecycleDispatcher('global', {
|
|
230
|
+
before: [
|
|
231
|
+
async () => {
|
|
232
|
+
throw new Error('Global health check failed');
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
after: [],
|
|
236
|
+
});
|
|
237
|
+
await expect(dispatcher.runGlobalBefore()).rejects.toThrow(HookExecutionError);
|
|
238
|
+
await expect(dispatcher.runGlobalBefore()).rejects.toThrow('Global health check failed');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('Parallel Spec Grouping & Dynamic Worker Sizing', () => {
|
|
242
|
+
it('prioritizes explicit user concurrency flag over auto-calculation', () => {
|
|
243
|
+
const targets = Array.from({ length: 10 }, () => 'spec.ts');
|
|
244
|
+
expect(calculateWorkerCount(targets, { userConcurrency: 4 })).toBe(4);
|
|
245
|
+
// Clamps to item count if items < userConcurrency
|
|
246
|
+
expect(calculateWorkerCount(['only-one.ts'], { userConcurrency: 4 })).toBe(1);
|
|
247
|
+
});
|
|
248
|
+
it('enforces low-CPU safety ratio based on container/host CPU quota', () => {
|
|
249
|
+
const targets = Array.from({ length: 10 }, () => 'spec.ts');
|
|
250
|
+
const highRam = 16 * 1024 * 1024 * 1024; // 16 GB
|
|
251
|
+
// CPU <= 2 (e.g. 2-vCPU standard CI runner): clamped to 1 worker
|
|
252
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 1, memoryBytesOverride: highRam })).toBe(1);
|
|
253
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 2, memoryBytesOverride: highRam })).toBe(1);
|
|
254
|
+
// CPU 3 - 4: allocates 2 workers
|
|
255
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 3, memoryBytesOverride: highRam })).toBe(2);
|
|
256
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 4, memoryBytesOverride: highRam })).toBe(2);
|
|
257
|
+
// CPU > 4: allocates (cpu - 2) capped at maxWorkersCap (default 6)
|
|
258
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 6, memoryBytesOverride: highRam })).toBe(4);
|
|
259
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 8, memoryBytesOverride: highRam })).toBe(6);
|
|
260
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: 64, memoryBytesOverride: highRam })).toBe(6);
|
|
261
|
+
});
|
|
262
|
+
it('enforces memory-aware guard to prevent OOM kills during browser automation', () => {
|
|
263
|
+
const targets = Array.from({ length: 10 }, () => 'spec.ts');
|
|
264
|
+
const highCpu = 16;
|
|
265
|
+
// 500 MB RAM budget -> allows at most 1 worker (~600MB budget per browser)
|
|
266
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: highCpu, memoryBytesOverride: 500 * 1024 * 1024 })).toBe(1);
|
|
267
|
+
// 1.5 GB RAM budget -> allows 2 workers
|
|
268
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: highCpu, memoryBytesOverride: 1500 * 1024 * 1024 })).toBe(2);
|
|
269
|
+
// 2.5 GB RAM budget -> allows 4 workers
|
|
270
|
+
expect(calculateWorkerCount(targets, { cpuQuotaOverride: highCpu, memoryBytesOverride: 2500 * 1024 * 1024 })).toBe(4);
|
|
271
|
+
});
|
|
272
|
+
it('groups specs by suite directory in nested array format when mode is "suite"', () => {
|
|
273
|
+
const specs = [
|
|
274
|
+
'specs/Auth/TC-001/web.test.ts',
|
|
275
|
+
'specs/Auth/TC-002/web.test.ts',
|
|
276
|
+
'specs/Cart/TC-010/web.test.ts',
|
|
277
|
+
'specs/Cart/TC-011/web.test.ts',
|
|
278
|
+
];
|
|
279
|
+
const grouped = groupSpecs(specs, 'suite');
|
|
280
|
+
expect(grouped.length).toBe(2);
|
|
281
|
+
expect(grouped[0]).toEqual(['specs/Auth/TC-001/web.test.ts', 'specs/Auth/TC-002/web.test.ts']);
|
|
282
|
+
expect(grouped[1]).toEqual(['specs/Cart/TC-010/web.test.ts', 'specs/Cart/TC-011/web.test.ts']);
|
|
283
|
+
});
|
|
284
|
+
it('flattens specs when mode is "testcase"', () => {
|
|
285
|
+
const specs = ['specs/Auth/TC-001/web.test.ts', 'specs/Auth/TC-002/web.test.ts', 'specs/Cart/TC-010/web.test.ts'];
|
|
286
|
+
const flat = groupSpecs(specs, 'testcase');
|
|
287
|
+
expect(flat).toEqual(specs);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PlatformTarget } from '../generator/type-generator.js';
|
|
2
|
+
export type HookEntry = string | (() => Promise<void> | void);
|
|
3
|
+
export interface DiscoveredSuiteHooks {
|
|
4
|
+
before: string[];
|
|
5
|
+
beforeEach: string[];
|
|
6
|
+
afterEach: string[];
|
|
7
|
+
after: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface DiscoveredGlobalHooks {
|
|
10
|
+
before: string[];
|
|
11
|
+
after: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface SuiteHooks {
|
|
14
|
+
before: HookEntry[];
|
|
15
|
+
beforeEach: HookEntry[];
|
|
16
|
+
afterEach: HookEntry[];
|
|
17
|
+
after: HookEntry[];
|
|
18
|
+
}
|
|
19
|
+
export interface GlobalHooks {
|
|
20
|
+
before: HookEntry[];
|
|
21
|
+
after: HookEntry[];
|
|
22
|
+
}
|
|
23
|
+
export declare function discoverGlobalHooks(cwd: string, platform?: PlatformTarget): DiscoveredGlobalHooks;
|
|
24
|
+
export declare function discoverSuiteHooks(suiteDir: string, platform?: PlatformTarget): DiscoveredSuiteHooks;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PLATFORM_HIERARCHY } from '../generator/type-generator.js';
|
|
4
|
+
function resolveHookFilesForType(dir, platform = 'web') {
|
|
5
|
+
if (!fs.existsSync(dir))
|
|
6
|
+
return [];
|
|
7
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8
|
+
const hierarchy = PLATFORM_HIERARCHY[platform] || ['common'];
|
|
9
|
+
const matched = [];
|
|
10
|
+
// Group files by base name if applicable or collect directly
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
if (!entry.isFile() || !entry.name.endsWith('.hook.ts'))
|
|
13
|
+
continue;
|
|
14
|
+
const parts = entry.name.split('.');
|
|
15
|
+
// Formats:
|
|
16
|
+
// 1. web.hook.ts -> parts = ['web', 'hook', 'ts']
|
|
17
|
+
// 2. auth.web.hook.ts -> parts = ['auth', 'web', 'hook', 'ts']
|
|
18
|
+
// 3. auth.hook.ts -> parts = ['auth', 'hook', 'ts']
|
|
19
|
+
let filePlatform = 'common';
|
|
20
|
+
if (parts.length === 3) {
|
|
21
|
+
filePlatform = parts[0];
|
|
22
|
+
}
|
|
23
|
+
else if (parts.length >= 4) {
|
|
24
|
+
filePlatform = parts[parts.length - 3];
|
|
25
|
+
}
|
|
26
|
+
if (hierarchy.includes(filePlatform) || filePlatform === 'common') {
|
|
27
|
+
matched.push(path.join(dir, entry.name));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Sort by hierarchy priority
|
|
31
|
+
matched.sort((a, b) => {
|
|
32
|
+
const getPriority = (filePath) => {
|
|
33
|
+
const base = path.basename(filePath);
|
|
34
|
+
for (let i = 0; i < hierarchy.length; i++) {
|
|
35
|
+
if (base.includes(`.${hierarchy[i]}.`) || base.startsWith(`${hierarchy[i]}.`)) {
|
|
36
|
+
return i;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return hierarchy.length;
|
|
40
|
+
};
|
|
41
|
+
return getPriority(a) - getPriority(b);
|
|
42
|
+
});
|
|
43
|
+
return matched;
|
|
44
|
+
}
|
|
45
|
+
export function discoverGlobalHooks(cwd, platform = 'web') {
|
|
46
|
+
const globalHooksDir = path.join(cwd, 'global-hooks');
|
|
47
|
+
return {
|
|
48
|
+
before: resolveHookFilesForType(path.join(globalHooksDir, 'before'), platform),
|
|
49
|
+
after: resolveHookFilesForType(path.join(globalHooksDir, 'after'), platform),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function discoverSuiteHooks(suiteDir, platform = 'web') {
|
|
53
|
+
const hooksDir = path.join(suiteDir, 'hooks');
|
|
54
|
+
return {
|
|
55
|
+
before: resolveHookFilesForType(path.join(hooksDir, 'before'), platform),
|
|
56
|
+
beforeEach: resolveHookFilesForType(path.join(hooksDir, 'beforeEach'), platform),
|
|
57
|
+
afterEach: resolveHookFilesForType(path.join(hooksDir, 'afterEach'), platform),
|
|
58
|
+
after: resolveHookFilesForType(path.join(hooksDir, 'after'), platform),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|