@testspectra/cli 1.1.7 → 1.1.8-rc.1

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 (251) hide show
  1. package/bin/.testspectra-runner.hash +1 -1
  2. package/bin/testspectra-runner +0 -0
  3. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  4. package/demo-app/assets/index-DOtRypCa.js +154 -0
  5. package/demo-app/index.html +2 -2
  6. package/dist/.tsbuildinfo +1 -1
  7. package/dist/commands/__tests__/run-e2e.test.js +254 -13
  8. package/dist/commands/__tests__/run.test.js +92 -16
  9. package/dist/commands/demo.js +17 -7
  10. package/dist/commands/init.d.ts +1 -1
  11. package/dist/commands/init.js +6 -1
  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 +76 -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 +1 -1
  29. package/dist/generator/tsconfig-generator.js +44 -16
  30. package/dist/generator/type-generator.d.ts +24 -1
  31. package/dist/generator/type-generator.js +104 -8
  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 +17 -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 +34 -21
  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 +29 -25
  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 +5388 -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 +14389 -0
  171. package/templates/nx/package.json +3 -3
  172. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
  173. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
  174. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
  175. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
  176. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  177. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  178. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  179. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
  180. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  181. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  182. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  183. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  184. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  185. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  186. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  187. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  188. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  189. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  190. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  191. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  192. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  193. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
  194. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
  195. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  196. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  197. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  198. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
  199. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  200. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  201. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  202. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  203. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  204. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  205. package/templates/nx/spectra.config.ts +1 -17
  206. package/templates/react-component/.cursorrules +17 -0
  207. package/templates/react-component/.vscode/extensions.json +5 -0
  208. package/templates/react-component/CLAUDE.md +14 -0
  209. package/templates/react-component/README.md +69 -0
  210. package/templates/react-component/fixtures/component-mock.json +12 -0
  211. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  212. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  213. package/templates/react-component/package.json +27 -0
  214. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  215. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  216. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  217. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  218. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  219. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  220. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  221. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  222. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  223. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  224. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  225. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  226. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  227. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  228. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  229. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  230. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  231. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  232. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  233. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  234. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  235. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  236. package/templates/react-component/spectra.config.ts +27 -0
  237. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  238. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  239. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  240. package/templates/react-component/src/test-utils.tsx +23 -0
  241. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  242. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  243. package/templates/react-component/tsconfig.json +8 -0
  244. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  245. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  246. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  247. package/dist/commands/cloud-run.d.ts +0 -13
  248. package/dist/commands/cloud-run.js +0 -120
  249. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  250. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  251. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -5,11 +5,13 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi }
5
5
  import { runCommand } from '../run.js';
6
6
  import { initCommand } from '../init.js';
7
7
  import { DemoServer } from '../../utils/demo-server.js';
8
+ import { DemoApiServer } from '../../utils/api-server.js';
8
9
  import { RustCoreBridge } from '../../runner/bridge.js';
9
10
  describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/core/e2e-testing-plan.md)', () => {
10
11
  let tmpDir;
11
12
  let originalCwd;
12
13
  let demoServer;
14
+ let apiServer;
13
15
  let consoleLogs = [];
14
16
  const originalLog = console.log;
15
17
  beforeAll(async () => {
@@ -21,13 +23,27 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
21
23
  catch {
22
24
  // Server may already be running on port 5173
23
25
  }
26
+ // The demo API server runs separately (port 5200). Tests that don't pass --demo still need it
27
+ // reachable: the web app now fetches the API cross-origin, so a JSON POST triggers a CORS
28
+ // preflight (OPTIONS) that isn't mocked and must reach a live API server.
29
+ apiServer = new DemoApiServer();
30
+ try {
31
+ await apiServer.start();
32
+ }
33
+ catch {
34
+ // API server may already be running on port 5200
35
+ }
24
36
  });
25
37
  afterAll(async () => {
26
- // 4. AfterAll: Stop demo server
38
+ // 4. AfterAll: Stop demo servers
27
39
  try {
28
40
  await demoServer.stop();
29
41
  }
30
42
  catch { }
43
+ try {
44
+ await apiServer.stop();
45
+ }
46
+ catch { }
31
47
  });
32
48
  beforeEach(async () => {
33
49
  originalCwd = process.cwd();
@@ -64,7 +80,7 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
64
80
  })();
65
81
  expect(binaryExists).toBe(true);
