@testspectra/cli 1.1.7 → 1.1.8-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/bin/.testspectra-runner.hash +1 -1
  2. package/bin/spectra.js +0 -0
  3. package/bin/testspectra-runner.exe +0 -0
  4. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  5. package/demo-app/assets/index-DOtRypCa.js +154 -0
  6. package/demo-app/index.html +2 -2
  7. package/dist/.tsbuildinfo +1 -1
  8. package/dist/commands/__tests__/run-e2e.test.js +270 -29
  9. package/dist/commands/__tests__/run.test.js +92 -16
  10. package/dist/commands/demo.js +17 -7
  11. package/dist/commands/init.d.ts +1 -1
  12. package/dist/commands/init.js +154 -149
  13. package/dist/commands/lint.d.ts +5 -0
  14. package/dist/commands/lint.js +55 -0
  15. package/dist/commands/refactor.d.ts +2 -1
  16. package/dist/commands/refactor.js +129 -35
  17. package/dist/commands/run.d.ts +2 -3
  18. package/dist/commands/run.js +93 -28
  19. package/dist/commands/skills.d.ts +9 -0
  20. package/dist/commands/skills.js +28 -0
  21. package/dist/commands/sync-types.js +1 -0
  22. package/dist/commands/test.d.ts +17 -0
  23. package/dist/commands/test.js +104 -0
  24. package/dist/commands/watch.js +9 -3
  25. package/dist/config/loader.js +1 -1
  26. package/dist/config/schema.d.ts +5 -114
  27. package/dist/config/schema.js +1 -17
  28. package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
  29. package/dist/generator/__tests__/type-generator.test.js +1 -1
  30. package/dist/generator/tsconfig-generator.js +44 -16
  31. package/dist/generator/type-generator.d.ts +24 -1
  32. package/dist/generator/type-generator.js +104 -8
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +76 -40
  35. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  36. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  37. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  38. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  39. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  40. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  41. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  42. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  43. package/dist/linter/__tests__/step-parity.test.js +372 -0
  44. package/dist/linter/discovery.d.ts +30 -0
  45. package/dist/linter/discovery.js +249 -0
  46. package/dist/linter/extractor.d.ts +67 -0
  47. package/dist/linter/extractor.js +245 -0
  48. package/dist/linter/index.d.ts +7 -0
  49. package/dist/linter/index.js +7 -0
  50. package/dist/linter/parser.d.ts +24 -0
  51. package/dist/linter/parser.js +219 -0
  52. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  53. package/dist/linter/schemas/spec.schema.js +150 -0
  54. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  55. package/dist/linter/schemas/suite.schema.js +83 -0
  56. package/dist/linter/spec-linter.d.ts +23 -0
  57. package/dist/linter/spec-linter.js +630 -0
  58. package/dist/linter/types.d.ts +28 -0
  59. package/dist/linter/types.js +1 -0
  60. package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
  61. package/dist/refactor/refactor-engine.d.ts +25 -0
  62. package/dist/refactor/refactor-engine.js +374 -42
  63. package/dist/reporter/semantic-formatter.js +22 -2
  64. package/dist/reporter/types.d.ts +1 -1
  65. package/dist/runner/bridge.d.ts +1 -1
  66. package/dist/runner/bridge.js +34 -21
  67. package/dist/runner/reporter.d.ts +2 -0
  68. package/dist/runner/reporter.js +11 -3
  69. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  70. package/dist/utils/__tests__/api-server.test.js +62 -0
  71. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  72. package/dist/utils/__tests__/demo-server.test.js +35 -0
  73. package/dist/utils/api-server.d.ts +19 -0
  74. package/dist/utils/api-server.js +179 -0
  75. package/dist/utils/demo-server.d.ts +5 -0
  76. package/dist/utils/demo-server.js +8 -1
  77. package/package.json +29 -25
  78. package/templates/default/CLAUDE.md +16 -11
  79. package/templates/default/global-hooks/after/mobile.hook.ts +3 -3
  80. package/templates/default/global-hooks/after/web.hook.ts +3 -3
  81. package/templates/default/global-hooks/before/mobile.hook.ts +3 -3
  82. package/templates/default/global-hooks/before/web.hook.ts +3 -3
  83. package/templates/default/package.json +3 -3
  84. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  85. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  86. package/templates/default/page-objects/MatchersPage/mobile.ts +70 -46
  87. package/templates/default/page-objects/MatchersPage/web.ts +43 -37
  88. package/templates/default/page-objects/NavigationPage/mobile.ts +14 -14
  89. package/templates/default/page-objects/NavigationPage/web.ts +8 -8
  90. package/templates/default/page-objects/PlaygroundPage/mobile.ts +42 -36
  91. package/templates/default/page-objects/PlaygroundPage/web.ts +31 -31
  92. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -11
  93. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -12
  94. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -27
  95. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  96. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  97. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -16
  98. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -12
  99. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -24
  100. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  101. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  102. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  103. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  104. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  105. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  106. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  107. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  108. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  109. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  110. package/templates/default/specs/ApiInterception/suite.md +10 -9
  111. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  112. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  113. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  114. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  115. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  116. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  117. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  118. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  119. package/templates/default/specs/Authentication/suite.md +9 -0
  120. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +9 -9
  121. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +17 -12
  122. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
  123. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  124. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  125. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  126. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  127. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +19 -12
  128. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +11 -11
  129. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +17 -12
  130. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
  131. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  132. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  133. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  134. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  135. package/templates/default/specs/DeepLink/suite.md +9 -0
  136. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +11 -11
  137. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +17 -12
  138. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
  139. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +29 -29
  140. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +19 -12
  141. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
  142. package/templates/default/specs/ElementMatchers/suite.md +10 -9
  143. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  144. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  145. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  146. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  147. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  148. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  149. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  150. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  151. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +6 -6
  152. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +18 -12
  153. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
  154. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +6 -6
  155. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +18 -12
  156. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
  157. package/templates/default/spectra.config.ts +52 -68
  158. package/templates/default/support/actions/fillCredentials/mobile.action.ts +4 -4
  159. package/templates/default/support/actions/fillCredentials/web.action.ts +4 -4
  160. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  161. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  162. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +5 -5
  163. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +5 -5
  164. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +6 -6
  165. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +6 -6
  166. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db +0 -0
  167. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-shm +0 -0
  168. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-wal +0 -0
  169. package/templates/nx/.nx/workspace-data/d/daemon.log +1577 -168
  170. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  171. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  172. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  173. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  174. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  175. package/templates/nx/.nx/workspace-data/project-graph.json +14389 -0
  176. package/templates/nx/CLAUDE.md +10 -10
  177. package/templates/nx/package.json +3 -3
  178. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -3
  179. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -27
  180. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -3
  181. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -24
  182. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -3
  183. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
  184. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -3
  185. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +17 -17
  186. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -3
  187. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
  188. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -3
  189. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
  190. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -3
  191. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
  192. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  193. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  194. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  195. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -3
  196. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
  197. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -3
  198. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
  199. package/templates/nx/pnpm-lock.yaml +6663 -6663
  200. package/templates/nx/pnpm-workspace.yaml +9 -9
  201. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +33 -33
  202. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +91 -37
  203. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +5 -5
  204. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +8 -8
  205. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +26 -26
  206. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +31 -31
  207. package/templates/nx/shared/testing/project.json +19 -19
  208. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +4 -4
  209. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +4 -4
  210. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +3 -3
  211. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +5 -5
  212. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +4 -4
  213. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +6 -6
  214. package/templates/nx/spectra.config.ts +52 -68
  215. package/templates/react-component/.cursorrules +17 -0
  216. package/templates/react-component/.vscode/extensions.json +5 -0
  217. package/templates/react-component/CLAUDE.md +14 -0
  218. package/templates/react-component/README.md +69 -0
  219. package/templates/react-component/fixtures/component-mock.json +12 -0
  220. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  221. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  222. package/templates/react-component/package.json +27 -0
  223. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  224. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  225. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  226. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  227. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  228. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  229. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  230. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  231. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  232. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  233. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  234. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  235. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  236. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  237. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  238. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  239. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  240. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  241. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  242. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  243. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  244. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  245. package/templates/react-component/spectra.config.ts +27 -0
  246. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  247. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  248. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  249. package/templates/react-component/src/test-utils.tsx +23 -0
  250. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  251. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  252. package/templates/react-component/tsconfig.json +8 -0
  253. package/bin/testspectra-runner +0 -0
  254. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  255. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  256. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  257. package/dist/commands/cloud-run.d.ts +0 -13
  258. package/dist/commands/cloud-run.js +0 -120
  259. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  260. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  261. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -4,23 +4,6 @@ import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
