@testspectra/cli 1.0.70 → 1.1.0
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/commands/add.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { fileURLToPath } from
|
|
4
|
-
import * as p from
|
|
5
|
-
import chalk from
|
|
6
|
-
import { TypeGenerator } from
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { TypeGenerator } from '../types/generator.js';
|
|
7
7
|
function parsePnpmWorkspaceGlobs(content) {
|
|
8
|
-
const lines = content.split(
|
|
8
|
+
const lines = content.split('\n');
|
|
9
9
|
const globs = [];
|
|
10
10
|
let inPackages = false;
|
|
11
11
|
for (const line of lines) {
|
|
12
12
|
const trimmed = line.trim();
|
|
13
|
-
if (trimmed.startsWith(
|
|
13
|
+
if (trimmed.startsWith('packages:')) {
|
|
14
14
|
inPackages = true;
|
|
15
15
|
continue;
|
|
16
16
|
}
|
|
17
17
|
if (inPackages) {
|
|
18
|
-
if (trimmed.startsWith(
|
|
19
|
-
const item = trimmed.replace(/^-\s*['"]?/,
|
|
18
|
+
if (trimmed.startsWith('-')) {
|
|
19
|
+
const item = trimmed.replace(/^-\s*['"]?/, '').replace(/['"]?\s*$/, '');
|
|
20
20
|
if (item)
|
|
21
21
|
globs.push(item);
|
|
22
22
|
}
|
|
23
|
-
else if (trimmed && !trimmed.startsWith(
|
|
23
|
+
else if (trimmed && !trimmed.startsWith('#')) {
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -34,11 +34,11 @@ function scanDirectoriesForProjects(cwd, patterns) {
|
|
|
34
34
|
if (currentDepth > maxDepth || !fs.existsSync(dir))
|
|
35
35
|
return;
|
|
36
36
|
const base = path.basename(dir);
|
|
37
|
-
if (base ===
|
|
37
|
+
if (base === 'node_modules' || base === 'dist' || base === '.testspectra' || base === '.git' || base === 'shared') {
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
const hasPkg = fs.existsSync(path.join(dir,
|
|
41
|
-
const hasNx = fs.existsSync(path.join(dir,
|
|
40
|
+
const hasPkg = fs.existsSync(path.join(dir, 'package.json'));
|
|
41
|
+
const hasNx = fs.existsSync(path.join(dir, 'project.json'));
|
|
42
42
|
if ((hasPkg || hasNx) && dir !== cwd) {
|
|
43
43
|
const rel = path.relative(cwd, dir);
|
|
44
44
|
if (!visited.has(rel)) {
|
|
@@ -46,7 +46,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
|
|
|
46
46
|
let name = path.basename(dir);
|
|
47
47
|
if (hasPkg) {
|
|
48
48
|
try {
|
|
49
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dir,
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
50
50
|
if (pkg.name)
|
|
51
51
|
name = pkg.name;
|
|
52
52
|
}
|
|
@@ -54,7 +54,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
|
|
|
54
54
|
}
|
|
55
55
|
else if (hasNx) {
|
|
56
56
|
try {
|
|
57
|
-
const nxJson = JSON.parse(fs.readFileSync(path.join(dir,
|
|
57
|
+
const nxJson = JSON.parse(fs.readFileSync(path.join(dir, 'project.json'), 'utf-8'));
|
|
58
58
|
if (nxJson.name)
|
|
59
59
|
name = nxJson.name;
|
|
60
60
|
}
|
|
@@ -81,7 +81,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
|
|
|
81
81
|
}
|
|
82
82
|
if (patterns.length > 0) {
|
|
83
83
|
for (const pat of patterns) {
|
|
84
|
-
const cleanPat = pat.replace(/\/\*\*?$/,
|
|
84
|
+
const cleanPat = pat.replace(/\/\*\*?$/, '').replace(/\/\*$/, '');
|
|
85
85
|
const searchRoot = path.join(cwd, cleanPat);
|
|
86
86
|
if (fs.existsSync(searchRoot)) {
|
|
87
87
|
const stat = fs.statSync(searchRoot);
|
|
@@ -92,7 +92,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
else {
|
|
95
|
-
for (const fallback of [
|
|
95
|
+
for (const fallback of ['packages', 'modules', 'apps', 'libs', 'src']) {
|
|
96
96
|
const fbPath = path.join(cwd, fallback);
|
|
97
97
|
if (fs.existsSync(fbPath)) {
|
|
98
98
|
search(fbPath, 1, 3);
|
|
@@ -107,43 +107,43 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
107
107
|
let rootDir = cwd;
|
|
108
108
|
let cur = cwd;
|
|
109
109
|
while (cur !== path.dirname(cur)) {
|
|
110
|
-
if (fs.existsSync(path.join(cur,
|
|
111
|
-
fs.existsSync(path.join(cur,
|
|
112
|
-
fs.existsSync(path.join(cur,
|
|
110
|
+
if (fs.existsSync(path.join(cur, 'spectra.config.ts')) ||
|
|
111
|
+
fs.existsSync(path.join(cur, 'pnpm-workspace.yaml')) ||
|
|
112
|
+
fs.existsSync(path.join(cur, 'nx.json'))) {
|
|
113
113
|
rootDir = cur;
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
116
116
|
cur = path.dirname(cur);
|
|
117
117
|
}
|
|
118
|
-
p.intro(chalk.bold.cyan(
|
|
118
|
+
p.intro(chalk.bold.cyan('✨ TestSpectra Module Generator'));
|
|
119
119
|
// Check if we are in a monorepo
|
|
120
|
-
const pnpmWorkspacePath = path.join(rootDir,
|
|
121
|
-
const nxJsonPath = path.join(rootDir,
|
|
120
|
+
const pnpmWorkspacePath = path.join(rootDir, 'pnpm-workspace.yaml');
|
|
121
|
+
const nxJsonPath = path.join(rootDir, 'nx.json');
|
|
122
122
|
const isMonorepo = fs.existsSync(pnpmWorkspacePath) || fs.existsSync(nxJsonPath);
|
|
123
123
|
// 2. Resolve template directory
|
|
124
124
|
const __filename = fileURLToPath(import.meta.url);
|
|
125
125
|
const __dirname = path.dirname(__filename);
|
|
126
|
-
let templateDir = path.resolve(__dirname,
|
|
126
|
+
let templateDir = path.resolve(__dirname, '../templates/nx');
|
|
127
127
|
if (!fs.existsSync(templateDir)) {
|
|
128
|
-
templateDir = path.resolve(__dirname,
|
|
128
|
+
templateDir = path.resolve(__dirname, '../../templates/nx');
|
|
129
129
|
}
|
|
130
130
|
if (!fs.existsSync(templateDir)) {
|
|
131
|
-
templateDir = path.resolve(__dirname,
|
|
131
|
+
templateDir = path.resolve(__dirname, '../../../templates/nx');
|
|
132
132
|
}
|
|
133
|
-
const sampleE2eSrc = path.join(templateDir,
|
|
133
|
+
const sampleE2eSrc = path.join(templateDir, 'packages/playground/e2e');
|
|
134
134
|
if (!fs.existsSync(sampleE2eSrc)) {
|
|
135
135
|
p.cancel(chalk.red(`Template directory not found at: ${sampleE2eSrc}`));
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
// 3. Resolve CLI dependency version
|
|
139
|
-
let cliVersion =
|
|
139
|
+
let cliVersion = '^1.0.65';
|
|
140
140
|
try {
|
|
141
141
|
let pDir = __dirname;
|
|
142
142
|
while (pDir !== path.dirname(pDir)) {
|
|
143
|
-
const cand = path.join(pDir,
|
|
143
|
+
const cand = path.join(pDir, 'package.json');
|
|
144
144
|
if (fs.existsSync(cand)) {
|
|
145
|
-
const cliPkg = JSON.parse(fs.readFileSync(cand,
|
|
146
|
-
if (cliPkg.name ===
|
|
145
|
+
const cliPkg = JSON.parse(fs.readFileSync(cand, 'utf-8'));
|
|
146
|
+
if (cliPkg.name === '@testspectra/cli' && cliPkg.version) {
|
|
147
147
|
cliVersion = `^${cliPkg.version}`;
|
|
148
148
|
break;
|
|
149
149
|
}
|
|
@@ -152,10 +152,10 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
catch { }
|
|
155
|
-
const cliDepVersion = cliVersion.startsWith(
|
|
155
|
+
const cliDepVersion = cliVersion.startsWith('^') ? cliVersion : `^${cliVersion}`;
|
|
156
156
|
function copyRecursive(src, dest, replacements = {}) {
|
|
157
157
|
const base = path.basename(src);
|
|
158
|
-
if (base ===
|
|
158
|
+
if (base === 'node_modules' || base === 'dist' || base === '.testspectra' || base === '.git') {
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
const stat = fs.statSync(src);
|
|
@@ -172,23 +172,23 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
172
172
|
if (fs.existsSync(dest) && !options.force) {
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
|
-
let content = fs.readFileSync(src,
|
|
175
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
176
176
|
for (const [k, v] of Object.entries(replacements)) {
|
|
177
177
|
content = content.replaceAll(k, v);
|
|
178
178
|
}
|
|
179
|
-
if (dest.endsWith(
|
|
179
|
+
if (dest.endsWith('package.json')) {
|
|
180
180
|
try {
|
|
181
181
|
const pkg = JSON.parse(content);
|
|
182
182
|
if (pkg.devDependencies) {
|
|
183
183
|
for (const key of Object.keys(pkg.devDependencies)) {
|
|
184
|
-
if (key.startsWith(
|
|
184
|
+
if (key.startsWith('@testspectra/') || pkg.devDependencies[key] === 'workspace:*') {
|
|
185
185
|
pkg.devDependencies[key] = cliDepVersion;
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
if (pkg.dependencies) {
|
|
190
190
|
for (const key of Object.keys(pkg.dependencies)) {
|
|
191
|
-
if (key.startsWith(
|
|
191
|
+
if (key.startsWith('@testspectra/') || pkg.dependencies[key] === 'workspace:*') {
|
|
192
192
|
pkg.dependencies[key] = cliDepVersion;
|
|
193
193
|
}
|
|
194
194
|
}
|
|
@@ -197,7 +197,7 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
197
197
|
}
|
|
198
198
|
catch { }
|
|
199
199
|
}
|
|
200
|
-
fs.writeFileSync(dest, content,
|
|
200
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
// If in a Monorepo
|
|
@@ -205,13 +205,13 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
205
205
|
let pnpmPatterns = [];
|
|
206
206
|
if (fs.existsSync(pnpmWorkspacePath)) {
|
|
207
207
|
try {
|
|
208
|
-
pnpmPatterns = parsePnpmWorkspaceGlobs(fs.readFileSync(pnpmWorkspacePath,
|
|
208
|
+
pnpmPatterns = parsePnpmWorkspaceGlobs(fs.readFileSync(pnpmWorkspacePath, 'utf-8'));
|
|
209
209
|
}
|
|
210
210
|
catch { }
|
|
211
211
|
}
|
|
212
212
|
const detectedProjects = scanDirectoriesForProjects(rootDir, pnpmPatterns);
|
|
213
|
-
let targetProjectPath =
|
|
214
|
-
let featureName =
|
|
213
|
+
let targetProjectPath = '';
|
|
214
|
+
let featureName = '';
|
|
215
215
|
if (moduleNameArg) {
|
|
216
216
|
// Find if moduleNameArg matches an existing detected package
|
|
217
217
|
const match = detectedProjects.find((p) => p.name === moduleNameArg || p.relPath === moduleNameArg || path.basename(p.relPath) === moduleNameArg);
|
|
@@ -226,35 +226,35 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
226
226
|
}
|
|
227
227
|
else {
|
|
228
228
|
const mode = await p.select({
|
|
229
|
-
message:
|
|
229
|
+
message: 'How would you like to add the TestSpectra module?',
|
|
230
230
|
options: [
|
|
231
231
|
{
|
|
232
|
-
label:
|
|
233
|
-
value:
|
|
234
|
-
hint:
|
|
232
|
+
label: 'Add E2E to an existing workspace project / feature',
|
|
233
|
+
value: 'existing',
|
|
234
|
+
hint: 'Attach E2E test suite to a detected workspace package',
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
|
-
label:
|
|
238
|
-
value:
|
|
239
|
-
hint:
|
|
237
|
+
label: 'Create a new custom feature / module path',
|
|
238
|
+
value: 'custom',
|
|
239
|
+
hint: 'Specify a custom path e.g. packages/features/billing',
|
|
240
240
|
},
|
|
241
241
|
],
|
|
242
242
|
});
|
|
243
243
|
if (p.isCancel(mode)) {
|
|
244
|
-
p.cancel(
|
|
244
|
+
p.cancel('Operation cancelled.');
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
|
-
if (mode ===
|
|
247
|
+
if (mode === 'existing' && detectedProjects.length > 0) {
|
|
248
248
|
const choice = await p.select({
|
|
249
|
-
message:
|
|
249
|
+
message: 'Select workspace project to add TestSpectra E2E testing to:',
|
|
250
250
|
options: detectedProjects.map((proj) => ({
|
|
251
251
|
value: proj.relPath,
|
|
252
252
|
label: `${proj.relPath} (${proj.name})`,
|
|
253
|
-
hint: proj.hasNxProject ?
|
|
253
|
+
hint: proj.hasNxProject ? 'Nx project.json' : 'package.json',
|
|
254
254
|
})),
|
|
255
255
|
});
|
|
256
256
|
if (p.isCancel(choice)) {
|
|
257
|
-
p.cancel(
|
|
257
|
+
p.cancel('Operation cancelled.');
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
targetProjectPath = choice;
|
|
@@ -262,22 +262,22 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
262
262
|
}
|
|
263
263
|
else {
|
|
264
264
|
const customPath = await p.text({
|
|
265
|
-
message:
|
|
266
|
-
placeholder:
|
|
265
|
+
message: 'Enter path for the new feature module (e.g. packages/features/payment):',
|
|
266
|
+
placeholder: 'packages/features/new-feature',
|
|
267
267
|
validate: (val) => {
|
|
268
268
|
if (!val || !val.trim())
|
|
269
|
-
return
|
|
269
|
+
return 'Path cannot be empty';
|
|
270
270
|
},
|
|
271
271
|
});
|
|
272
272
|
if (p.isCancel(customPath)) {
|
|
273
|
-
p.cancel(
|
|
273
|
+
p.cancel('Operation cancelled.');
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
276
276
|
targetProjectPath = customPath.trim();
|
|
277
277
|
featureName = path.basename(targetProjectPath);
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
-
const e2eFolderName = options.e2eFolder ||
|
|
280
|
+
const e2eFolderName = options.e2eFolder || 'e2e';
|
|
281
281
|
const targetE2eDir = path.join(rootDir, targetProjectPath, e2eFolderName);
|
|
282
282
|
const e2eProjectName = `${featureName}-${e2eFolderName}`;
|
|
283
283
|
if (fs.existsSync(targetE2eDir) && !options.force) {
|
|
@@ -287,38 +287,39 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
287
287
|
const s = p.spinner();
|
|
288
288
|
s.start(`Scaffolding ${e2eProjectName} in ./${path.relative(rootDir, targetE2eDir)}...`);
|
|
289
289
|
copyRecursive(sampleE2eSrc, targetE2eDir, {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
'playground-e2e': e2eProjectName,
|
|
291
|
+
'packages/playground/e2e': path.relative(rootDir, targetE2eDir).replace(/\\/g, '/'),
|
|
292
|
+
'scope:playground': `scope:${featureName}`,
|
|
293
293
|
});
|
|
294
294
|
// Adjust project.json
|
|
295
|
-
const projJsonPath = path.join(targetE2eDir,
|
|
295
|
+
const projJsonPath = path.join(targetE2eDir, 'project.json');
|
|
296
296
|
if (fs.existsSync(projJsonPath)) {
|
|
297
297
|
try {
|
|
298
|
-
const pObj = JSON.parse(fs.readFileSync(projJsonPath,
|
|
298
|
+
const pObj = JSON.parse(fs.readFileSync(projJsonPath, 'utf-8'));
|
|
299
299
|
pObj.name = e2eProjectName;
|
|
300
|
-
pObj.sourceRoot = path.relative(rootDir, targetE2eDir).replace(/\\/g,
|
|
301
|
-
pObj.targets.e2e.options.cwd = path.relative(rootDir, targetE2eDir).replace(/\\/g,
|
|
302
|
-
pObj.targets[
|
|
303
|
-
pObj.tags = [`scope:${featureName}`,
|
|
304
|
-
fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2),
|
|
300
|
+
pObj.sourceRoot = path.relative(rootDir, targetE2eDir).replace(/\\/g, '/');
|
|
301
|
+
pObj.targets.e2e.options.cwd = path.relative(rootDir, targetE2eDir).replace(/\\/g, '/');
|
|
302
|
+
pObj.targets['type-check'].options.cwd = '.';
|
|
303
|
+
pObj.tags = [`scope:${featureName}`, 'type:e2e'];
|
|
304
|
+
fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
|
|
305
305
|
}
|
|
306
306
|
catch { }
|
|
307
307
|
}
|
|
308
308
|
// Ensure no local spectra.config.ts (centralized in root)
|
|
309
|
-
const localConfig = path.join(targetE2eDir,
|
|
309
|
+
const localConfig = path.join(targetE2eDir, 'spectra.config.ts');
|
|
310
310
|
if (fs.existsSync(localConfig))
|
|
311
311
|
fs.unlinkSync(localConfig);
|
|
312
|
-
// Ensure Root Solution tsconfig.json references tsconfig.
|
|
313
|
-
const rootTsConfigPath = path.join(rootDir,
|
|
312
|
+
// Ensure Root Solution tsconfig.json references .testspectra/tsconfig.json
|
|
313
|
+
const rootTsConfigPath = path.join(rootDir, 'tsconfig.json');
|
|
314
314
|
if (fs.existsSync(rootTsConfigPath)) {
|
|
315
315
|
try {
|
|
316
|
-
const rootTsConfig = JSON.parse(fs.readFileSync(rootTsConfigPath,
|
|
316
|
+
const rootTsConfig = JSON.parse(fs.readFileSync(rootTsConfigPath, 'utf-8'));
|
|
317
317
|
if (!rootTsConfig.references)
|
|
318
318
|
rootTsConfig.references = [];
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
rootTsConfig.references = rootTsConfig.references.filter((r) => r.path !== './tsconfig.spectra.json');
|
|
320
|
+
if (!rootTsConfig.references.some((r) => r.path === './.testspectra/tsconfig.json')) {
|
|
321
|
+
rootTsConfig.references.push({ path: './.testspectra/tsconfig.json' });
|
|
322
|
+
fs.writeFileSync(rootTsConfigPath, JSON.stringify(rootTsConfig, null, 2), 'utf-8');
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
catch { }
|
|
@@ -329,38 +330,38 @@ export async function addCommand(moduleNameArg, options = {}) {
|
|
|
329
330
|
p.log.success(chalk.green(`Directory: ./${path.relative(rootDir, targetE2eDir)}`));
|
|
330
331
|
p.log.success(chalk.green(`Nx Target: ${e2eProjectName}:e2e`));
|
|
331
332
|
p.log.message(chalk.cyan(`Run tests: pnpm nx run ${e2eProjectName}:e2e`));
|
|
332
|
-
p.outro(chalk.bold.green(
|
|
333
|
+
p.outro(chalk.bold.green('🎉 New TestSpectra module ready!'));
|
|
333
334
|
return;
|
|
334
335
|
}
|
|
335
336
|
// Standalone mode: adding a new spec suite in specs/<ModuleName>
|
|
336
337
|
let targetModule = moduleNameArg;
|
|
337
338
|
if (!targetModule) {
|
|
338
339
|
const input = await p.text({
|
|
339
|
-
message:
|
|
340
|
-
placeholder:
|
|
340
|
+
message: 'Enter name of the new test suite/module (e.g. Payment, Profile):',
|
|
341
|
+
placeholder: 'Checkout',
|
|
341
342
|
validate: (val) => {
|
|
342
343
|
if (!val || !val.trim())
|
|
343
|
-
return
|
|
344
|
+
return 'Module name cannot be empty';
|
|
344
345
|
},
|
|
345
346
|
});
|
|
346
347
|
if (p.isCancel(input)) {
|
|
347
|
-
p.cancel(
|
|
348
|
+
p.cancel('Operation cancelled.');
|
|
348
349
|
return;
|
|
349
350
|
}
|
|
350
351
|
targetModule = input.trim();
|
|
351
352
|
}
|
|
352
|
-
const specDir = path.join(cwd,
|
|
353
|
-
const poDir = path.join(cwd,
|
|
353
|
+
const specDir = path.join(cwd, 'specs', targetModule, 'TC-01');
|
|
354
|
+
const poDir = path.join(cwd, 'page-objects', `${targetModule}Page`);
|
|
354
355
|
if (!fs.existsSync(specDir))
|
|
355
356
|
fs.mkdirSync(specDir, { recursive: true });
|
|
356
357
|
if (!fs.existsSync(poDir))
|
|
357
358
|
fs.mkdirSync(poDir, { recursive: true });
|
|
358
359
|
// Create starter page object
|
|
359
360
|
const poContent = `class ${targetModule}Page {\n get mainTitle() {\n return $("h1");\n }\n\n async open() {\n await Spectra.navigate("/${targetModule.toLowerCase()}");\n }\n}\n\nexport default new ${targetModule}Page();\n`;
|
|
360
|
-
fs.writeFileSync(path.join(poDir,
|
|
361
|
+
fs.writeFileSync(path.join(poDir, 'common.ts'), poContent, 'utf-8');
|
|
361
362
|
// Create starter test spec (Zero-import, flat it() block)
|
|
362
363
|
const specContent = `it("should verify ${targetModule} page components", async () => {\n await ${targetModule}Page.open();\n await ${targetModule}Page.mainTitle.shouldBeVisible();\n});\n`;
|
|
363
|
-
fs.writeFileSync(path.join(specDir,
|
|
364
|
+
fs.writeFileSync(path.join(specDir, 'web.test.ts'), specContent, 'utf-8');
|
|
364
365
|
TypeGenerator.writeDeclarationFiles(cwd);
|
|
365
366
|
p.log.success(chalk.green(`Created Page Object: ./page-objects/${targetModule}Page/common.ts`));
|
|
366
367
|
p.log.success(chalk.green(`Created Test Spec: ./specs/${targetModule}/TC-01/web.test.ts`));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CloudRunOptions {
|
|
2
|
+
project: string;
|
|
3
|
+
projectName?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
commit?: string;
|
|
6
|
+
platform?: string;
|
|
7
|
+
wait?: boolean;
|
|
8
|
+
timeout?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseTimeoutMs(timeout: string): number;
|
|
11
|
+
export declare function cloudRunCommand(options: CloudRunOptions): Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
const POLL_INTERVAL_MS = 5000;
|
|
5
|
+
const DEFAULT_TIMEOUT = '30m';
|
|
6
|
+
function detectGitRef(...args) {
|
|
7
|
+
try {
|
|
8
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseTimeoutMs(timeout) {
|
|
15
|
+
const match = /^(\d+)(s|m|h)?$/.exec(timeout.trim());
|
|
16
|
+
if (!match)
|
|
17
|
+
return 30 * 60 * 1000;
|
|
18
|
+
const value = Number(match[1]);
|
|
19
|
+
const unit = match[2] ?? 'm';
|
|
20
|
+
const multiplier = unit === 's' ? 1000 : unit === 'h' ? 3_600_000 : 60_000;
|
|
21
|
+
return value * multiplier;
|
|
22
|
+
}
|
|
23
|
+
// Same credential path as the trigger endpoint expects: a Gitea PAT, not the
|
|
24
|
+
// dashboard's OAuth2 session — a CI runner can't complete a browser redirect.
|
|
25
|
+
// See docs/v2/cloud/git-spec-standards-and-workflow.md.
|
|
26
|
+
function requireEnv(name) {
|
|
27
|
+
const value = process.env[name];
|
|
28
|
+
if (!value) {
|
|
29
|
+
console.error(chalk.red(`Missing required environment variable: ${name}`));
|
|
30
|
+
console.error(chalk.gray('Set it as a CI secret — see docs/v2/cloud/git-spec-standards-and-workflow.md'));
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export async function cloudRunCommand(options) {
|
|
35
|
+
const spectraUrl = requireEnv('SPECTRA_URL')?.replace(/\/$/, '');
|
|
36
|
+
const spectraToken = requireEnv('SPECTRA_TOKEN');
|
|
37
|
+
if (!spectraUrl || !spectraToken) {
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const branch = options.branch ?? detectGitRef('rev-parse', '--abbrev-ref', 'HEAD');
|
|
42
|
+
if (!branch) {
|
|
43
|
+
console.error(chalk.red('Could not determine branch: pass --branch or run inside a git repository.'));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const commit = options.commit ?? detectGitRef('rev-parse', 'HEAD');
|
|
48
|
+
const platform = options.platform ?? 'web';
|
|
49
|
+
const wait = options.wait !== false;
|
|
50
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
51
|
+
const spinner = ora(`Triggering cloud run for ${chalk.bold(options.project)}@${branch}...`).start();
|
|
52
|
+
let trigger;
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`${spectraUrl}/api/v1/runs/trigger`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
Authorization: `Bearer ${spectraToken}`,
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
project_id: options.project,
|
|
62
|
+
project_name: options.projectName ?? options.project,
|
|
63
|
+
branch,
|
|
64
|
+
commit_hash: commit,
|
|
65
|
+
platform,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
70
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
71
|
+
}
|
|
72
|
+
trigger = (await res.json());
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
spinner.fail(chalk.red(`Failed to trigger cloud run: ${err.message}`));
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
spinner.succeed(`Cloud run queued: ${chalk.cyan(trigger.job_id)}`);
|
|
80
|
+
if (!wait) {
|
|
81
|
+
console.log(chalk.gray(`Not waiting for result. Poll ${spectraUrl}/api/v1/jobs/${trigger.job_id}/status yourself.`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await waitForJob(spectraUrl, spectraToken, trigger.job_id, timeout);
|
|
85
|
+
}
|
|
86
|
+
async function waitForJob(spectraUrl, spectraToken, jobId, timeout) {
|
|
87
|
+
const spinner = ora('Waiting for run to finish...').start();
|
|
88
|
+
const deadline = Date.now() + parseTimeoutMs(timeout);
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
91
|
+
let job;
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(`${spectraUrl}/api/v1/jobs/${jobId}/status`, {
|
|
94
|
+
headers: { Authorization: `Bearer ${spectraToken}` },
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
throw new Error(`HTTP ${res.status}`);
|
|
98
|
+
job = (await res.json());
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
spinner.text = chalk.yellow(`Poll failed, retrying: ${err.message}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (job.status === 'done') {
|
|
105
|
+
spinner.succeed(chalk.green(`Run passed (run_id: ${job.run_id})`));
|
|
106
|
+
process.exitCode = 0;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (job.status === 'failed') {
|
|
110
|
+
spinner.fail(chalk.red(`Run failed (run_id: ${job.run_id})${job.error_log ? `: ${job.error_log}` : ''}`));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
spinner.text = `Run status: ${job.status}`;
|
|
115
|
+
}
|
|
116
|
+
spinner.fail(chalk.red(`Timed out after ${timeout} waiting for the run to finish.`));
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
package/dist/commands/demo.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import * as p from
|
|
3
|
-
import { DemoServer } from
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import { DemoServer } from '../utils/demo-server.js';
|
|
4
4
|
export async function demoCommand(options) {
|
|
5
5
|
const port = options.port ? parseInt(options.port, 10) : 5173;
|
|
6
|
-
p.intro(chalk.bold.cyan(
|
|
6
|
+
p.intro(chalk.bold.cyan('🌐 TestSpectra Demo Web App'));
|
|
7
7
|
const server = new DemoServer(port);
|
|
8
8
|
try {
|
|
9
9
|
const { url } = await server.start();
|
|
10
10
|
p.log.success(chalk.green(`Demo web app is live at: ${chalk.bold.underline(url)}`));
|
|
11
11
|
p.log.info(chalk.dim("You can now run tests against this server in another terminal (e.g. 'spectra run -t web')."));
|
|
12
|
-
p.log.info(chalk.dim(
|
|
12
|
+
p.log.info(chalk.dim('Press Ctrl+C to stop the server.'));
|
|
13
13
|
// Handle termination gracefully
|
|
14
|
-
process.on(
|
|
14
|
+
process.on('SIGINT', async () => {
|
|
15
15
|
await server.stop();
|
|
16
16
|
process.exit(0);
|
|
17
17
|
});
|
|
18
|
-
process.on(
|
|
18
|
+
process.on('SIGTERM', async () => {
|
|
19
19
|
await server.stop();
|
|
20
20
|
process.exit(0);
|
|
21
21
|
});
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export interface AdbDevice {
|
|
2
|
+
udid: string;
|
|
3
|
+
id: string;
|
|
4
|
+
model: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
connectionType: 'usb' | 'wifi' | string;
|
|
8
|
+
version?: string | null;
|
|
9
|
+
androidId?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function getConnectedDevices(cwd?: string): Promise<AdbDevice[]>;
|
|
12
|
+
export declare function devicesCommand(options?: {
|
|
13
|
+
target?: 'android' | 'ios' | 'web' | 'all';
|
|
14
|
+
json?: boolean;
|
|
3
15
|
}): Promise<void>;
|