66
82
  // Execute testspectra run -t web on full template project
67
- await runCommand(undefined, { target: 'web' });
83
+ await runCommand(undefined, { target: 'web', demo: true });
68
84
  // Assert reports created
69
85
  const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
70
86
  expect(fs.existsSync(resultJsonPath)).toBe(true);
@@ -82,12 +98,12 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
82
98
  // Performance RSS assertion (< 30 MB)
83
99
  expect(resultJson.performance.peakMemoryRssMb).toBeLessThan(30);
84
100
  expect(resultJson.suites.length).toBeGreaterThanOrEqual(1);
85
- }, 15000);
101
+ }, 30000);
86
102
  // E2E-02: Single Suite Targeted Run
87
103
  it('E2E-02: Single Suite Targeted Run executes only specified spec', async () => {
88
104
  const targetSpec = 'specs/Playground/TC-0001-form-controls/web.test.ts';
89
105
  expect(fs.existsSync(path.join(tmpDir, targetSpec))).toBe(true);
90
- await runCommand(targetSpec, { target: 'web' });
106
+ await runCommand([targetSpec], { target: 'web', demo: true });
91
107
  const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
92
108
  expect(fs.existsSync(resultJsonPath)).toBe(true);
93
109
  const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
@@ -99,10 +115,10 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
99
115
  expect(resultJson.summary.passed).toBe(1);
100
116
  expect(resultJson.summary.failed).toBe(0);
101
117
  expect(resultJson.summary.passRate).toBe('100%');
102
- }, 15000);
118
+ }, 30000);
103
119
  // E2E-03: Network Recording Audit
104
120
  it('E2E-03: Network Recording Audit creates network-resources.json with request entries', async () => {
105
- await runCommand(undefined, { target: 'web' });
121
+ await runCommand(undefined, { target: 'web', demo: true });
106
122
  const networkJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'network-resources.json');
107
123
  expect(fs.existsSync(networkJsonPath)).toBe(true);
108
124
  const networkRaw = fs.readFileSync(networkJsonPath, 'utf-8');
@@ -114,17 +130,17 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
114
130
  expect(first).toHaveProperty('url');
115
131
  expect(first).toHaveProperty('method');
116
132
  expect(first).toHaveProperty('status');
117
- }, 15000);
133
+ }, 30000);
118
134
  // E2E-04: Semantic Logs Streaming & Summary Card Terminal
119
135
  it('E2E-04: Semantic Logs Streaming displays natural logs and summary card', async () => {
120
- await runCommand(undefined, { target: 'web' });
136
+ await runCommand(undefined, { target: 'web', demo: true });
121
137
  const allOutput = consoleLogs.join('\n');
122
138
  expect(allOutput).toContain('TESTSPECTRA EXECUTION SUMMARY');
123
139
  expect(allOutput).toContain('PASSED');
124
140
  expect(allOutput).toContain('Platform');
125
141
  expect(allOutput).toContain('Duration');
126
142
  expect(allOutput).toContain('Report');
127
- }, 15000);
143
+ }, 30000);
128
144
  // E2E-05: Failure Handling & Trace
129
145
  it('E2E-05: Failure Handling & Trace properly records failed status and error details', async () => {
130
146
  // Inject a failing step / invalid selector in a new test case
@@ -137,14 +153,14 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
137
153
  `, 'utf-8');
138
154
  // Mock exit to prevent process termination during unit test assertion
139
155
  const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { }));
140
- await runCommand('specs/Playground/TC-0099-failing/web.test.ts', { target: 'web' });
156
+ await runCommand(['specs/Playground/TC-0099-failing/web.test.ts'], { target: 'web', demo: true });
141
157
  const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
142
158
  expect(fs.existsSync(resultJsonPath)).toBe(true);
143
159
  const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
144
160
  // If failure was triggered, status is failed and failed count >= 1
145
161
  expect(resultJson.status).toBeDefined();
146
162
  exitSpy.mockRestore();
147
- }, 15000);
163
+ }, 30000);
148
164
  // E2E-06: Spectra Static API & Assertion Mismatch Failure in Real Chromium
149
165
  it('E2E-06: Spectra static actions and negative assertion failures strictly fail in real Chromium', async () => {
150
166
  // 1. Test a valid spec using pure Spectra static commands
@@ -158,7 +174,7 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
158
174
  await Spectra.getAll(".demo-product-item").shouldHaveLengthGreaterThan(0);
159
175
  });
160
176
  `, 'utf-8');
161
- await runCommand('specs/Playground/TC-0100-spectra-static/web.test.ts', { target: 'web' });
177
+ await runCommand(['specs/Playground/TC-0100-spectra-static/web.test.ts'], { target: 'web', demo: true });
162
178
  const resultStatic = JSON.parse(fs.readFileSync(path.join(tmpDir, '.testspectra', 'reports', 'result.json'), 'utf-8'));
163
179
  expect(resultStatic.status).toBe('passed');
164
180
  expect(resultStatic.summary.passed).toBe(1);
@@ -172,10 +188,235 @@ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/cor
172
188
  });