- // npm/pnpm pack+extract doesn't reliably preserve the executable bit on
8
- // non-`bin`-field binaries — testspectra-runner has been observed installed
9
- // as 0644 from a tarball that was 0755 at publish time. Defensive chmod so
10
- // every consumer works regardless of how it arrived on disk.
11
- function ensureExecutable(binPath) {
12
- if (process.platform === 'win32')
13
- return;
14
- try {
15
- const mode = fs.statSync(binPath).mode;
16
- if ((mode & 0o111) === 0) {
17
- fs.chmodSync(binPath, 0o755);
18
- }
19
- }
20
- catch {
21
- // best-effort — if this fails, the spawn below will surface the real error
22
- }
23
- }
24
7
  export class RustCoreBridge {
25
8
  static resolveBinaryPath() {
26
9
  // 1. Look for precompiled release/debug binary in workspace target
@@ -40,7 +23,6 @@ export class RustCoreBridge {
40
23
  ];
41
24
  for (const p of candidatePaths) {
42
25
  if (fs.existsSync(p)) {
43
- ensureExecutable(p);
44
26
  return p;
45
27
  }
46
28
  if (fs.existsSync(`${p}.exe`)) {
@@ -62,7 +44,7 @@ export class RustCoreBridge {
62
44
  targetDevice: options.targetDevice,
63
45
  licenseKey: options.licenseKey,
64
46
  outputJsonPath: options.outputJsonPath,
65
- spec: options.spec,
47
+ specs: options.specs,
66
48
  concurrency: options.concurrency,
67
49
  failedOnly: options.failedOnly,
68
50
  failedTarget: options.failedTarget,
@@ -80,6 +62,7 @@ export class RustCoreBridge {
80
62
  TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
81
63
  RUST_LOG: 'info',
82
64
  };
65
+ const runStartedAt = Date.now();
83
66
  return new Promise((resolve, reject) => {
84
67
  const child = spawn(binPath, [payload], {
85
68
  env: runnerEnv,
@@ -97,6 +80,15 @@ export class RustCoreBridge {
97
80
  level: 'INFO',
98
81
  message: line,
99
82
  });
83
+ // In interactive terminals, the reporter above already renders this tag as part of
84
+ // its live-updating spinner UI (`renderActiveSlots`), so echoing the raw tag too would
85
+ // just duplicate/clutter that UI. But that live rendering is a no-op when stdout isn't
86
+ // a real TTY (CI, or an external tool like the VS Code extension spawning this process
87
+ // via a pipe) — in that case this raw line is the *only* live signal a wrapping process
88
+ // can see, so it must still reach stdout verbatim.
89
+ if (!reporter.isInteractiveMode()) {
90
+ console.log(line);
91
+ }
100
92
  continue;
101
93
  }
102
94
  try {
@@ -138,16 +130,37 @@ export class RustCoreBridge {
138
130
  const str = data.toString();
139
131
  // Ignore normal compilation progress
140
132
  if (!str.includes('Compiling') && !str.includes('Checking') && !str.includes('Finished')) {
133
+ const trimmed = str.trim();
141
134
  reporter.addLog({
142
135
  timestamp: new Date().toLocaleTimeString(),
143
136
  level: 'DEBUG',
144
- message: str.trim(),
137
+ message: trimmed,
145
138
  });
139
+ // Surface fatal runner errors (e.g. bootstrap failures like a missing APK) immediately —
140
+ // previously these only landed in the JSON report's `logs[]`, so a failed run gave no
141
+ // visible reason on the terminal.
142
+ if (trimmed.startsWith('Error:')) {
143
+ console.error(`\x1b[38;2;248;113;113m✖ ${trimmed}\x1b[0m`);
144
+ }
146
145
  }
147
146
  });
148
147
  child.on('close', (code) => {
149
148
  let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
149
+ // Only trust a pre-existing outputJsonPath when it was actually written during THIS run
150
+ // (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
151
+ // native runner writes its own complete report once the pipeline reaches the report
152
+ // stage, even for a run with failing tests (which still exits non-zero); but if it bails
153
+ // before ever getting there (missing APK, driver bootstrap error, etc.) a stale file from
154
+ // an unrelated previous run may still be sitting at this path — trusting it purely based
155
+ // on exit code would silently report that old run's result instead of the real failure.
156
+ let isFresh = false;
150
157
  if (options.outputJsonPath && fs.existsSync(options.outputJsonPath)) {
158
+ try {
159
+ isFresh = fs.statSync(options.outputJsonPath).mtimeMs >= runStartedAt;
160
+ }
161
+ catch { }
162
+ }
163
+ if (isFresh && options.outputJsonPath) {
151
164
  try {
152
165
  const raw = fs.readFileSync(options.outputJsonPath, 'utf-8');
153
166
  const parsed = JSON.parse(raw);
@@ -161,7 +174,7 @@ export class RustCoreBridge {
161
174
  }
162
175
  catch { }
163
176
  }
164
- else if (options.outputJsonPath) {
177
+ if (options.outputJsonPath && !isFresh) {
165
178
  const outDir = path.dirname(options.outputJsonPath);
166
179
  if (!fs.existsSync(outDir)) {
167
180
  fs.mkdirSync(outDir, { recursive: true });
@@ -43,6 +43,8 @@ export declare class Reporter {
43
43
  private spinnerIndex;
44
44
  private spinnerTimer;
45
45
  constructor();
46
+ /** Whether this reporter is drawing the live-updating interactive terminal UI (real TTY, not CI/VS Code/test). */
47
+ isInteractiveMode(): boolean;
46
48
  private clearActiveSlots;
47
49
  private renderActiveSlots;
48
50
  private resolveTestCase;
@@ -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.2",
4
4
  "description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,15 @@
16
16
  "spectra": "./bin/spectra.js",
17
17
  "testspectra": "./bin/spectra.js"
18
18
  },
19
+ "scripts": {
20
+ "build:rust": "node scripts/ensure-runner-binary.js",
21
+ "prebuild": "node scripts/ensure-runner-binary.js",
22
+ "build": "tsc && node scripts/copy-demo.js",
23
+ "watch": "tsc -w",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "prepublishOnly": "npm run build"
27
+ },
19
28
  "files": [
20
29
  "dist",
21
30
  "bin",
@@ -25,31 +34,26 @@
25
34
  "LICENSE.md"
26
35
  ],
27
36
  "dependencies": {
28
- "@clack/prompts": "^1.7.0",
29
- "chalk": "^5.3.0",
30
- "commander": "^12.1.0",
31
- "dotenv": "^16.4.5",
32
- "ora": "^8.0.1",
33
- "zod": "^3.23.8",
34
- "@testspectra/matchers": "1.1.0"
37
+ "@clack/prompts": "catalog:",
38
+ "@testspectra/matchers": "^1.1.8-rc.2",
39
+ "@testspectra/react": "^1.1.8-rc.2",
40
+ "@testspectra/skills": "^1.1.8-rc.2",
41
+ "chalk": "catalog:",
42
+ "commander": "catalog:",
43
+ "dotenv": "catalog:",
44
+ "ora": "catalog:",
45
+ "typescript": "catalog:",
46
+ "yaml": "catalog:",
47
+ "zod": "catalog:"
35
48
  },
36
49
  "devDependencies": {
37
- "@types/node": "^20.14.0",
38
- "@wdio/globals": "^9.24.0",
39
- "@wdio/mocha-framework": "^9.24.0",
40
- "@wdio/types": "^9.24.0",
41
- "esbuild": "^0.28.2",
42
- "typescript": "^5.6.3",
43
- "vitest": "^2.1.8"
50
+ "@types/node": "catalog:",
51
+ "@wdio/globals": "catalog:",
52
+ "@wdio/mocha-framework": "catalog:",
53
+ "@wdio/types": "catalog:",
54
+ "esbuild": "catalog:",
55
+ "vitest": "catalog:"
44
56
  },
45
57
  "type": "module",
46
- "license": "SEE LICENSE IN LICENSE.md",
47
- "scripts": {
48
- "build:rust": "node scripts/ensure-runner-binary.js",
49
- "prebuild": "node scripts/ensure-runner-binary.js",
50
- "build": "tsc && node scripts/copy-demo.js",
51
- "watch": "tsc -w",
52
- "test": "vitest run",
53
- "test:watch": "vitest"
54
- }
55
- }
58
+ "license": "SEE LICENSE IN LICENSE.md"
59
+ }
@@ -1,11 +1,16 @@
1
- # CLAUDE.md - TestSpectra Framework Conventions
2
-
3
- ## Commands
4
- - Run all Web tests: `pnpm test:web` (or `spectra run -t web`)
5
- - Run all Mobile tests: `pnpm test:mobile` (or `spectra run -t mobile`)
6
- - Run Android tests: `pnpm test:android` (or `spectra run -t android`)
7
- - Run iOS tests: `pnpm test:ios` (or `spectra run -t ios`)
8
- - Run specific spec: `spectra run "specs/Playground/TC-0001-form-controls/mobile.test.ts" -t mobile`
9
- - Type checking: `pnpm type-check` (or `tsc -b`)
10
- - Preflight doctor: `pnpm doctor` (or `spectra doctor`)
11
- - Sync ambient types: `pnpm sync-types`
1
+ # CLAUDE.md - TestSpectra Framework Conventions
2
+
3
+ ## Commands
4
+ - Run all Web tests: `pnpm test:web` (or `spectra run -t web`)
5
+ - Run all Mobile tests: `pnpm test:mobile` (or `spectra run -t mobile`)
6
+ - Run Android tests: `pnpm test:android` (or `spectra run -t android`)
7
+ - Run iOS tests: `pnpm test:ios` (or `spectra run -t ios`)
8
+ - Run specific spec: `spectra run "specs/Playground/TC-0001-form-controls/mobile.test.ts" -t mobile`
9
+ - Type checking: `pnpm type-check` (or `tsc -b`)
10
+ - Preflight doctor: `pnpm doctor` (or `spectra doctor`)
11
+ - Sync ambient types: `pnpm sync-types`
12
+
13
+ ## Element Selectors
14
+ - `~name` → **accessibility id** (Android `content-desc`, RN `accessibilityLabel`, web `aria-label`) — the portable default.
15
+ - `#name` → **native id** (Android `resource-id`, RN `testID`, web CSS `id`).
16
+ - Each prefix maps to exactly one attribute (no silent cross-matching). Prefer `~` for user-perceivable elements, `#` for stable test ids. See `docs/v2/cli/selectors.md`.
@@ -1,3 +1,3 @@
1
- export default async function () {
2
- // Global mobile teardown
3
- }
1
+ export default async function () {
2
+ // Global mobile teardown
3
+ }
@@ -1,3 +1,3 @@
1
- export default async function () {
2
- // Global web teardown
3
- }
1
+ export default async function () {
2
+ // Global web teardown
3
+ }
@@ -1,3 +1,3 @@
1
- export default async function () {
2
- // Global mobile initialization
3
- }
1
+ export default async function () {
2
+ // Global mobile initialization
3
+ }
@@ -1,3 +1,3 @@
1
- export default async function () {
2
- // Global web initialization
3
- }
1
+ export default async function () {
2
+ // Global web initialization
3
+ }
@@ -17,8 +17,8 @@
17
17
  "postinstall": "spectra sync-types"
18
18
  },
19
19
  "devDependencies": {
20
- "@testspectra/cli": "link:../..",
21
- "@testspectra/matchers": "link:../../../tools/matchers",
20
+ "@testspectra/cli": "^1.1.8-rc.2",
21
+ "@testspectra/matchers": "^1.1.8-rc.2",
22
22
  "@types/node": "^20.14.0",
23
23
  "@wdio/cli": "^9.2.8",
24
24
  "@wdio/local-runner": "^9.2.8",
@@ -29,4 +29,4 @@
29
29
  "webdriverio": "^9.2.8",
30
30
  "typescript": "^5.4.5"
31
31
  }
32
- }
32
+ }
@@ -0,0 +1,5 @@
1
+ class AuthPage {
2
+ get statusBadge() { return Spectra.get("~auth-status-badge"); }
3
+ }
4
+
5
+ export default new AuthPage();