@testspectra/cli 1.1.7 → 1.1.8-rc.10
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/README.md +47 -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 +254 -13
- package/dist/commands/__tests__/run.test.js +92 -16
- package/dist/commands/add.js +2 -1
- package/dist/commands/demo.js +17 -7
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +8 -2
- 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 +39 -1
- package/dist/generator/tsconfig-generator.js +44 -16
- package/dist/generator/type-generator.d.ts +29 -3
- package/dist/generator/type-generator.js +115 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +65 -29
- 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 +85 -24
- 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 +13 -4
- package/templates/default/CLAUDE.md +5 -0
- 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 +26 -2
- package/templates/default/page-objects/MatchersPage/web.ts +8 -2
- package/templates/default/page-objects/NavigationPage/mobile.ts +1 -1
- package/templates/default/page-objects/NavigationPage/web.ts +1 -1
- package/templates/default/page-objects/PlaygroundPage/mobile.ts +12 -6
- package/templates/default/page-objects/PlaygroundPage/web.ts +2 -2
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +45 -3
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +7 -0
- package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +8 -4
- 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 +13 -2
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +7 -0
- package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +6 -4
- 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 +1 -0
- package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
- package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
- package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
- package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
- package/templates/default/specs/ApiSeeding/suite.md +9 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
- package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
- package/templates/default/specs/Authentication/suite.md +9 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
- package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
- package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
- package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
- package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
- 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 +1 -1
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
- package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
- package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
- package/templates/default/specs/ElementMatchers/suite.md +2 -1
- package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
- package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
- package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
- package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
- package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
- package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
- package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
- package/templates/default/specs/PermissionRationale/suite.md +9 -0
- package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
- package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
- package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
- package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
- package/templates/default/spectra.config.ts +1 -17
- package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
- package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
- package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
- package/templates/default/support/actions/seedSession/web.action.ts +13 -0
- package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
- package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +1 -1
- package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
- package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +1 -1
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +5803 -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 +14388 -0
- package/templates/nx/package.json +3 -3
- package/templates/nx/packages/matchers/e2e/project.json +2 -2
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
- package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
- package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
- 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/project.json +2 -2
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
- package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
- package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
- package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
- package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
- package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
- package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
- package/templates/nx/shared/testing/project.json +1 -2
- package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
- package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
- package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
- package/templates/nx/spectra.config.ts +1 -17
- 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.hash +0 -1
- 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
|
@@ -1,32 +1,60 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = path.dirname(__filename);
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
// Maps process.platform/process.arch to the optionalDependency package that carries the
|
|
10
|
+
// prebuilt native binary for that platform (see cli/npm/*). Keep in sync with the
|
|
11
|
+
// optionalDependencies list in cli/package.json and the packages built by
|
|
12
|
+
// scripts/release-cli-local.sh.
|
|
13
|
+
function platformPackageName() {
|
|
14
|
+
const { platform, arch } = process;
|
|
15
|
+
if (platform === 'darwin' && arch === 'arm64')
|
|
16
|
+
return '@testspectra/cli-darwin-arm64';
|
|
17
|
+
if (platform === 'linux' && arch === 'x64')
|
|
18
|
+
return '@testspectra/cli-linux-x64';
|
|
19
|
+
if (platform === 'win32' && arch === 'arm64')
|
|
20
|
+
return '@testspectra/cli-win32-arm64';
|
|
21
|
+
if (platform === 'win32' && arch === 'x64')
|
|
22
|
+
return '@testspectra/cli-win32-x64';
|
|
23
|
+
return null;
|
|
23
24
|
}
|
|
24
25
|
export class RustCoreBridge {
|
|
25
26
|
static resolveBinaryPath() {
|
|
26
|
-
// 1. Look for precompiled release/debug binary in workspace target
|
|
27
|
-
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
28
27
|
const isWindows = process.platform === 'win32';
|
|
29
28
|
const binaryName = isWindows ? 'testspectra-runner.exe' : 'testspectra-runner';
|
|
29
|
+
// 1. Production install: resolve the prebuilt binary from the platform-specific
|
|
30
|
+
// optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
|
|
31
|
+
// published npm package ships native binaries for every supported platform without
|
|
32
|
+
// bundling all of them into @testspectra/cli itself.
|
|
33
|
+
const pkgName = platformPackageName();
|
|
34
|
+
if (pkgName) {
|
|
35
|
+
try {
|
|
36
|
+
const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
|
|
37
|
+
const pkgBinPath = path.join(path.dirname(pkgJsonPath), 'bin', binaryName);
|
|
38
|
+
if (fs.existsSync(pkgBinPath)) {
|
|
39
|
+
if (process.platform !== 'win32') {
|
|
40
|
+
try {
|
|
41
|
+
const stat = fs.statSync(pkgBinPath);
|
|
42
|
+
if ((stat.mode & 0o111) === 0) {
|
|
43
|
+
fs.chmodSync(pkgBinPath, 0o755);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
return pkgBinPath;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Optional dependency not installed — fall through to the workspace dev paths below.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2. Monorepo dev fallback: look for a precompiled release/debug binary in the workspace
|
|
56
|
+
// target, as produced by scripts/ensure-runner-binary.js during local development.
|
|
57
|
+
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
30
58
|
const candidatePaths = [
|
|
31
59
|
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
32
60
|
path.join(workspaceRoot, 'target/release', binaryName),
|
|
@@ -40,15 +68,17 @@ export class RustCoreBridge {
|
|
|
40
68
|
];
|
|
41
69
|
for (const p of candidatePaths) {
|
|
42
70
|
if (fs.existsSync(p)) {
|
|
43
|
-
ensureExecutable(p);
|
|
44
71
|
return p;
|
|
45
72
|
}
|
|
46
73
|
if (fs.existsSync(`${p}.exe`)) {
|
|
47
74
|
return `${p}.exe`;
|
|
48
75
|
}
|
|
49
76
|
}
|
|
50
|
-
throw new Error(
|
|
51
|
-
|
|
77
|
+
throw new Error(pkgName
|
|
78
|
+
? `[TestSpectra] Native core runner binary not found for ${process.platform}/${process.arch}.\n` +
|
|
79
|
+
`Expected the optional dependency '${pkgName}' to provide it. Try reinstalling ` +
|
|
80
|
+
"('npm install' / 'pnpm install') without --no-optional, or build locally with 'pnpm run build:core'."
|
|
81
|
+
: `[TestSpectra] ${process.platform}/${process.arch} is not currently supported by @testspectra/cli.`);
|
|
52
82
|
}
|
|
53
83
|
static async run(options, reporter) {
|
|
54
84
|
const binPath = this.resolveBinaryPath();
|
|
@@ -62,7 +92,7 @@ export class RustCoreBridge {
|
|
|
62
92
|
targetDevice: options.targetDevice,
|
|
63
93
|
licenseKey: options.licenseKey,
|
|
64
94
|
outputJsonPath: options.outputJsonPath,
|
|
65
|
-
|
|
95
|
+
specs: options.specs,
|
|
66
96
|
concurrency: options.concurrency,
|
|
67
97
|
failedOnly: options.failedOnly,
|
|
68
98
|
failedTarget: options.failedTarget,
|
|
@@ -80,6 +110,7 @@ export class RustCoreBridge {
|
|
|
80
110
|
TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
|
|
81
111
|
RUST_LOG: 'info',
|
|
82
112
|
};
|
|
113
|
+
const runStartedAt = Date.now();
|
|
83
114
|
return new Promise((resolve, reject) => {
|
|
84
115
|
const child = spawn(binPath, [payload], {
|
|
85
116
|
env: runnerEnv,
|
|
@@ -97,6 +128,15 @@ export class RustCoreBridge {
|
|
|
97
128
|
level: 'INFO',
|
|
98
129
|
message: line,
|
|
99
130
|
});
|
|
131
|
+
// In interactive terminals, the reporter above already renders this tag as part of
|
|
132
|
+
// its live-updating spinner UI (`renderActiveSlots`), so echoing the raw tag too would
|
|
133
|
+
// just duplicate/clutter that UI. But that live rendering is a no-op when stdout isn't
|
|
134
|
+
// a real TTY (CI, or an external tool like the VS Code extension spawning this process
|
|
135
|
+
// via a pipe) — in that case this raw line is the *only* live signal a wrapping process
|
|
136
|
+
// can see, so it must still reach stdout verbatim.
|
|
137
|
+
if (!reporter.isInteractiveMode()) {
|
|
138
|
+
console.log(line);
|
|
139
|
+
}
|
|
100
140
|
continue;
|
|
101
141
|
}
|
|
102
142
|
try {
|
|
@@ -138,16 +178,37 @@ export class RustCoreBridge {
|
|
|
138
178
|
const str = data.toString();
|
|
139
179
|
// Ignore normal compilation progress
|
|
140
180
|
if (!str.includes('Compiling') && !str.includes('Checking') && !str.includes('Finished')) {
|
|
181
|
+
const trimmed = str.trim();
|
|
141
182
|
reporter.addLog({
|
|
142
183
|
timestamp: new Date().toLocaleTimeString(),
|
|
143
184
|
level: 'DEBUG',
|
|
144
|
-
message:
|
|
185
|
+
message: trimmed,
|
|
145
186
|
});
|
|
187
|
+
// Surface fatal runner errors (e.g. bootstrap failures like a missing APK) immediately —
|
|
188
|
+
// previously these only landed in the JSON report's `logs[]`, so a failed run gave no
|
|
189
|
+
// visible reason on the terminal.
|
|
190
|
+
if (trimmed.startsWith('Error:')) {
|
|
191
|
+
console.error(`\x1b[38;2;248;113;113m✖ ${trimmed}\x1b[0m`);
|
|
192
|
+
}
|
|
146
193
|
}
|
|
147
194
|
});
|
|
148
195
|
child.on('close', (code) => {
|
|
149
196
|
let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
|
|
197
|
+
// Only trust a pre-existing outputJsonPath when it was actually written during THIS run
|
|
198
|
+
// (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
|
|
199
|
+
// native runner writes its own complete report once the pipeline reaches the report
|
|
200
|
+
// stage, even for a run with failing tests (which still exits non-zero); but if it bails
|
|
201
|
+
// before ever getting there (missing APK, driver bootstrap error, etc.) a stale file from
|
|
202
|
+
// an unrelated previous run may still be sitting at this path — trusting it purely based
|
|
203
|
+
// on exit code would silently report that old run's result instead of the real failure.
|
|
204
|
+
let isFresh = false;
|
|
150
205
|
if (options.outputJsonPath && fs.existsSync(options.outputJsonPath)) {
|
|
206
|
+
try {
|
|
207
|
+
isFresh = fs.statSync(options.outputJsonPath).mtimeMs >= runStartedAt;
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
}
|
|
211
|
+
if (isFresh && options.outputJsonPath) {
|
|
151
212
|
try {
|
|
152
213
|
const raw = fs.readFileSync(options.outputJsonPath, 'utf-8');
|
|
153
214
|
const parsed = JSON.parse(raw);
|
|
@@ -161,7 +222,7 @@ export class RustCoreBridge {
|
|
|
161
222
|
}
|
|
162
223
|
catch { }
|
|
163
224
|
}
|
|
164
|
-
|
|
225
|
+
if (options.outputJsonPath && !isFresh) {
|
|
165
226
|
const outDir = path.dirname(options.outputJsonPath);
|
|
166
227
|
if (!fs.existsSync(outDir)) {
|
|
167
228
|
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.10",
|
|
4
4
|
"description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,12 +26,22 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
+
"@testspectra/matchers": "^1.1.8-rc.10",
|
|
30
|
+
"@testspectra/react": "^1.1.8-rc.10",
|
|
31
|
+
"@testspectra/skills": "^1.1.8-rc.10",
|
|
29
32
|
"chalk": "^5.3.0",
|
|
30
33
|
"commander": "^12.1.0",
|
|
31
34
|
"dotenv": "^16.4.5",
|
|
32
35
|
"ora": "^8.0.1",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
36
|
+
"typescript": "^5.6.3",
|
|
37
|
+
"yaml": "^2.7.0",
|
|
38
|
+
"zod": "^3.23.8"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@testspectra/cli-darwin-arm64": "1.1.8-rc.10",
|
|
42
|
+
"@testspectra/cli-linux-x64": "1.1.8-rc.10",
|
|
43
|
+
"@testspectra/cli-win32-arm64": "1.1.8-rc.10",
|
|
44
|
+
"@testspectra/cli-win32-x64": "1.1.8-rc.10"
|
|
35
45
|
},
|
|
36
46
|
"devDependencies": {
|
|
37
47
|
"@types/node": "^20.14.0",
|
|
@@ -39,7 +49,6 @@
|
|
|
39
49
|
"@wdio/mocha-framework": "^9.24.0",
|
|
40
50
|
"@wdio/types": "^9.24.0",
|
|
41
51
|
"esbuild": "^0.28.2",
|
|
42
|
-
"typescript": "^5.6.3",
|
|
43
52
|
"vitest": "^2.1.8"
|
|
44
53
|
},
|
|
45
54
|
"type": "module",
|
|
@@ -9,3 +9,8 @@
|
|
|
9
9
|
- Type checking: `pnpm type-check` (or `tsc -b`)
|
|
10
10
|
- Preflight doctor: `pnpm doctor` (or `spectra doctor`)
|
|
11
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`.
|
|
@@ -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.10",
|
|
21
|
+
"@testspectra/matchers": "^1.1.8-rc.10",
|
|
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
|
+
}
|