173
189
  `, 'utf-8');
174
190
  const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { }));
175
- await runCommand('specs/Playground/TC-0101-mismatch/web.test.ts', { target: 'web' });
191
+ await runCommand(['specs/Playground/TC-0101-mismatch/web.test.ts'], { target: 'web', demo: true });
176
192
  const resultMismatch = JSON.parse(fs.readFileSync(path.join(tmpDir, '.testspectra', 'reports', 'result.json'), 'utf-8'));
177
193
  expect(resultMismatch.status).toBe('failed');
178
194
  expect(resultMismatch.summary.failed).toBeGreaterThanOrEqual(1);
179
195
  exitSpy.mockRestore();
180
196
  }, 25000);
197
+ // E2E-07: Spectra.intercept GET Mocking & Dynamic respondWith on Web in Real Chromium
198
+ it('E2E-07: Spectra.intercept GET Mocking & Dynamic respondWith on Web in real Chromium', async () => {
199
+ const targetSpec = 'specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts';
200
+ expect(fs.existsSync(path.join(tmpDir, targetSpec))).toBe(true);
201
+ await runCommand([targetSpec], { target: 'web' });
202
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
203
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
204
+ const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
205
+ expect(resultJson.version).toBe('2.0.0');
206
+ expect(resultJson.platform).toBe('web');
207
+ expect(resultJson.status).toBe('passed');
208
+ expect(resultJson.summary.totalSuites).toBe(1);
209
+ expect(resultJson.summary.totalTests).toBe(1);
210
+ expect(resultJson.summary.passed).toBe(1);
211
+ expect(resultJson.summary.failed).toBe(0);
212
+ expect(resultJson.summary.passRate).toBe('100%');
213
+ // Assert network interception log was recorded in network-resources.json
214
+ const networkJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'network-resources.json');
215
+ expect(fs.existsSync(networkJsonPath)).toBe(true);
216
+ const networkEntries = JSON.parse(fs.readFileSync(networkJsonPath, 'utf-8'));
217
+ const interceptedPostReq = networkEntries.find((entry) => entry.url && entry.url.includes('/api/posts'));
218
+ expect(interceptedPostReq).toBeDefined();
219
+ expect(interceptedPostReq.status).toBe(200);
220
+ }, 20000);
221
+ // E2E-08: Spectra.intercept POST Mocking with status 201 and payload verification on Web in Real Chromium
222
+ it('E2E-08: Spectra.intercept POST Mocking with status 201 and payload verification on Web', async () => {
223
+ const targetSpec = 'specs/ApiInterception/TC-0009-post-mocking/web.test.ts';
224
+ expect(fs.existsSync(path.join(tmpDir, targetSpec))).toBe(true);
225
+ await runCommand([targetSpec], { target: 'web' });
226
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
227
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
228
+ const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
229
+ expect(resultJson.version).toBe('2.0.0');
230
+ expect(resultJson.platform).toBe('web');
231
+ expect(resultJson.status).toBe('passed');
232
+ expect(resultJson.summary.totalSuites).toBe(1);
233
+ expect(resultJson.summary.totalTests).toBe(1);
234
+ expect(resultJson.summary.passed).toBe(1);
235
+ expect(resultJson.summary.failed).toBe(0);
236
+ const networkJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'network-resources.json');
237
+ expect(fs.existsSync(networkJsonPath)).toBe(true);
238
+ const networkEntries = JSON.parse(fs.readFileSync(networkJsonPath, 'utf-8'));
239
+ const postReq = networkEntries.find((entry) => entry.url && entry.url.includes('/api/posts') && entry.status === 201);
240
+ expect(postReq).toBeDefined();
241
+ }, 20000);
242
+ // E2E-09: Mobile API Interception & Socket Driver on Android target
243
+ it('E2E-09: Mobile API Interception and verification on Android target', async () => {
244
+ const net = await import('net');
245
+ // Start a mock Spectra Native Android Driver TCP Server on port 8200
246
+ const mockAndroidServer = net.createServer((socket) => {
247
+ let rxBuf = Buffer.alloc(0);
248
+ let fetchClickCount = 0;
249
+ socket.on('data', (chunk) => {
250
+ rxBuf = Buffer.concat([rxBuf, chunk]);
251
+ while (rxBuf.length >= 4) {
252
+ const payloadLen = rxBuf.readUInt32BE(0);
253
+ if (rxBuf.length < 4 + payloadLen)
254
+ break;
255
+ const payloadStr = rxBuf.slice(4, 4 + payloadLen).toString('utf8');
256
+ rxBuf = rxBuf.slice(4 + payloadLen);
257
+ try {
258
+ const msg = JSON.parse(payloadStr);
259
+ const cmd = msg.cmd;
260
+ const id = msg.id;
261
+ const params = msg.params || {};
262
+ let responseData = {};
263
+ if (cmd === 'FIND_ALL') {
264
+ const sel = (params.selector || '').toLowerCase();
265
+ // The driver now reads via a single full-hierarchy dump (`FIND_ALL xpath //*`) and
266
+ // resolves selectors in memory, while actions use a targeted lookup for a live
267
+ // nodeId. This mock returns the same node set for both, so either path resolves.
268
+ //
269
+ // TC-0008's mobile.test.ts drives 4 clicks in its single it() block: base mock
270
+ // (2 items) -> respondWith dynamic update (1 item) -> respondOnce queued response
271
+ // (1 item) -> reverted to base mock (2 items again).
272
+ const items = fetchClickCount === 2
273
+ ? [{ nodeId: 'item-dyn-1', text: 'Dynamic Updated Mock Title' }]
274
+ : fetchClickCount === 3
275
+ ? [{ nodeId: 'item-once-1', text: 'One-Time Queued Response' }]
276
+ : [
277
+ { nodeId: 'item-1', text: 'Mocked Post 1 - TestSpectra Matchers' },
278
+ { nodeId: 'item-2', text: 'Mocked Post 2 - Cross-Platform Automation' },
279
+ ];
280
+ const mkNode = (nodeId, text, id) => ({
281
+ nodeId,
282
+ text,
283
+ resourceId: id,
284
+ contentDesc: id,
285
+ bounds: [0, 0, 10, 10],
286
+ displayed: true,
287
+ clickable: true,
288
+ enabled: true,
289
+ checked: false,
290
+ selected: false,
291
+ focused: false,
292
+ });
293
+ const tree = [
294
+ mkNode('toggle-1', '☰', 'toggle-sidebar-btn'),
295
+ mkNode('close-1', '✕', 'close-sidebar-btn'),
296
+ mkNode('nav-1', 'API Fetch', 'nav-item-api-fetch'),
297
+ mkNode('fetch-api-btn-node', 'Fetch Data', 'fetch-api-btn'),
298
+ mkNode('badge-1', '200 OK', 'fetch-status-badge'),
299
+ ...items.map((n, i) => mkNode(n.nodeId || `item-${i}`, n.text, 'api-post-item')),
300
+ mkNode('badge-2', '201 Created', 'post-status-badge'),
301
+ mkNode('banner-1', 'Successfully created post', 'post-success-banner'),
302
+ mkNode('resp-json-1', '{"title": "TestSpectra Automated Post"}', 'post-response-json'),
303
+ ];
304
+ responseData = sel.startsWith('//')
305
+ ? tree
306
+ : tree.filter((n) => n.resourceId === sel || n.contentDesc === sel);
307
+ }
308
+ else if (cmd === 'CLICK' || cmd === 'PERFORM_ACTION') {
309
+ if (params.node_id === 'fetch-api-btn-node' || params.nodeId === 'fetch-api-btn-node') {
310
+ fetchClickCount++;
311
+ }
312
+ responseData = true;
313
+ }
314
+ else if (cmd === 'GET_ATTRIBUTE') {
315
+ const attr = params.attr;
316
+ if (attr === 'text') {
317
+ responseData = '200 OK';
318
+ }
319
+ else {
320
+ responseData = 'Mock Attribute';
321
+ }
322
+ }
323
+ const respObj = { id, ok: true, data: responseData };
324
+ const respStr = JSON.stringify(respObj);
325
+ const respLen = Buffer.byteLength(respStr, 'utf8');
326
+ const frame = Buffer.alloc(4 + respLen);
327
+ frame.writeUInt32BE(respLen, 0);
328
+ frame.write(respStr, 4, 'utf8');
329
+ socket.write(frame);
330
+ }
331
+ catch { }
332
+ }
333
+ });
334
+ });
335
+ let isMockServerRunning = false;
336
+ await new Promise((resolve) => {
337
+ mockAndroidServer.once('error', (err) => {
338
+ if (err.code === 'EADDRINUSE') {
339
+ resolve();
340
+ }
341
+ });
342
+ mockAndroidServer.listen(8200, '127.0.0.1', () => {
343
+ isMockServerRunning = true;
344
+ resolve();
345
+ });
346
+ });
347
+ try {
348
+ const configPath = path.join(tmpDir, 'spectra.config.ts');
349
+ if (fs.existsSync(configPath)) {
350
+ let conf = fs.readFileSync(configPath, 'utf-8');
351
+ conf = conf.replace(/appPackage:\s*["'][^"']*["'],/, 'appPackage: "",');
352
+ fs.writeFileSync(configPath, conf, 'utf-8');
353
+ }
354
+ const targetSpec = 'specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts';
355
+ expect(fs.existsSync(path.join(tmpDir, targetSpec))).toBe(true);
356
+ await runCommand([targetSpec], { target: 'android', device: 'mock-device' });
357
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
358
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
359
+ const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
360
+ expect(resultJson.version).toBe('2.0.0');
361
+ expect(resultJson.platform).toBe('android');
362
+ expect(resultJson.status).toBe('passed');
363
+ expect(resultJson.summary.passed).toBe(1);
364
+ expect(resultJson.summary.failed).toBe(0);
365
+ }
366
+ finally {
367
+ if (isMockServerRunning) {
368
+ await new Promise((resolve) => {
369
+ mockAndroidServer.close(() => resolve());
370
+ });
371
+ }
372
+ }
373
+ }, 25000);
374
+ // E2E-10: Spectra.intercept FIFO respondOnce sequential polling and abort simulation
375
+ it('E2E-10: Spectra.intercept FIFO respondOnce sequential polling and abort simulation in real Chromium', async () => {
376
+ const fifoAbortDir = path.join(tmpDir, 'specs', 'ApiInterception', 'TC-0010-fifo-abort');
377
+ fs.mkdirSync(fifoAbortDir, { recursive: true });
378
+ fs.writeFileSync(path.join(fifoAbortDir, 'web.test.ts'), `it("should handle FIFO respondOnce queue and abort simulation", async () => {
379
+ await MatchersPage.open("api-fetch");
380
+
381
+ // Setup intercept with default fallback and FIFO queue
382
+ const mock = await Spectra.intercept("api/posts", "GET", [
383
+ { id: 999, title: "Default Fallback Post" }
384
+ ]);
385
+
386
+ await mock.respondOnce([
387
+ { id: 101, title: "FIFO Queued Post #1" }
388
+ ]);
389
+ await mock.respondOnce([
390
+ { id: 102, title: "FIFO Queued Post #2" }
391
+ ]);
392
+
393
+ // 1. First fetch triggers FIFO item #1
394
+ await MatchersPage.fetchApiButton.click();
395
+ await MatchersPage.fetchedItems.first().shouldContainText("FIFO Queued Post #1");
396
+
397
+ // 2. Second fetch triggers FIFO item #2
398
+ await MatchersPage.fetchApiButton.click();
399
+ await MatchersPage.fetchedItems.first().shouldContainText("FIFO Queued Post #2");
400
+
401
+ // 3. Third fetch falls back to default response
402
+ await MatchersPage.fetchApiButton.click();
403
+ await MatchersPage.fetchedItems.first().shouldContainText("Default Fallback Post");
404
+
405
+ // 4. Test abort simulation
406
+ await mock.abort("Failed");
407
+ await MatchersPage.fetchApiButton.click();
408
+ // On network failure, demo app shows fallback 200 OK from catch block
409
+ await MatchersPage.fetchStatusBadge.shouldHaveText("200 OK");
410
+ });
411
+ `, 'utf-8');
412
+ await runCommand(['specs/ApiInterception/TC-0010-fifo-abort/web.test.ts'], { target: 'web' });
413
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
414
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
415
+ const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
416
+ expect(resultJson.version).toBe('2.0.0');
417
+ expect(resultJson.platform).toBe('web');
418
+ expect(resultJson.status).toBe('passed');
419
+ expect(resultJson.summary.passed).toBe(1);
420
+ expect(resultJson.summary.failed).toBe(0);
421
+ }, 25000);
181
422
  });
