@testspectra/cli 1.1.7 → 1.1.8-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/.testspectra-runner.hash +1 -1
- package/bin/spectra.js +0 -0
- package/bin/testspectra-runner.exe +0 -0
- package/demo-app/assets/index-BhlW-eXG.css +1 -0
- package/demo-app/assets/index-DOtRypCa.js +154 -0
- package/demo-app/index.html +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/run-e2e.test.js +270 -29
- package/dist/commands/__tests__/run.test.js +92 -16
- package/dist/commands/demo.js +17 -7
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +154 -149
- package/dist/commands/lint.d.ts +5 -0
- package/dist/commands/lint.js +55 -0
- package/dist/commands/refactor.d.ts +2 -1
- package/dist/commands/refactor.js +129 -35
- package/dist/commands/run.d.ts +2 -3
- package/dist/commands/run.js +93 -28
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.js +28 -0
- package/dist/commands/sync-types.js +1 -0
- package/dist/commands/test.d.ts +17 -0
- package/dist/commands/test.js +104 -0
- package/dist/commands/watch.js +9 -3
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +5 -114
- package/dist/config/schema.js +1 -17
- package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
- package/dist/generator/__tests__/type-generator.test.js +1 -1
- package/dist/generator/tsconfig-generator.js +44 -16
- package/dist/generator/type-generator.d.ts +24 -1
- package/dist/generator/type-generator.js +104 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +76 -40
- package/dist/linter/__tests__/anonymous-export.test.js +184 -0
- package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
- package/dist/linter/__tests__/component-test-imports.test.js +115 -0
- package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
- package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
- package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
- package/dist/linter/__tests__/spec-schema.test.js +369 -0
- package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
- package/dist/linter/__tests__/step-parity.test.js +372 -0
- package/dist/linter/discovery.d.ts +30 -0
- package/dist/linter/discovery.js +249 -0
- package/dist/linter/extractor.d.ts +67 -0
- package/dist/linter/extractor.js +245 -0
- package/dist/linter/index.d.ts +7 -0
- package/dist/linter/index.js +7 -0
- package/dist/linter/parser.d.ts +24 -0
- package/dist/linter/parser.js +219 -0
- package/dist/linter/schemas/spec.schema.d.ts +83 -0
- package/dist/linter/schemas/spec.schema.js +150 -0
- package/dist/linter/schemas/suite.schema.d.ts +59 -0
- package/dist/linter/schemas/suite.schema.js +83 -0
- package/dist/linter/spec-linter.d.ts +23 -0
- package/dist/linter/spec-linter.js +630 -0
- package/dist/linter/types.d.ts +28 -0
- package/dist/linter/types.js +1 -0
- package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
- package/dist/refactor/refactor-engine.d.ts +25 -0
- package/dist/refactor/refactor-engine.js +374 -42
- package/dist/reporter/semantic-formatter.js +22 -2
- package/dist/reporter/types.d.ts +1 -1
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +34 -21
- package/dist/runner/reporter.d.ts +2 -0
- package/dist/runner/reporter.js +11 -3
- package/dist/utils/__tests__/api-server.test.d.ts +1 -0
- package/dist/utils/__tests__/api-server.test.js +62 -0
- package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
- package/dist/utils/__tests__/demo-server.test.js +35 -0
- package/dist/utils/api-server.d.ts +19 -0
- package/dist/utils/api-server.js +179 -0
- package/dist/utils/demo-server.d.ts +5 -0
- package/dist/utils/demo-server.js +8 -1
- package/package.json +29 -25
- package/templates/default/CLAUDE.md +16 -11
- package/templates/default/global-hooks/after/mobile.hook.ts +3 -3
- package/templates/default/global-hooks/after/web.hook.ts +3 -3
- package/templates/default/global-hooks/before/mobile.hook.ts +3 -3
- package/templates/default/global-hooks/before/web.hook.ts +3 -3
- package/templates/default/package.json +3 -3
- package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
- package/templates/default/page-objects/AuthPage/web.ts +10 -0
- package/templates/default/page-objects/MatchersPage/mobile.ts +70 -46
- package/templates/default/page-objects/MatchersPage/web.ts +43 -37
- package/templates/default/page-objects/NavigationPage/mobile.ts +14 -14
- package/templates/default/page-objects/NavigationPage/web.ts +8 -8
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +42 -36
- package/templates/default/page-objects/PlaygroundPage/web.ts +31 -31
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -11
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -12
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -27
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -16
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -12
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -24
- package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
- package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
- package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
- package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
- package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
- package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
- package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
- package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
- package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
- package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
- package/templates/default/specs/ApiInterception/suite.md +10 -9
- package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
- package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
- package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
- package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
- package/templates/default/specs/ApiSeeding/suite.md +9 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
- package/templates/default/specs/Authentication/suite.md +9 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +9 -9
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +17 -12
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +19 -12
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +11 -11
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +17 -12
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
- package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
- package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
- package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
- package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
- package/templates/default/specs/DeepLink/suite.md +9 -0
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +11 -11
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +17 -12
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +29 -29
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +19 -12
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
- package/templates/default/specs/ElementMatchers/suite.md +10 -9
- package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
- package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
- package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
- package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
- package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
- package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
- package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
- package/templates/default/specs/PermissionRationale/suite.md +9 -0
- package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +6 -6
- package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +18 -12
- package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +6 -6
- package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +18 -12
- package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
- package/templates/default/spectra.config.ts +52 -68
- package/templates/default/support/actions/fillCredentials/mobile.action.ts +4 -4
- package/templates/default/support/actions/fillCredentials/web.action.ts +4 -4
- package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
- package/templates/default/support/actions/seedSession/web.action.ts +13 -0
- package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +5 -5
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +5 -5
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +6 -6
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +6 -6
- package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db +0 -0
- package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +1577 -168
- package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
- package/templates/nx/.nx/workspace-data/file-map.json +464 -0
- package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
- package/templates/nx/.nx/workspace-data/project-graph.json +14389 -0
- package/templates/nx/CLAUDE.md +10 -10
- package/templates/nx/package.json +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -3
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -27
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -3
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -24
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +17 -17
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -3
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
- package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
- package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
- package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -3
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -3
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
- package/templates/nx/pnpm-lock.yaml +6663 -6663
- package/templates/nx/pnpm-workspace.yaml +9 -9
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +33 -33
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +91 -37
- package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +5 -5
- package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +8 -8
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +26 -26
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +31 -31
- package/templates/nx/shared/testing/project.json +19 -19
- package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +4 -4
- package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +4 -4
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +3 -3
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +5 -5
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +4 -4
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +6 -6
- package/templates/nx/spectra.config.ts +52 -68
- package/templates/react-component/.cursorrules +17 -0
- package/templates/react-component/.vscode/extensions.json +5 -0
- package/templates/react-component/CLAUDE.md +14 -0
- package/templates/react-component/README.md +69 -0
- package/templates/react-component/fixtures/component-mock.json +12 -0
- package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
- package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
- package/templates/react-component/package.json +27 -0
- package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
- package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
- package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
- package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
- package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
- package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
- package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
- package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
- package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
- package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
- package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
- package/templates/react-component/specs/InputComponent/suite.md +12 -0
- package/templates/react-component/spectra.config.ts +27 -0
- package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
- package/templates/react-component/src/components/Button/Button.tsx +57 -0
- package/templates/react-component/src/components/Input/Input.tsx +41 -0
- package/templates/react-component/src/test-utils.tsx +23 -0
- package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
- package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
- package/templates/react-component/tsconfig.json +8 -0
- package/bin/testspectra-runner +0 -0
- package/demo-app/assets/index-DPfv5_EF.js +0 -116
- package/demo-app/assets/index-gIwu2EMV.css +0 -1
- package/dist/commands/__tests__/cloud-run.test.js +0 -93
- package/dist/commands/cloud-run.d.ts +0 -13
- package/dist/commands/cloud-run.js +0 -120
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
- /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
package/dist/runner/bridge.js
CHANGED
|
@@ -4,23 +4,6 @@ import path from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
|
-
// npm/pnpm pack+extract doesn't reliably preserve the executable bit on
|
|
8
|
-
// non-`bin`-field binaries — testspectra-runner has been observed installed
|
|
9
|
-
// as 0644 from a tarball that was 0755 at publish time. Defensive chmod so
|
|
10
|
-
// every consumer works regardless of how it arrived on disk.
|
|
11
|
-
function ensureExecutable(binPath) {
|
|
12
|
-
if (process.platform === 'win32')
|
|
13
|
-
return;
|
|
14
|
-
try {
|
|
15
|
-
const mode = fs.statSync(binPath).mode;
|
|
16
|
-
if ((mode & 0o111) === 0) {
|
|
17
|
-
fs.chmodSync(binPath, 0o755);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
// best-effort — if this fails, the spawn below will surface the real error
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
7
|
export class RustCoreBridge {
|
|
25
8
|
static resolveBinaryPath() {
|
|
26
9
|
// 1. Look for precompiled release/debug binary in workspace target
|
|
@@ -40,7 +23,6 @@ export class RustCoreBridge {
|
|
|
40
23
|
];
|
|
41
24
|
for (const p of candidatePaths) {
|
|
42
25
|
if (fs.existsSync(p)) {
|
|
43
|
-
ensureExecutable(p);
|
|
44
26
|
return p;
|
|
45
27
|
}
|
|
46
28
|
if (fs.existsSync(`${p}.exe`)) {
|
|
@@ -62,7 +44,7 @@ export class RustCoreBridge {
|
|
|
62
44
|
targetDevice: options.targetDevice,
|
|
63
45
|
licenseKey: options.licenseKey,
|
|
64
46
|
outputJsonPath: options.outputJsonPath,
|
|
65
|
-
|
|
47
|
+
specs: options.specs,
|
|
66
48
|
concurrency: options.concurrency,
|
|
67
49
|
failedOnly: options.failedOnly,
|
|
68
50
|
failedTarget: options.failedTarget,
|
|
@@ -80,6 +62,7 @@ export class RustCoreBridge {
|
|
|
80
62
|
TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
|
|
81
63
|
RUST_LOG: 'info',
|
|
82
64
|
};
|
|
65
|
+
const runStartedAt = Date.now();
|
|
83
66
|
return new Promise((resolve, reject) => {
|
|
84
67
|
const child = spawn(binPath, [payload], {
|
|
85
68
|
env: runnerEnv,
|
|
@@ -97,6 +80,15 @@ export class RustCoreBridge {
|
|
|
97
80
|
level: 'INFO',
|
|
98
81
|
message: line,
|
|
99
82
|
});
|
|
83
|
+
// In interactive terminals, the reporter above already renders this tag as part of
|
|
84
|
+
// its live-updating spinner UI (`renderActiveSlots`), so echoing the raw tag too would
|
|
85
|
+
// just duplicate/clutter that UI. But that live rendering is a no-op when stdout isn't
|
|
86
|
+
// a real TTY (CI, or an external tool like the VS Code extension spawning this process
|
|
87
|
+
// via a pipe) — in that case this raw line is the *only* live signal a wrapping process
|
|
88
|
+
// can see, so it must still reach stdout verbatim.
|
|
89
|
+
if (!reporter.isInteractiveMode()) {
|
|
90
|
+
console.log(line);
|
|
91
|
+
}
|
|
100
92
|
continue;
|
|
101
93
|
}
|
|
102
94
|
try {
|
|
@@ -138,16 +130,37 @@ export class RustCoreBridge {
|
|
|
138
130
|
const str = data.toString();
|
|
139
131
|
// Ignore normal compilation progress
|
|
140
132
|
if (!str.includes('Compiling') && !str.includes('Checking') && !str.includes('Finished')) {
|
|
133
|
+
const trimmed = str.trim();
|
|
141
134
|
reporter.addLog({
|
|
142
135
|
timestamp: new Date().toLocaleTimeString(),
|
|
143
136
|
level: 'DEBUG',
|
|
144
|
-
message:
|
|
137
|
+
message: trimmed,
|
|
145
138
|
});
|
|
139
|
+
// Surface fatal runner errors (e.g. bootstrap failures like a missing APK) immediately —
|
|
140
|
+
// previously these only landed in the JSON report's `logs[]`, so a failed run gave no
|
|
141
|
+
// visible reason on the terminal.
|
|
142
|
+
if (trimmed.startsWith('Error:')) {
|
|
143
|
+
console.error(`\x1b[38;2;248;113;113m✖ ${trimmed}\x1b[0m`);
|
|
144
|
+
}
|
|
146
145
|
}
|
|
147
146
|
});
|
|
148
147
|
child.on('close', (code) => {
|
|
149
148
|
let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
|
|
149
|
+
// Only trust a pre-existing outputJsonPath when it was actually written during THIS run
|
|
150
|
+
// (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
|
|
151
|
+
// native runner writes its own complete report once the pipeline reaches the report
|
|
152
|
+
// stage, even for a run with failing tests (which still exits non-zero); but if it bails
|
|
153
|
+
// before ever getting there (missing APK, driver bootstrap error, etc.) a stale file from
|
|
154
|
+
// an unrelated previous run may still be sitting at this path — trusting it purely based
|
|
155
|
+
// on exit code would silently report that old run's result instead of the real failure.
|
|
156
|
+
let isFresh = false;
|
|
150
157
|
if (options.outputJsonPath && fs.existsSync(options.outputJsonPath)) {
|
|
158
|
+
try {
|
|
159
|
+
isFresh = fs.statSync(options.outputJsonPath).mtimeMs >= runStartedAt;
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
}
|
|
163
|
+
if (isFresh && options.outputJsonPath) {
|
|
151
164
|
try {
|
|
152
165
|
const raw = fs.readFileSync(options.outputJsonPath, 'utf-8');
|
|
153
166
|
const parsed = JSON.parse(raw);
|
|
@@ -161,7 +174,7 @@ export class RustCoreBridge {
|
|
|
161
174
|
}
|
|
162
175
|
catch { }
|
|
163
176
|
}
|
|
164
|
-
|
|
177
|
+
if (options.outputJsonPath && !isFresh) {
|
|
165
178
|
const outDir = path.dirname(options.outputJsonPath);
|
|
166
179
|
if (!fs.existsSync(outDir)) {
|
|
167
180
|
fs.mkdirSync(outDir, { recursive: true });
|
|
@@ -43,6 +43,8 @@ export declare class Reporter {
|
|
|
43
43
|
private spinnerIndex;
|
|
44
44
|
private spinnerTimer;
|
|
45
45
|
constructor();
|
|
46
|
+
/** Whether this reporter is drawing the live-updating interactive terminal UI (real TTY, not CI/VS Code/test). */
|
|
47
|
+
isInteractiveMode(): boolean;
|
|
46
48
|
private clearActiveSlots;
|
|
47
49
|
private renderActiveSlots;
|
|
48
50
|
private resolveTestCase;
|
package/dist/runner/reporter.js
CHANGED
|
@@ -19,6 +19,8 @@ function mapActionArgs(key, arr) {
|
|
|
19
19
|
return { durationMs: arr[0] };
|
|
20
20
|
case 'navigate':
|
|
21
21
|
return { url: arr[0] };
|
|
22
|
+
case 'setCookies':
|
|
23
|
+
return { url: arr[0] };
|
|
22
24
|
case 'setViewport':
|
|
23
25
|
return { width: arr[0], height: arr[1] };
|
|
24
26
|
case 'wait':
|
|
@@ -63,9 +65,11 @@ function mapAssertionArgs(key, arr) {
|
|
|
63
65
|
function parseAndFormat(cmdText) {
|
|
64
66
|
try {
|
|
65
67
|
const payload = JSON.parse(cmdText);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// Passed through as-is (not pre-flattened to a bare string here) — `formatAction`/
|
|
69
|
+
// `formatAssertion` already do the same `.selector`/`.name`/`.target` fallback lookup
|
|
70
|
+
// themselves, and only the raw object retains `.parent` for a scoped (`element.get()`/
|
|
71
|
+
// `.getAll()`-chained) target, which they need to render the "within" phrasing.
|
|
72
|
+
const target = payload.target;
|
|
69
73
|
if (payload.type === 'action') {
|
|
70
74
|
return formatAction(payload.key, target, mapActionArgs(payload.key, payload.args || []));
|
|
71
75
|
}
|
|
@@ -167,6 +171,10 @@ export class Reporter {
|
|
|
167
171
|
}, 80);
|
|
168
172
|
}
|
|
169
173
|
}
|
|
174
|
+
/** Whether this reporter is drawing the live-updating interactive terminal UI (real TTY, not CI/VS Code/test). */
|
|
175
|
+
isInteractiveMode() {
|
|
176
|
+
return this.isInteractive;
|
|
177
|
+
}
|
|
170
178
|
clearActiveSlots() {
|
|
171
179
|
if (!this.isInteractive || this.lastRenderedLines === 0)
|
|
172
180
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { DemoApiServer } from '../api-server.js';
|
|
3
|
+
describe('DemoApiServer self-hosted REST API (cli/src/utils/api-server.ts)', () => {
|
|
4
|
+
let server;
|
|
5
|
+
let port;
|
|
6
|
+
const base = () => `http://127.0.0.1:${port}`;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
server = new DemoApiServer(0);
|
|
9
|
+
const info = await server.start();
|
|
10
|
+
port = info.port;
|
|
11
|
+
});
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await server?.stop();
|
|
14
|
+
});
|
|
15
|
+
it('GET /api/posts returns the self-hosted post list', async () => {
|
|
16
|
+
const res = await fetch(`${base()}/api/posts`);
|
|
17
|
+
expect(res.status).toBe(200);
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
expect(Array.isArray(data)).toBe(true);
|
|
20
|
+
expect(data.length).toBeGreaterThan(0);
|
|
21
|
+
expect(data[0]).toHaveProperty('id');
|
|
22
|
+
expect(data[0]).toHaveProperty('title');
|
|
23
|
+
});
|
|
24
|
+
it('GET /api/users returns a user list with name/email', async () => {
|
|
25
|
+
const res = await fetch(`${base()}/api/users`);
|
|
26
|
+
expect(res.status).toBe(200);
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
expect(Array.isArray(data)).toBe(true);
|
|
29
|
+
expect(data[0]).toHaveProperty('name');
|
|
30
|
+
expect(data[0]).toHaveProperty('email');
|
|
31
|
+
});
|
|
32
|
+
it('GET /api/todos/1 returns a single todo', async () => {
|
|
33
|
+
const res = await fetch(`${base()}/api/todos/1`);
|
|
34
|
+
expect(res.status).toBe(200);
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
expect(data).toHaveProperty('id', 1);
|
|
37
|
+
expect(data).toHaveProperty('completed');
|
|
38
|
+
});
|
|
39
|
+
it('POST /api/posts creates a post with a numeric id (201)', async () => {
|
|
40
|
+
const res = await fetch(`${base()}/api/posts`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
|
43
|
+
body: JSON.stringify({ title: 'TestSpectra seeded post', body: 'Hook seeding', userId: 1 }),
|
|
44
|
+
});
|
|
45
|
+
expect(res.status).toBe(201);
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
expect(typeof data.id).toBe('number');
|
|
48
|
+
expect(data.title).toBe('TestSpectra seeded post');
|
|
49
|
+
});
|
|
50
|
+
it('DELETE /api/posts/:id cleans up a seeded post (200)', async () => {
|
|
51
|
+
const res = await fetch(`${base()}/api/posts/123`, { method: 'DELETE' });
|
|
52
|
+
expect(res.status).toBe(200);
|
|
53
|
+
});
|
|
54
|
+
it('returns CORS headers on API responses', async () => {
|
|
55
|
+
const res = await fetch(`${base()}/api/posts`);
|
|
56
|
+
expect(res.headers.get('access-control-allow-origin')).toBe('*');
|
|
57
|
+
});
|
|
58
|
+
it('returns 404 for unknown API routes', async () => {
|
|
59
|
+
const res = await fetch(`${base()}/api/does-not-exist`);
|
|
60
|
+
expect(res.status).toBe(404);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { DemoServer } from '../demo-server.js';
|
|
3
|
+
describe('DemoServer static web app (cli/src/utils/demo-server.ts)', () => {
|
|
4
|
+
let server;
|
|
5
|
+
let port;
|
|
6
|
+
const demoDistAvailable = DemoServer.resolveDemoDist() !== null;
|
|
7
|
+
const base = () => `http://127.0.0.1:${port}`;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
if (!demoDistAvailable)
|
|
10
|
+
return;
|
|
11
|
+
server = new DemoServer(0);
|
|
12
|
+
const info = await server.start();
|
|
13
|
+
port = info.port;
|
|
14
|
+
});
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await server?.stop();
|
|
17
|
+
});
|
|
18
|
+
it.skipIf(!demoDistAvailable)('serves the demo web app index', async () => {
|
|
19
|
+
const res = await fetch(`${base()}/`);
|
|
20
|
+
expect(res.status).toBe(200);
|
|
21
|
+
expect(await res.text()).toContain('<div id="app"></div>');
|
|
22
|
+
});
|
|
23
|
+
it.skipIf(!demoDistAvailable)('falls back to index.html for SPA routes', async () => {
|
|
24
|
+
const res = await fetch(`${base()}/some/spa/route`);
|
|
25
|
+
expect(res.status).toBe(200);
|
|
26
|
+
expect(await res.text()).toContain('<div id="app"></div>');
|
|
27
|
+
});
|
|
28
|
+
it.skipIf(!demoDistAvailable)('does not serve /api/* (that is the separate DemoApiServer)', async () => {
|
|
29
|
+
const res = await fetch(`${base()}/api/posts`);
|
|
30
|
+
// /api/posts is not a static file, so the SPA fallback serves index.html (200) — never JSON.
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
expect(text).toContain('<div id="app"></div>');
|
|
33
|
+
expect(() => JSON.parse(text)).toThrow();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Port the self-hosted demo API server listens on (separate from the demo web app's 5173). */
|
|
2
|
+
export declare const DEMO_API_PORT = 5200;
|
|
3
|
+
/**
|
|
4
|
+
* Standalone self-hosted REST API server for the demo apps, kept separate from `DemoServer` (the
|
|
5
|
+
* static web app) so the API can run independently and either server can be swapped for a real
|
|
6
|
+
* backend in a project. Mirrors the small subset of jsonplaceholder.typicode.com shapes the demo's
|
|
7
|
+
* API sections exercise.
|
|
8
|
+
*/
|
|
9
|
+
export declare class DemoApiServer {
|
|
10
|
+
private server;
|
|
11
|
+
private port;
|
|
12
|
+
constructor(port?: number);
|
|
13
|
+
start(): Promise<{
|
|
14
|
+
url: string;
|
|
15
|
+
port: number;
|
|
16
|
+
}>;
|
|
17
|
+
private handleApi;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
/** Port the self-hosted demo API server listens on (separate from the demo web app's 5173). */
|
|
3
|
+
export const DEMO_API_PORT = 5200;
|
|
4
|
+
// Self-hosted dummy REST data for the demo app's API sections — keeps the ApiInterception and
|
|
5
|
+
// ApiSeeding suites fully offline/deterministic instead of depending on an external service
|
|
6
|
+
// (jsonplaceholder.typicode.com) whose reachability and latency were themselves a source of
|
|
7
|
+
// flakiness. Mocked requests never reach these handlers (CDP/proxy intercepts them first); they
|
|
8
|
+
// back the genuine passthrough cases (real network call recording, seed/cleanup hooks).
|
|
9
|
+
const API_POSTS = [
|
|
10
|
+
{
|
|
11
|
+
userId: 1,
|
|
12
|
+
id: 1,
|
|
13
|
+
title: 'TestSpectra demo post one',
|
|
14
|
+
body: 'Self-hosted demo data served by the built-in TestSpectra API server.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
userId: 1,
|
|
18
|
+
id: 2,
|
|
19
|
+
title: 'TestSpectra demo post two',
|
|
20
|
+
body: 'No external network required — deterministic for CI and offline runs.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
userId: 2,
|
|
24
|
+
id: 3,
|
|
25
|
+
title: 'TestSpectra demo post three',
|
|
26
|
+
body: 'Intercept it with Spectra.intercept() or let it pass through untouched.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
userId: 2,
|
|
30
|
+
id: 4,
|
|
31
|
+
title: 'TestSpectra demo post four',
|
|
32
|
+
body: 'Served independently of the demo web app so either can run without the other.',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const API_USERS = [
|
|
36
|
+
{ id: 1, name: 'Ada Lovelace', username: 'ada', email: 'ada@testspectra.dev' },
|
|
37
|
+
{ id: 2, name: 'Alan Turing', username: 'alan', email: 'alan@testspectra.dev' },
|
|
38
|
+
{ id: 3, name: 'Grace Hopper', username: 'grace', email: 'grace@testspectra.dev' },
|
|
39
|
+
];
|
|
40
|
+
const API_TODOS = [{ userId: 1, id: 1, title: 'Verify the self-hosted demo API responds', completed: false }];
|
|
41
|
+
/** Fixed demo credentials — this is a deterministic offline sandbox, not a real auth system. */
|
|
42
|
+
const DEMO_ACCOUNT = { email: 'demo@testspectra.dev', password: 'demo-password' };
|
|
43
|
+
/**
|
|
44
|
+
* Issued session tokens, in-memory only (reset on server restart) — backs both `/api/login`'s
|
|
45
|
+
* `Set-Cookie` (web, read via `Spectra.browser.setCookies()`) and its JSON `token` field
|
|
46
|
+
* (Android, read by the app's own deep-link-triggered login and stored via its own secure
|
|
47
|
+
* storage — see docs/v2/features/authentication-and-session-seeding.md). `/api/me` looks a
|
|
48
|
+
* caller's cookie *or* `Authorization: Bearer <token>` up in this same map.
|
|
49
|
+
*/
|
|
50
|
+
const issuedSessions = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Standalone self-hosted REST API server for the demo apps, kept separate from `DemoServer` (the
|
|
53
|
+
* static web app) so the API can run independently and either server can be swapped for a real
|
|
54
|
+
* backend in a project. Mirrors the small subset of jsonplaceholder.typicode.com shapes the demo's
|
|
55
|
+
* API sections exercise.
|
|
56
|
+
*/
|
|
57
|
+
export class DemoApiServer {
|
|
58
|
+
server = null;
|
|
59
|
+
port;
|
|
60
|
+
constructor(port = DEMO_API_PORT) {
|
|
61
|
+
this.port = port;
|
|
62
|
+
}
|
|
63
|
+
async start() {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
this.server = http.createServer((req, res) => {
|
|
66
|
+
this.handleApi(req, res, req.url ? req.url.split('?')[0] : '/');
|
|
67
|
+
});
|
|
68
|
+
this.server.on('error', (err) => {
|
|
69
|
+
if (err.code === 'EADDRINUSE') {
|
|
70
|
+
// Port already in use (e.g. user already running the API server)
|
|
71
|
+
resolve({ url: `http://localhost:${this.port}`, port: this.port });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
reject(err);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.server.listen(this.port, '0.0.0.0', () => {
|
|
78
|
+
const addr = this.server?.address();
|
|
79
|
+
const actualPort = typeof addr === 'object' && addr !== null ? addr.port : this.port;
|
|
80
|
+
resolve({ url: `http://localhost:${actualPort}`, port: actualPort });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
handleApi(req, res, pathname) {
|
|
85
|
+
// The demo web app (port 5173) calls this server (port 5200) cross-origin. A wildcard origin
|
|
86
|
+
// can't be combined with credentialed requests (Access-Control-Allow-Credentials), which
|
|
87
|
+
// /api/me below needs to receive the session cookie seeded via Spectra.browser.setCookies() —
|
|
88
|
+
// reflect the actual request Origin instead so both credentialed and simple requests work.
|
|
89
|
+
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
|
|
90
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
91
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
92
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
93
|
+
if (req.method === 'OPTIONS') {
|
|
94
|
+
res.writeHead(204);
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const sendJson = (status, payload) => {
|
|
99
|
+
// no-store: the demo app fetches these without a cache-buster, so a cached body would be
|
|
100
|
+
// served for later, differently-mocked requests to the same URL instead of reaching the
|
|
101
|
+
// mock proxy.
|
|
102
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
103
|
+
res.end(JSON.stringify(payload));
|
|
104
|
+
};
|
|
105
|
+
if (pathname === '/api/login' && req.method === 'POST') {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
108
|
+
req.on('end', () => {
|
|
109
|
+
let payload = {};
|
|
110
|
+
try {
|
|
111
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf-8') || '{}');
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
if (payload.email !== DEMO_ACCOUNT.email || payload.password !== DEMO_ACCOUNT.password) {
|
|
115
|
+
sendJson(401, { error: 'Invalid credentials' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const token = `demo-session-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
119
|
+
issuedSessions.set(token, { email: DEMO_ACCOUNT.email });
|
|
120
|
+
// HttpOnly: JS on the page can't read this — only the browser's own network layer (and
|
|
121
|
+
// Spectra.browser.setCookies(), which operates at that same layer) can set/send it.
|
|
122
|
+
res.setHeader('Set-Cookie', `demo_session=${token}; Path=/; HttpOnly; SameSite=Lax`);
|
|
123
|
+
sendJson(200, { token, user: { email: DEMO_ACCOUNT.email } });
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (pathname === '/api/me' && req.method === 'GET') {
|
|
128
|
+
const cookieHeader = req.headers.cookie || '';
|
|
129
|
+
const cookieToken = /(?:^|;\s*)demo_session=([^;]+)/.exec(cookieHeader)?.[1];
|
|
130
|
+
const bearerToken = /^Bearer\s+(.+)$/.exec(req.headers.authorization || '')?.[1];
|
|
131
|
+
const session = issuedSessions.get(cookieToken || '') || issuedSessions.get(bearerToken || '');
|
|
132
|
+
sendJson(200, session ? { authenticated: true, user: session } : { authenticated: false });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (pathname === '/api/posts' && req.method === 'GET') {
|
|
136
|
+
sendJson(200, API_POSTS);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (pathname === '/api/users' && req.method === 'GET') {
|
|
140
|
+
sendJson(200, API_USERS);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (pathname === '/api/todos/1' && req.method === 'GET') {
|
|
144
|
+
sendJson(200, API_TODOS[0]);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (pathname === '/api/posts' && req.method === 'POST') {
|
|
148
|
+
const chunks = [];
|
|
149
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
150
|
+
req.on('end', () => {
|
|
151
|
+
let payload = {};
|
|
152
|
+
try {
|
|
153
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf-8') || '{}');
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
sendJson(201, { ...payload, id: Date.now() });
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (pathname.startsWith('/api/posts/') && req.method === 'DELETE') {
|
|
161
|
+
sendJson(200, {});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
sendJson(404, { error: 'Not Found' });
|
|
165
|
+
}
|
|
166
|
+
async stop() {
|
|
167
|
+
if (this.server) {
|
|
168
|
+
if (typeof this.server.closeAllConnections === 'function') {
|
|
169
|
+
this.server.closeAllConnections();
|
|
170
|
+
}
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
this.server?.close(() => {
|
|
173
|
+
this.server = null;
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static file server for the demo web app bundle. Serves ONLY the web app — the self-hosted demo
|
|
3
|
+
* REST API lives in a separate `DemoApiServer` (`cli/src/utils/api-server.ts`, port 5200) so the
|
|
4
|
+
* two can run and be swapped independently.
|
|
5
|
+
*/
|
|
1
6
|
export declare class DemoServer {
|
|
2
7
|
private server;
|
|
3
8
|
private port;
|
|
@@ -16,6 +16,11 @@ const MIME_TYPES = {
|
|
|
16
16
|
'.woff2': 'font/woff2',
|
|
17
17
|
'.ttf': 'font/ttf',
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Static file server for the demo web app bundle. Serves ONLY the web app — the self-hosted demo
|
|
21
|
+
* REST API lives in a separate `DemoApiServer` (`cli/src/utils/api-server.ts`, port 5200) so the
|
|
22
|
+
* two can run and be swapped independently.
|
|
23
|
+
*/
|
|
19
24
|
export class DemoServer {
|
|
20
25
|
server = null;
|
|
21
26
|
port;
|
|
@@ -80,7 +85,9 @@ export class DemoServer {
|
|
|
80
85
|
}
|
|
81
86
|
});
|
|
82
87
|
this.server.listen(this.port, '0.0.0.0', () => {
|
|
83
|
-
|
|
88
|
+
const addr = this.server?.address();
|
|
89
|
+
const actualPort = typeof addr === 'object' && addr !== null ? addr.port : this.port;
|
|
90
|
+
resolve({ url: `http://localhost:${actualPort}`, port: actualPort });
|
|
84
91
|
});
|
|
85
92
|
});
|
|
86
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testspectra/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8-rc.2",
|
|
4
4
|
"description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
"spectra": "./bin/spectra.js",
|
|
17
17
|
"testspectra": "./bin/spectra.js"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build:rust": "node scripts/ensure-runner-binary.js",
|
|
21
|
+
"prebuild": "node scripts/ensure-runner-binary.js",
|
|
22
|
+
"build": "tsc && node scripts/copy-demo.js",
|
|
23
|
+
"watch": "tsc -w",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
19
28
|
"files": [
|
|
20
29
|
"dist",
|
|
21
30
|
"bin",
|
|
@@ -25,31 +34,26 @@
|
|
|
25
34
|
"LICENSE.md"
|
|
26
35
|
],
|
|
27
36
|
"dependencies": {
|
|
28
|
-
"@clack/prompts": "
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
37
|
+
"@clack/prompts": "catalog:",
|
|
38
|
+
"@testspectra/matchers": "^1.1.8-rc.2",
|
|
39
|
+
"@testspectra/react": "^1.1.8-rc.2",
|
|
40
|
+
"@testspectra/skills": "^1.1.8-rc.2",
|
|
41
|
+
"chalk": "catalog:",
|
|
42
|
+
"commander": "catalog:",
|
|
43
|
+
"dotenv": "catalog:",
|
|
44
|
+
"ora": "catalog:",
|
|
45
|
+
"typescript": "catalog:",
|
|
46
|
+
"yaml": "catalog:",
|
|
47
|
+
"zod": "catalog:"
|
|
35
48
|
},
|
|
36
49
|
"devDependencies": {
|
|
37
|
-
"@types/node": "
|
|
38
|
-
"@wdio/globals": "
|
|
39
|
-
"@wdio/mocha-framework": "
|
|
40
|
-
"@wdio/types": "
|
|
41
|
-
"esbuild": "
|
|
42
|
-
"
|
|
43
|
-
"vitest": "^2.1.8"
|
|
50
|
+
"@types/node": "catalog:",
|
|
51
|
+
"@wdio/globals": "catalog:",
|
|
52
|
+
"@wdio/mocha-framework": "catalog:",
|
|
53
|
+
"@wdio/types": "catalog:",
|
|
54
|
+
"esbuild": "catalog:",
|
|
55
|
+
"vitest": "catalog:"
|
|
44
56
|
},
|
|
45
57
|
"type": "module",
|
|
46
|
-
"license": "SEE LICENSE IN LICENSE.md"
|
|
47
|
-
|
|
48
|
-
"build:rust": "node scripts/ensure-runner-binary.js",
|
|
49
|
-
"prebuild": "node scripts/ensure-runner-binary.js",
|
|
50
|
-
"build": "tsc && node scripts/copy-demo.js",
|
|
51
|
-
"watch": "tsc -w",
|
|
52
|
-
"test": "vitest run",
|
|
53
|
-
"test:watch": "vitest"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
58
|
+
"license": "SEE LICENSE IN LICENSE.md"
|
|
59
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# CLAUDE.md - TestSpectra Framework Conventions
|
|
2
|
-
|
|
3
|
-
## Commands
|
|
4
|
-
- Run all Web tests: `pnpm test:web` (or `spectra run -t web`)
|
|
5
|
-
- Run all Mobile tests: `pnpm test:mobile` (or `spectra run -t mobile`)
|
|
6
|
-
- Run Android tests: `pnpm test:android` (or `spectra run -t android`)
|
|
7
|
-
- Run iOS tests: `pnpm test:ios` (or `spectra run -t ios`)
|
|
8
|
-
- Run specific spec: `spectra run "specs/Playground/TC-0001-form-controls/mobile.test.ts" -t mobile`
|
|
9
|
-
- Type checking: `pnpm type-check` (or `tsc -b`)
|
|
10
|
-
- Preflight doctor: `pnpm doctor` (or `spectra doctor`)
|
|
11
|
-
- Sync ambient types: `pnpm sync-types`
|
|
1
|
+
# CLAUDE.md - TestSpectra Framework Conventions
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
- Run all Web tests: `pnpm test:web` (or `spectra run -t web`)
|
|
5
|
+
- Run all Mobile tests: `pnpm test:mobile` (or `spectra run -t mobile`)
|
|
6
|
+
- Run Android tests: `pnpm test:android` (or `spectra run -t android`)
|
|
7
|
+
- Run iOS tests: `pnpm test:ios` (or `spectra run -t ios`)
|
|
8
|
+
- Run specific spec: `spectra run "specs/Playground/TC-0001-form-controls/mobile.test.ts" -t mobile`
|
|
9
|
+
- Type checking: `pnpm type-check` (or `tsc -b`)
|
|
10
|
+
- Preflight doctor: `pnpm doctor` (or `spectra doctor`)
|
|
11
|
+
- Sync ambient types: `pnpm sync-types`
|
|
12
|
+
|
|
13
|
+
## Element Selectors
|
|
14
|
+
- `~name` → **accessibility id** (Android `content-desc`, RN `accessibilityLabel`, web `aria-label`) — the portable default.
|
|
15
|
+
- `#name` → **native id** (Android `resource-id`, RN `testID`, web CSS `id`).
|
|
16
|
+
- Each prefix maps to exactly one attribute (no silent cross-matching). Prefer `~` for user-perceivable elements, `#` for stable test ids. See `docs/v2/cli/selectors.md`.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export default async function () {
|
|
2
|
-
// Global mobile teardown
|
|
3
|
-
}
|
|
1
|
+
export default async function () {
|
|
2
|
+
// Global mobile teardown
|
|
3
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export default async function () {
|
|
2
|
-
// Global web teardown
|
|
3
|
-
}
|
|
1
|
+
export default async function () {
|
|
2
|
+
// Global web teardown
|
|
3
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export default async function () {
|
|
2
|
-
// Global mobile initialization
|
|
3
|
-
}
|
|
1
|
+
export default async function () {
|
|
2
|
+
// Global mobile initialization
|
|
3
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export default async function () {
|
|
2
|
-
// Global web initialization
|
|
3
|
-
}
|
|
1
|
+
export default async function () {
|
|
2
|
+
// Global web initialization
|
|
3
|
+
}
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"postinstall": "spectra sync-types"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@testspectra/cli": "
|
|
21
|
-
"@testspectra/matchers": "
|
|
20
|
+
"@testspectra/cli": "^1.1.8-rc.2",
|
|
21
|
+
"@testspectra/matchers": "^1.1.8-rc.2",
|
|
22
22
|
"@types/node": "^20.14.0",
|
|
23
23
|
"@wdio/cli": "^9.2.8",
|
|
24
24
|
"@wdio/local-runner": "^9.2.8",
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"webdriverio": "^9.2.8",
|
|
30
30
|
"typescript": "^5.4.5"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|