@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.
Files changed (256) hide show
  1. package/README.md +47 -0
  2. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  3. package/demo-app/assets/index-DOtRypCa.js +154 -0
  4. package/demo-app/index.html +2 -2
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/commands/__tests__/run-e2e.test.js +254 -13
  7. package/dist/commands/__tests__/run.test.js +92 -16
  8. package/dist/commands/add.js +2 -1
  9. package/dist/commands/demo.js +17 -7
  10. package/dist/commands/init.d.ts +1 -1
  11. package/dist/commands/init.js +8 -2
  12. package/dist/commands/lint.d.ts +5 -0
  13. package/dist/commands/lint.js +55 -0
  14. package/dist/commands/refactor.d.ts +2 -1
  15. package/dist/commands/refactor.js +129 -35
  16. package/dist/commands/run.d.ts +2 -3
  17. package/dist/commands/run.js +93 -28
  18. package/dist/commands/skills.d.ts +9 -0
  19. package/dist/commands/skills.js +28 -0
  20. package/dist/commands/sync-types.js +1 -0
  21. package/dist/commands/test.d.ts +17 -0
  22. package/dist/commands/test.js +104 -0
  23. package/dist/commands/watch.js +9 -3
  24. package/dist/config/loader.js +1 -1
  25. package/dist/config/schema.d.ts +5 -114
  26. package/dist/config/schema.js +1 -17
  27. package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
  28. package/dist/generator/__tests__/type-generator.test.js +39 -1
  29. package/dist/generator/tsconfig-generator.js +44 -16
  30. package/dist/generator/type-generator.d.ts +29 -3
  31. package/dist/generator/type-generator.js +115 -22
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +65 -29
  34. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  35. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  36. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  37. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  38. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  39. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  40. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  41. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  42. package/dist/linter/__tests__/step-parity.test.js +372 -0
  43. package/dist/linter/discovery.d.ts +30 -0
  44. package/dist/linter/discovery.js +249 -0
  45. package/dist/linter/extractor.d.ts +67 -0
  46. package/dist/linter/extractor.js +245 -0
  47. package/dist/linter/index.d.ts +7 -0
  48. package/dist/linter/index.js +7 -0
  49. package/dist/linter/parser.d.ts +24 -0
  50. package/dist/linter/parser.js +219 -0
  51. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  52. package/dist/linter/schemas/spec.schema.js +150 -0
  53. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  54. package/dist/linter/schemas/suite.schema.js +83 -0
  55. package/dist/linter/spec-linter.d.ts +23 -0
  56. package/dist/linter/spec-linter.js +630 -0
  57. package/dist/linter/types.d.ts +28 -0
  58. package/dist/linter/types.js +1 -0
  59. package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
  60. package/dist/refactor/refactor-engine.d.ts +25 -0
  61. package/dist/refactor/refactor-engine.js +374 -42
  62. package/dist/reporter/semantic-formatter.js +22 -2
  63. package/dist/reporter/types.d.ts +1 -1
  64. package/dist/runner/bridge.d.ts +1 -1
  65. package/dist/runner/bridge.js +85 -24
  66. package/dist/runner/reporter.d.ts +2 -0
  67. package/dist/runner/reporter.js +11 -3
  68. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  69. package/dist/utils/__tests__/api-server.test.js +62 -0
  70. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  71. package/dist/utils/__tests__/demo-server.test.js +35 -0
  72. package/dist/utils/api-server.d.ts +19 -0
  73. package/dist/utils/api-server.js +179 -0
  74. package/dist/utils/demo-server.d.ts +5 -0
  75. package/dist/utils/demo-server.js +8 -1
  76. package/package.json +13 -4
  77. package/templates/default/CLAUDE.md +5 -0
  78. package/templates/default/package.json +3 -3
  79. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  80. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  81. package/templates/default/page-objects/MatchersPage/mobile.ts +26 -2
  82. package/templates/default/page-objects/MatchersPage/web.ts +8 -2
  83. package/templates/default/page-objects/NavigationPage/mobile.ts +1 -1
  84. package/templates/default/page-objects/NavigationPage/web.ts +1 -1
  85. package/templates/default/page-objects/PlaygroundPage/mobile.ts +12 -6
  86. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -2
  87. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +45 -3
  88. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +7 -0
  89. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +8 -4
  90. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  91. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  92. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +13 -2
  93. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +7 -0
  94. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +6 -4
  95. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  96. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  97. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  98. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  99. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  100. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  101. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  102. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  103. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  104. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  105. package/templates/default/specs/ApiInterception/suite.md +1 -0
  106. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  107. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  108. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  109. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  110. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  111. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  112. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  113. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  114. package/templates/default/specs/Authentication/suite.md +9 -0
  115. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  116. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
  117. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  118. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  119. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  120. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  121. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  122. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
  123. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  124. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
  125. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  126. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  127. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  128. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  129. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  130. package/templates/default/specs/DeepLink/suite.md +9 -0
  131. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  132. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
  133. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  134. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  135. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
  136. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  137. package/templates/default/specs/ElementMatchers/suite.md +2 -1
  138. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  139. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  140. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  141. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  142. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  143. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  144. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  145. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  146. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  147. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
  148. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  149. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  150. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
  151. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  152. package/templates/default/spectra.config.ts +1 -17
  153. package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
  154. package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
  155. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  156. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  157. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  158. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  159. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  160. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  161. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  162. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  163. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  164. package/templates/nx/.nx/workspace-data/d/daemon.log +5803 -168
  165. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  166. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  167. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  168. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  169. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  170. package/templates/nx/.nx/workspace-data/project-graph.json +14388 -0
  171. package/templates/nx/package.json +3 -3
  172. package/templates/nx/packages/matchers/e2e/project.json +2 -2
  173. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
  174. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
  175. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
  176. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
  177. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  178. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  179. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  180. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
  181. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  182. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  183. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  184. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  185. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  186. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  187. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  188. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  189. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  190. package/templates/nx/packages/playground/e2e/project.json +2 -2
  191. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  192. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  193. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  194. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  195. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
  196. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
  197. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  198. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  199. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  200. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
  201. package/templates/nx/shared/testing/project.json +1 -2
  202. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  203. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  204. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  205. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  206. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  207. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  208. package/templates/nx/spectra.config.ts +1 -17
  209. package/templates/react-component/.cursorrules +17 -0
  210. package/templates/react-component/.vscode/extensions.json +5 -0
  211. package/templates/react-component/CLAUDE.md +14 -0
  212. package/templates/react-component/README.md +69 -0
  213. package/templates/react-component/fixtures/component-mock.json +12 -0
  214. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  215. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  216. package/templates/react-component/package.json +27 -0
  217. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  218. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  219. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  220. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  221. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  222. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  223. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  224. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  225. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  226. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  227. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  228. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  229. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  230. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  231. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  232. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  233. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  234. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  235. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  236. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  237. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  238. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  239. package/templates/react-component/spectra.config.ts +27 -0
  240. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  241. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  242. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  243. package/templates/react-component/src/test-utils.tsx +23 -0
  244. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  245. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  246. package/templates/react-component/tsconfig.json +8 -0
  247. package/bin/.testspectra-runner.hash +0 -1
  248. package/bin/testspectra-runner +0 -0
  249. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  250. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  251. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  252. package/dist/commands/cloud-run.d.ts +0 -13
  253. package/dist/commands/cloud-run.js +0 -120
  254. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  255. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  256. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -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