@@ -6,19 +6,17 @@ import { TypeGenerator } from '../../types/generator.js';
6
6
  describe('runCommand Integration & Dynamic Worker Sizing', () => {
7
7
  it('passes calculated dynamic worker count to RustCoreBridge payload', async () => {
8
8
  vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
9
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
9
10
  vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
10
11
  webConfig: {
11
12
  baseUrl: 'http://localhost:3000',
12
13
  maxConcurrentSessions: '1',
13
14
  headless: true,
14
15
  implicitWait: '5000',
15
- pageLoadTimeout: '30000',
16
- scriptTimeout: '30000',
17
16
  parallelizationMode: 'suite',
18
17
  },
19
18
  androidConfig: {},
20
19
  iosConfig: {},
21
- browsers: [],
22
20
  executionConfig: {},
23
21
  });
24
22
  let bridgeOptionsReceived = null;
@@ -32,26 +30,24 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
32
30
  };
33
31
  });
34
32
  // Run single spec
35
- await runCommand('specs/Auth/TC-0001/web.test.ts');
33
+ await runCommand(['specs/Auth/TC-0001/web.test.ts']);
36
34
  expect(bridgeOptionsReceived).not.toBeNull();
37
35
  // 1 test case -> worker count must be clamped to 1
38
36
  expect(bridgeOptionsReceived.config.webConfig.maxConcurrentSessions).toBe('1');
39
37
  });
40
38
  it('respects user-specified --concurrency option and passes it to RustCoreBridge', async () => {
41
39
  vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
40
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
42
41
  vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
43
42
  webConfig: {
44
43
  baseUrl: 'http://localhost:3000',
45
44
  maxConcurrentSessions: '1',
46
45
  headless: true,
47
46
  implicitWait: '5000',
48
- pageLoadTimeout: '30000',
49
- scriptTimeout: '30000',
50
47
  parallelizationMode: 'suite',
51
48
  },
52
49
  androidConfig: {},
53
50
  iosConfig: {},
54
- browsers: [],
55
51
  executionConfig: {},
56
52
  });
57
53
  let bridgeOptionsReceived = null;
@@ -64,13 +60,14 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
64
60
  logs: [],
65
61
  };
66
62
  });
67
- await runCommand('specs/Auth/TC-0001/web.test.ts', { concurrency: 4 });
63
+ await runCommand(['specs/Auth/TC-0001/web.test.ts'], { concurrency: 4 });
68
64
  expect(bridgeOptionsReceived).not.toBeNull();
69
65
  // 1 item clamped to 1
70
66
  expect(bridgeOptionsReceived.config.webConfig.maxConcurrentSessions).toBe('1');
71
67
  });
72
68
  it('correctly serializes target android and devices options into RustCoreBridge payload', async () => {
73
69
  vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
70
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
74
71
  vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
75
72
  webConfig: {
76
73
  baseUrl: 'http://localhost:3000',
@@ -81,7 +78,6 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
81
78
  devices: [{ id: 'emulator-5554' }],
82
79
  },
83
80
  iosConfig: {},
84
- browsers: [],
85
81
  executionConfig: {},
86
82
  });