- // 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
- }
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("[TestSpectra] Native core runner binary 'testspectra-runner' not found.\n" +
51
- "Please build the project first using 'pnpm run build' or 'pnpm run build:core'.");
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
- spec: options.spec,
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: str.trim(),
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
- else if (options.outputJsonPath) {
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;
@@ -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
- const target = typeof payload.target === 'object' && payload.target !== null
67
- ? payload.target.selector || payload.target.name || payload.target.target || payload.target
68
- : payload.target;
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
- resolve({ url: `http://localhost:${this.port}`, port: this.port });
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.7",
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
- "zod": "^3.23.8",
34
- "@testspectra/matchers": "1.1.0"
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": "link:../..",
21
- "@testspectra/matchers": "link:../../../tools/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
+ }
@@ -0,0 +1,5 @@
1
+ class AuthPage {
2
+ get statusBadge() { return Spectra.get("~auth-status-badge"); }
3
+ }
4
+
5
+ export default new AuthPage();
@@ -0,0 +1,10 @@
1
+ class AuthPage {
2
+ get checkStatusButton() { return Spectra.get("#check-auth-status-btn"); }
3
+ get statusBadge() { return Spectra.get("#auth-status-badge"); }
4
+
5
+ async open() {
6
+ await Spectra.navigate("/#auth");
7
+ }
8
+ }
9
+
10
+ export default new AuthPage();