87
83
  let bridgeOptionsReceived = null;
@@ -94,7 +90,7 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
94
90
  logs: [],
95
91
  };
96
92
  });
97
- await runCommand('specs/Auth/TC-0001/android.test.ts', {
93
+ await runCommand(['specs/Auth/TC-0001/android.test.ts'], {
98
94
  target: 'android',
99
95
  devices: 'emulator-5554,emulator-5556',
100
96
  });
@@ -109,11 +105,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
109
105
  });
110
106
  it('correctly serializes single --device option into devices array of objects', async () => {
111
107
  vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
108
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
112
109
  vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
113
110
  webConfig: { baseUrl: 'http://localhost:3000' },
114
111
  androidConfig: { driverServer: 'http://127.0.0.1:4800' },
115
112
  iosConfig: {},
116
- browsers: [],
117
113
  executionConfig: {},
118
114
  });
119
115
  let bridgeOptionsReceived = null;
@@ -126,7 +122,7 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
126
122
  logs: [],
127
123
  };
128
124
  });
129
- await runCommand('specs/Auth/TC-0001/android.test.ts', {
125
+ await runCommand(['specs/Auth/TC-0001/android.test.ts'], {
130
126
  target: 'android',
131
127
  device: 'emulator-5554',
132
128
  });
@@ -138,11 +134,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
138
134
  });
139
135
  it('correctly serializes --all-devices flag into auto devices in config', async () => {
140
136
  vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
137
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
141
138
  vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
142
139
  webConfig: { baseUrl: 'http://localhost:3000' },
143
140
  androidConfig: { driverServer: 'http://127.0.0.1:4800' },
144
141
  iosConfig: {},
145
- browsers: [],
146
142
  executionConfig: {},
147
143
  });
148
144
  let bridgeOptionsReceived = null;
@@ -155,7 +151,7 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
155
151
  logs: [],
156
152
  };
157
153
  });
158
- await runCommand('specs/Auth/TC-0001/android.test.ts', {
154
+ await runCommand(['specs/Auth/TC-0001/android.test.ts'], {
159
155
  target: 'android',
160
156
  allDevices: true,
161
157
  });
@@ -167,11 +163,11 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
167
163
  });
168
164
  it('correctly serializes --multi-device flag into auto devices in config', async () => {
169
165
  vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
166
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
170
167
  vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
171
168
  webConfig: { baseUrl: 'http://localhost:3000' },
172
169
  androidConfig: { driverServer: 'http://127.0.0.1:4800' },
173
170
  iosConfig: {},
174
- browsers: [],
175
171
  executionConfig: {},
176
172
  });
177
173
  let bridgeOptionsReceived = null;
@@ -184,7 +180,7 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
184
180
  logs: [],
185
181
  };
186
182
  });
187
- await runCommand('specs/Auth/TC-0001/android.test.ts', {
183
+ await runCommand(['specs/Auth/TC-0001/android.test.ts'], {
188
184
  target: 'android',
189
185
  multiDevice: true,
190
186
  });
@@ -194,4 +190,84 @@ describe('runCommand Integration & Dynamic Worker Sizing', () => {
194
190
  expect(bridgeOptionsReceived.config.androidConfig.devices).toBe('auto');
195
191
  expect(bridgeOptionsReceived.targetDevice).toBe('auto');
196
192
  });
193
+ it('sets process.exitCode to 1 when tests fail', async () => {
194
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
195
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
196
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
197
+ webConfig: { baseUrl: 'http://localhost:3000' },
198
+ androidConfig: {},
199
+ iosConfig: {},
200
+ executionConfig: {},
201
+ });
202
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async () => {
203
+ return {
204
+ status: 'failed',
205
+ duration: '1.5s',
206
+ passedCount: 1,
207
+ failedCount: 1,
208
+ results: [],
209
+ logs: [],
210
+ };
211
+ });
212
+ process.exitCode = undefined;
213
+ await runCommand(['specs/ApiInterception'], { target: 'web' });
214
+ expect(process.exitCode).toBe(1);
215
+ process.exitCode = undefined; // reset
216
+ });
217
+ it('incorporates TESTSPECTRA_CARRIED_OVER into summary card calculation', async () => {
218
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
219
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
220
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
221
+ webConfig: { baseUrl: 'http://localhost:3000' },
222
+ androidConfig: {},
223
+ iosConfig: {},
224
+ executionConfig: {},
225
+ });
226
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async () => {
227
+ return {
228
+ status: 'passed',
229
+ duration: '2.0s',
230
+ passedCount: 1,
231
+ failedCount: 0,
232
+ results: [],
233
+ logs: [],
234
+ };
235
+ });
236
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
237
+ process.env.TESTSPECTRA_CARRIED_OVER = JSON.stringify({
238
+ passed: 3,
239
+ durationMs: 4000,
240
+ });
241
+ await runCommand(['specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts'], { target: 'web' });
242
+ delete process.env.TESTSPECTRA_CARRIED_OVER;
243
+ const logged = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
244
+ expect(logged).toContain('4 passed');
245
+ expect(logged).toContain('3 carried over');
246
+ expect(logged).toContain('4 total');
247
+ expect(logged).toContain('6.0s');
248
+ consoleSpy.mockRestore();
249
+ });
250
+ it('correctly passes multiple specs to RustCoreBridge payload', async () => {
251
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
252
+ vi.spyOn(TypeGenerator, 'syncEnvDeclaration').mockResolvedValue(undefined);
253
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
254
+ webConfig: { baseUrl: 'http://localhost:3000' },
255
+ androidConfig: {},
256
+ iosConfig: {},
257
+ executionConfig: {},
258
+ });
259
+ let bridgeOptionsReceived = null;
260
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
261
+ bridgeOptionsReceived = options;
262
+ return {
263
+ status: 'passed',
264
+ duration: '1.0s',
265
+ results: [],
266
+ logs: [],
267
+ };
268
+ });
269
+ await runCommand(['specs/Auth/TC-0001/web.test.ts', 'specs/Auth/TC-0002/web.test.ts']);
270
+ expect(bridgeOptionsReceived).not.toBeNull();
271
+ expect(bridgeOptionsReceived.specs).toEqual(['specs/Auth/TC-0001/web.test.ts', 'specs/Auth/TC-0002/web.test.ts']);
272
+ });
197
273
  });
@@ -1,24 +1,34 @@
1
1
  import chalk from 'chalk';
2
2
  import * as p from '@clack/prompts';
3
3
  import { DemoServer } from '../utils/demo-server.js';
4
+ import { DemoApiServer } from '../utils/api-server.js';
4
5
  export async function demoCommand(options) {
5
6
  const port = options.port ? parseInt(options.port, 10) : 5173;
6
7
  p.intro(chalk.bold.cyan('🌐 TestSpectra Demo Web App'));
7
8
  const server = new DemoServer(port);
9
+ const apiServer = new DemoApiServer();
8
10
  try {
9
11
  const { url } = await server.start();
10
12
  p.log.success(chalk.green(`Demo web app is live at: ${chalk.bold.underline(url)}`));
13
+ // The web app fetches its API sections from the separate self-hosted API server, so start it
14
+ // too — otherwise the API demo sections show only their offline fallback data.
15
+ try {
16
+ const { url: apiUrl } = await apiServer.start();
17
+ p.log.success(chalk.green(`Demo API server is live at: ${chalk.bold.underline(apiUrl)}`));
18
+ }
19
+ catch (err) {
20
+ p.log.warn(chalk.yellow(`Could not start demo API server: ${err.message}`));
21
+ }
11
22
  p.log.info(chalk.dim("You can now run tests against this server in another terminal (e.g. 'spectra run -t web')."));
12
- p.log.info(chalk.dim('Press Ctrl+C to stop the server.'));
23
+ p.log.info(chalk.dim('Press Ctrl+C to stop the servers.'));
13
24
  // Handle termination gracefully
14
- process.on('SIGINT', async () => {
25
+ const shutdown = async () => {
15
26
  await server.stop();
27
+ await apiServer.stop();
16
28
  process.exit(0);
17
- });
18
- process.on('SIGTERM', async () => {
19
- await server.stop();
20
- process.exit(0);
21
- });
29
+ };
30
+ process.on('SIGINT', shutdown);
31
+ process.on('SIGTERM', shutdown);
22
32
  await new Promise(() => { });
23
33
  }
24
34
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  export interface InitCommandOptions {
2
2
  force?: boolean;
3
- template?: 'default' | 'nx';
3
+ template?: 'default' | 'nx' | 'react-component';
4
4
  }
5
5
  export declare function initCommand(options?: InitCommandOptions): Promise<void>;
@@ -215,10 +215,15 @@ export async function initCommand(options = {}) {
215
215
  hint: 'Interactive workspace feature discovery & centralized config',
216
216
  },
217
217
  {
218
- label: 'Standard Project',
218
+ label: 'Standard E2E Project',
219
219
  value: 'default',
220
220
  hint: 'Standalone single-suite E2E testing project',
221
221
  },
222
+ {
223
+ label: 'React Component Testing',
224
+ value: 'react-component',
225
+ hint: 'Real-browser React component testing with Vite & Testing Library',
226
+ },
222
227
  ],
223
228
  });
224
229
  if (p.isCancel(templateChoice)) {