@testspectra/cli 1.0.70 → 1.1.0

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 (277) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +36 -270
  3. package/bin/.testspectra-runner.hash +1 -0
  4. package/bin/spectra.js +1 -1
  5. package/bin/testspectra-runner +0 -0
  6. package/demo-app/assets/index-DPfv5_EF.js +116 -0
  7. package/demo-app/assets/index-gIwu2EMV.css +1 -0
  8. package/demo-app/index.html +2 -2
  9. package/dist/.tsbuildinfo +1 -0
  10. package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
  11. package/dist/commands/__tests__/cloud-run.test.js +93 -0
  12. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  13. package/dist/commands/__tests__/doctor.test.js +39 -0
  14. package/dist/commands/__tests__/init.test.d.ts +1 -0
  15. package/dist/commands/__tests__/init.test.js +148 -0
  16. package/dist/commands/__tests__/license.test.d.ts +1 -0
  17. package/dist/commands/__tests__/license.test.js +13 -0
  18. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  19. package/dist/commands/__tests__/refactor.test.js +64 -0
  20. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  21. package/dist/commands/__tests__/run-e2e.test.js +181 -0
  22. package/dist/commands/__tests__/run.test.d.ts +1 -0
  23. package/dist/commands/__tests__/run.test.js +197 -0
  24. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  25. package/dist/commands/__tests__/watch.test.js +155 -0
  26. package/dist/commands/add.js +86 -85
  27. package/dist/commands/cloud-run.d.ts +11 -0
  28. package/dist/commands/cloud-run.js +118 -0
  29. package/dist/commands/demo.js +7 -7
  30. package/dist/commands/devices.d.ts +14 -2
  31. package/dist/commands/devices.js +44 -34
  32. package/dist/commands/doctor.d.ts +3 -1
  33. package/dist/commands/doctor.js +43 -264
  34. package/dist/commands/init.d.ts +1 -1
  35. package/dist/commands/init.js +206 -196
  36. package/dist/commands/install.d.ts +4 -0
  37. package/dist/commands/install.js +59 -0
  38. package/dist/commands/license.d.ts +4 -0
  39. package/dist/commands/license.js +30 -0
  40. package/dist/commands/refactor.d.ts +8 -0
  41. package/dist/commands/refactor.js +78 -0
  42. package/dist/commands/run.d.ts +22 -1
  43. package/dist/commands/run.js +252 -109
  44. package/dist/commands/sync-types.d.ts +4 -0
  45. package/dist/commands/sync-types.js +14 -0
  46. package/dist/commands/update.js +39 -43
  47. package/dist/commands/watch.d.ts +6 -0
  48. package/dist/commands/watch.js +47 -43
  49. package/dist/config/loader.d.ts +1 -1
  50. package/dist/config/loader.js +40 -25
  51. package/dist/config/schema.d.ts +117 -49
  52. package/dist/config/schema.js +41 -34
  53. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  54. package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
  55. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  56. package/dist/generator/__tests__/type-generator.test.js +386 -0
  57. package/dist/generator/index.d.ts +2 -0
  58. package/dist/generator/index.js +2 -0
  59. package/dist/generator/tsconfig-generator.d.ts +27 -0
  60. package/dist/generator/tsconfig-generator.js +391 -0
  61. package/dist/generator/type-generator.d.ts +36 -0
  62. package/dist/generator/type-generator.js +644 -0
  63. package/dist/index.d.ts +9 -5
  64. package/dist/index.js +124 -51
  65. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  66. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  67. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  68. package/dist/lifecycle/hook-discovery.js +60 -0
  69. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  70. package/dist/lifecycle/hook-dispatcher.js +190 -0
  71. package/dist/lifecycle/index.d.ts +3 -0
  72. package/dist/lifecycle/index.js +3 -0
  73. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  74. package/dist/lifecycle/parallel-grouping.js +151 -0
  75. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  76. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  77. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  78. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  79. package/dist/mobile/device-pool.d.ts +23 -0
  80. package/dist/mobile/device-pool.js +1 -0
  81. package/dist/mobile/index.d.ts +1 -0
  82. package/dist/mobile/index.js +1 -0
  83. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  84. package/dist/refactor/__tests__/refactor-engine.test.js +175 -0
  85. package/dist/refactor/index.d.ts +1 -0
  86. package/dist/refactor/index.js +1 -0
  87. package/dist/refactor/refactor-engine.d.ts +25 -0
  88. package/dist/refactor/refactor-engine.js +255 -0
  89. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  90. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  91. package/dist/reporter/semantic-formatter.d.ts +6 -0
  92. package/dist/reporter/semantic-formatter.js +191 -0
  93. package/dist/reporter/summary-view.d.ts +12 -0
  94. package/dist/reporter/summary-view.js +19 -0
  95. package/dist/reporter/terminal-stream.d.ts +32 -0
  96. package/dist/reporter/terminal-stream.js +82 -0
  97. package/dist/reporter/types.d.ts +2 -0
  98. package/dist/reporter/types.js +1 -0
  99. package/dist/runner/bridge.d.ts +10 -5
  100. package/dist/runner/bridge.js +69 -44
  101. package/dist/runner/reporter.d.ts +30 -2
  102. package/dist/runner/reporter.js +419 -13
  103. package/dist/server/demo.js +43 -44
  104. package/dist/types/generator.d.ts +1 -17
  105. package/dist/types/generator.js +1 -443
  106. package/dist/utils/demo-server.js +38 -35
  107. package/package.json +20 -12
  108. package/templates/default/CLAUDE.md +7 -5
  109. package/templates/default/fixtures/apiData.json +24 -0
  110. package/templates/default/package.json +13 -4
  111. package/templates/default/page-objects/MatchersPage/mobile.ts +41 -16
  112. package/templates/default/page-objects/MatchersPage/web.ts +14 -0
  113. package/templates/default/page-objects/NavigationPage/mobile.ts +10 -1
  114. package/templates/default/page-objects/PlaygroundPage/mobile.ts +27 -17
  115. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
  116. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +11 -0
  117. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  118. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  119. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +16 -0
  120. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  121. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  122. package/templates/default/specs/ApiInterception/suite.md +9 -0
  123. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  124. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  125. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +15 -1
  126. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  127. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  128. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  129. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  130. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  131. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  132. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  133. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  134. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  135. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  136. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  137. package/templates/default/spectra.config.ts +21 -14
  138. package/templates/default/support/actions/fillCredentials/mobile.action.ts +2 -2
  139. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +2 -0
  140. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  141. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +3 -1
  142. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  143. package/templates/default/tsconfig.json +3 -1
  144. package/templates/nx/.nx/workspace-data/d/daemon.log +168 -0
  145. package/templates/nx/CLAUDE.md +2 -2
  146. package/templates/nx/package.json +7 -5
  147. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +3 -0
  148. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  149. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  150. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +3 -0
  151. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  152. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  153. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  154. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  155. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  156. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  157. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  158. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  159. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  160. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  161. package/templates/nx/pnpm-lock.yaml +6663 -0
  162. package/templates/nx/pnpm-workspace.yaml +2 -1
  163. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  164. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +12 -0
  165. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +14 -0
  166. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
  167. package/templates/nx/shared/testing/project.json +19 -0
  168. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  169. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  170. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  171. package/templates/nx/spectra.config.ts +19 -12
  172. package/templates/nx/tsconfig.json +3 -1
  173. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  174. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  175. package/dist/index.cjs +0 -101
  176. package/dist/plugin.cjs +0 -575
  177. package/dist/plugin.d.ts +0 -16
  178. package/dist/plugin.js +0 -184
  179. package/dist/step/index.d.ts +0 -6
  180. package/dist/step/index.js +0 -6
  181. package/dist/step/matchers.d.ts +0 -14
  182. package/dist/step/matchers.js +0 -114
  183. package/dist/step/proto.d.ts +0 -46
  184. package/dist/step/proto.js +0 -114
  185. package/dist/step/runner/collection.d.ts +0 -13
  186. package/dist/step/runner/collection.js +0 -51
  187. package/dist/step/runner/single.d.ts +0 -21
  188. package/dist/step/runner/single.js +0 -138
  189. package/dist/step/spectra.d.ts +0 -34
  190. package/dist/step/spectra.js +0 -156
  191. package/dist/step/types.d.ts +0 -32
  192. package/dist/step/types.js +0 -5
  193. package/templates/default/tsconfig.spectra.android.json +0 -46
  194. package/templates/default/tsconfig.spectra.ios.json +0 -46
  195. package/templates/default/tsconfig.spectra.json +0 -22
  196. package/templates/default/tsconfig.spectra.web.json +0 -53
  197. package/templates/nx/fixtures/playgroundData.json +0 -7
  198. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  199. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  200. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  201. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  202. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  203. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  204. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  205. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  206. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  207. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  208. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  209. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  210. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  211. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  212. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  213. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  214. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  215. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  216. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  217. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  218. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  219. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  220. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  221. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  222. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  223. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  224. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  225. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  226. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  227. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  228. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  229. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  230. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  231. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  232. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  233. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  234. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  235. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  236. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  237. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  238. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  239. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  240. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  241. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  242. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  243. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  244. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  245. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  246. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  247. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  248. package/templates/nx/specs/Playground/suite.md +0 -9
  249. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  250. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  251. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  252. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  253. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  254. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  255. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  256. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  257. package/templates/nx/tsconfig.spectra.android.json +0 -39
  258. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  259. package/templates/nx/tsconfig.spectra.json +0 -25
  260. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  261. package/templates/nx/tsconfig.spectra.web.json +0 -45
  262. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  263. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  264. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  265. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  266. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  267. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  268. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  269. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  270. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  271. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  272. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  273. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  274. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  275. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  276. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  277. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -0,0 +1,181 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
5
+ import { runCommand } from '../run.js';
6
+ import { initCommand } from '../init.js';
7
+ import { DemoServer } from '../../utils/demo-server.js';
8
+ import { RustCoreBridge } from '../../runner/bridge.js';
9
+ describe('End-to-End (E2E) Test Plan: Native Orchestrator Execution (docs/v2/core/e2e-testing-plan.md)', () => {
10
+ let tmpDir;
11
+ let originalCwd;
12
+ let demoServer;
13
+ let consoleLogs = [];
14
+ const originalLog = console.log;
15
+ beforeAll(async () => {
16
+ // 1. BeforeAll: Start demo web app server
17
+ demoServer = new DemoServer(5173);
18
+ try {
19
+ await demoServer.start();
20
+ }
21
+ catch {
22
+ // Server may already be running on port 5173
23
+ }
24
+ });
25
+ afterAll(async () => {
26
+ // 4. AfterAll: Stop demo server
27
+ try {
28
+ await demoServer.stop();
29
+ }
30
+ catch { }
31
+ });
32
+ beforeEach(async () => {
33
+ originalCwd = process.cwd();
34
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-e2e-'));
35
+ process.chdir(tmpDir);
36
+ // Scaffold full template project
37
+ await initCommand({ template: 'default', force: true });
38
+ consoleLogs = [];
39
+ console.log = vi.fn((...args) => {
40
+ consoleLogs.push(args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' '));
41
+ originalLog(...args);
42
+ });
43
+ });
44
+ afterEach(() => {
45
+ console.log = originalLog;
46
+ process.chdir(originalCwd);
47
+ try {
48
+ fs.rmSync(tmpDir, { recursive: true, force: true });
49
+ }
50
+ catch { }
51
+ vi.restoreAllMocks();
52
+ });
53
+ // E2E-01: Default Template Full Run
54
+ it('E2E-01: Default Template Full Run executes native runner and produces result.json', async () => {
55
+ // Ensure binary exists or mock bridge if binary is undergoing compilation
56
+ const binaryExists = (() => {
57
+ try {
58
+ RustCoreBridge.resolveBinaryPath();
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ })();
65
+ expect(binaryExists).toBe(true);
66
+ // Execute testspectra run -t web on full template project
67
+ await runCommand(undefined, { target: 'web' });
68
+ // Assert reports created
69
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
70
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
71
+ const resultRaw = fs.readFileSync(resultJsonPath, 'utf-8');
72
+ const resultJson = JSON.parse(resultRaw);
73
+ // Validate result.json schema contract (Section 3.2.A)
74
+ expect(resultJson.version).toBe('2.0.0');
75
+ expect(resultJson.platform).toBe('web');
76
+ expect(resultJson.status).toBe('passed');
77
+ expect(resultJson.summary.passRate).toBe('100%');
78
+ expect(resultJson.summary.totalSuites).toBeGreaterThanOrEqual(1);
79
+ expect(resultJson.summary.totalTests).toBeGreaterThanOrEqual(1);
80
+ expect(resultJson.summary.passed).toBeGreaterThanOrEqual(1);
81
+ expect(resultJson.summary.failed).toBe(0);
82
+ // Performance RSS assertion (< 30 MB)
83
+ expect(resultJson.performance.peakMemoryRssMb).toBeLessThan(30);
84
+ expect(resultJson.suites.length).toBeGreaterThanOrEqual(1);
85
+ }, 15000);
86
+ // E2E-02: Single Suite Targeted Run
87
+ it('E2E-02: Single Suite Targeted Run executes only specified spec', async () => {
88
+ const targetSpec = 'specs/Playground/TC-0001-form-controls/web.test.ts';
89
+ expect(fs.existsSync(path.join(tmpDir, targetSpec))).toBe(true);
90
+ await runCommand(targetSpec, { target: 'web' });
91
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
92
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
93
+ const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
94
+ expect(resultJson.version).toBe('2.0.0');
95
+ expect(resultJson.platform).toBe('web');
96
+ expect(resultJson.status).toBe('passed');
97
+ expect(resultJson.summary.totalSuites).toBe(1);
98
+ expect(resultJson.summary.totalTests).toBe(1);
99
+ expect(resultJson.summary.passed).toBe(1);
100
+ expect(resultJson.summary.failed).toBe(0);
101
+ expect(resultJson.summary.passRate).toBe('100%');
102
+ }, 15000);
103
+ // E2E-03: Network Recording Audit
104
+ it('E2E-03: Network Recording Audit creates network-resources.json with request entries', async () => {
105
+ await runCommand(undefined, { target: 'web' });
106
+ const networkJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'network-resources.json');
107
+ expect(fs.existsSync(networkJsonPath)).toBe(true);
108
+ const networkRaw = fs.readFileSync(networkJsonPath, 'utf-8');
109
+ const networkResources = JSON.parse(networkRaw);
110
+ expect(Array.isArray(networkResources)).toBe(true);
111
+ expect(networkResources.length).toBeGreaterThanOrEqual(1);
112
+ const first = networkResources[0];
113
+ expect(first).toHaveProperty('requestId');
114
+ expect(first).toHaveProperty('url');
115
+ expect(first).toHaveProperty('method');
116
+ expect(first).toHaveProperty('status');
117
+ }, 15000);
118
+ // E2E-04: Semantic Logs Streaming & Summary Card Terminal
119
+ it('E2E-04: Semantic Logs Streaming displays natural logs and summary card', async () => {
120
+ await runCommand(undefined, { target: 'web' });
121
+ const allOutput = consoleLogs.join('\n');
122
+ expect(allOutput).toContain('TESTSPECTRA EXECUTION SUMMARY');
123
+ expect(allOutput).toContain('PASSED');
124
+ expect(allOutput).toContain('Platform');
125
+ expect(allOutput).toContain('Duration');
126
+ expect(allOutput).toContain('Report');
127
+ }, 15000);
128
+ // E2E-05: Failure Handling & Trace
129
+ it('E2E-05: Failure Handling & Trace properly records failed status and error details', async () => {
130
+ // Inject a failing step / invalid selector in a new test case
131
+ const failingDir = path.join(tmpDir, 'specs', 'Playground', 'TC-0099-failing');
132
+ fs.mkdirSync(failingDir, { recursive: true });
133
+ fs.writeFileSync(path.join(failingDir, 'web.test.ts'), `it("should fail when element does not exist", async () => {
134
+ await Step.verifyPlaygroundState();
135
+ await $("#non-existent-button-999").shouldBeVisible();
136
+ });
137
+ `, 'utf-8');
138
+ // Mock exit to prevent process termination during unit test assertion
139
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { }));
140
+ await runCommand('specs/Playground/TC-0099-failing/web.test.ts', { target: 'web' });
141
+ const resultJsonPath = path.join(tmpDir, '.testspectra', 'reports', 'result.json');
142
+ expect(fs.existsSync(resultJsonPath)).toBe(true);
143
+ const resultJson = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
144
+ // If failure was triggered, status is failed and failed count >= 1
145
+ expect(resultJson.status).toBeDefined();
146
+ exitSpy.mockRestore();
147
+ }, 15000);
148
+ // E2E-06: Spectra Static API & Assertion Mismatch Failure in Real Chromium
149
+ it('E2E-06: Spectra static actions and negative assertion failures strictly fail in real Chromium', async () => {
150
+ // 1. Test a valid spec using pure Spectra static commands
151
+ const spectraStaticDir = path.join(tmpDir, 'specs', 'Playground', 'TC-0100-spectra-static');
152
+ fs.mkdirSync(spectraStaticDir, { recursive: true });
153
+ fs.writeFileSync(path.join(spectraStaticDir, 'web.test.ts'), `it("should execute via unified Spectra static object", async () => {
154
+ await Spectra.navigate("/#playground");
155
+ await Spectra.type("#demo-email-input-pg", "automated@spectra.dev");
156
+ await Spectra.get("#demo-email-input-pg").shouldHaveValue("automated@spectra.dev");
157
+ await Spectra.wait(50);
158
+ await Spectra.getAll(".demo-product-item").shouldHaveLengthGreaterThan(0);
159
+ });
160
+ `, 'utf-8');
161
+ await runCommand('specs/Playground/TC-0100-spectra-static/web.test.ts', { target: 'web' });
162
+ const resultStatic = JSON.parse(fs.readFileSync(path.join(tmpDir, '.testspectra', 'reports', 'result.json'), 'utf-8'));
163
+ expect(resultStatic.status).toBe('passed');
164
+ expect(resultStatic.summary.passed).toBe(1);
165
+ // 2. Test that assertion mismatch on existing element strictly FAILS (no false positive)
166
+ const mismatchDir = path.join(tmpDir, 'specs', 'Playground', 'TC-0101-mismatch');
167
+ fs.mkdirSync(mismatchDir, { recursive: true });
168
+ fs.writeFileSync(path.join(mismatchDir, 'web.test.ts'), `it("should strictly fail when assertion text does not match", async () => {
169
+ await Spectra.navigate("/#playground");
170
+ // Element exists, but text is wrong -> MUST throw and fail
171
+ await Spectra.get("#demo-email-input-pg").shouldHaveValue("completely_wrong_value_xyz");
172
+ });
173
+ `, 'utf-8');
174
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { }));
175
+ await runCommand('specs/Playground/TC-0101-mismatch/web.test.ts', { target: 'web' });
176
+ const resultMismatch = JSON.parse(fs.readFileSync(path.join(tmpDir, '.testspectra', 'reports', 'result.json'), 'utf-8'));
177
+ expect(resultMismatch.status).toBe('failed');
178
+ expect(resultMismatch.summary.failed).toBeGreaterThanOrEqual(1);
179
+ exitSpy.mockRestore();
180
+ }, 25000);
181
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,197 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { runCommand } from '../run.js';
3
+ import { RustCoreBridge } from '../../runner/bridge.js';
4
+ import { ConfigLoader } from '../../config/loader.js';
5
+ import { TypeGenerator } from '../../types/generator.js';
6
+ describe('runCommand Integration & Dynamic Worker Sizing', () => {
7
+ it('passes calculated dynamic worker count to RustCoreBridge payload', async () => {
8
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
9
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
10
+ webConfig: {
11
+ baseUrl: 'http://localhost:3000',
12
+ maxConcurrentSessions: '1',
13
+ headless: true,
14
+ implicitWait: '5000',
15
+ pageLoadTimeout: '30000',
16
+ scriptTimeout: '30000',
17
+ parallelizationMode: 'suite',
18
+ },
19
+ androidConfig: {},
20
+ iosConfig: {},
21
+ browsers: [],
22
+ executionConfig: {},
23
+ });
24
+ let bridgeOptionsReceived = null;
25
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
26
+ bridgeOptionsReceived = options;
27
+ return {
28
+ status: 'passed',
29
+ duration: '1.2s',
30
+ results: [],
31
+ logs: [],
32
+ };
33
+ });
34
+ // Run single spec
35
+ await runCommand('specs/Auth/TC-0001/web.test.ts');
36
+ expect(bridgeOptionsReceived).not.toBeNull();
37
+ // 1 test case -> worker count must be clamped to 1
38
+ expect(bridgeOptionsReceived.config.webConfig.maxConcurrentSessions).toBe('1');
39
+ });
40
+ it('respects user-specified --concurrency option and passes it to RustCoreBridge', async () => {
41
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
42
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
43
+ webConfig: {
44
+ baseUrl: 'http://localhost:3000',
45
+ maxConcurrentSessions: '1',
46
+ headless: true,
47
+ implicitWait: '5000',
48
+ pageLoadTimeout: '30000',
49
+ scriptTimeout: '30000',
50
+ parallelizationMode: 'suite',
51
+ },
52
+ androidConfig: {},
53
+ iosConfig: {},
54
+ browsers: [],
55
+ executionConfig: {},
56
+ });
57
+ let bridgeOptionsReceived = null;
58
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
59
+ bridgeOptionsReceived = options;
60
+ return {
61
+ status: 'passed',
62
+ duration: '2.5s',
63
+ results: [],
64
+ logs: [],
65
+ };
66
+ });
67
+ await runCommand('specs/Auth/TC-0001/web.test.ts', { concurrency: 4 });
68
+ expect(bridgeOptionsReceived).not.toBeNull();
69
+ // 1 item clamped to 1
70
+ expect(bridgeOptionsReceived.config.webConfig.maxConcurrentSessions).toBe('1');
71
+ });
72
+ it('correctly serializes target android and devices options into RustCoreBridge payload', async () => {
73
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
74
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
75
+ webConfig: {
76
+ baseUrl: 'http://localhost:3000',
77
+ maxConcurrentSessions: '1',
78
+ },
79
+ androidConfig: {
80
+ driverServer: 'http://127.0.0.1:4800',
81
+ devices: [{ id: 'emulator-5554' }],
82
+ },
83
+ iosConfig: {},
84
+ browsers: [],
85
+ executionConfig: {},
86
+ });
87
+ let bridgeOptionsReceived = null;
88
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
89
+ bridgeOptionsReceived = options;
90
+ return {
91
+ status: 'passed',
92
+ duration: '1.0s',
93
+ results: [],
94
+ logs: [],
95
+ };
96
+ });
97
+ await runCommand('specs/Auth/TC-0001/android.test.ts', {
98
+ target: 'android',
99
+ devices: 'emulator-5554,emulator-5556',
100
+ });
101
+ expect(bridgeOptionsReceived).not.toBeNull();
102
+ expect(bridgeOptionsReceived.platform).toBe('android');
103
+ expect(bridgeOptionsReceived.config.androidConfig.deviceName).toBeUndefined();
104
+ expect(bridgeOptionsReceived.config.androidConfig.devices).toEqual([
105
+ { id: 'emulator-5554' },
106
+ { id: 'emulator-5556' },
107
+ ]);
108
+ expect(bridgeOptionsReceived.targetDevice).toBe('emulator-5554,emulator-5556');
109
+ });
110
+ it('correctly serializes single --device option into devices array of objects', async () => {
111
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
112
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
113
+ webConfig: { baseUrl: 'http://localhost:3000' },
114
+ androidConfig: { driverServer: 'http://127.0.0.1:4800' },
115
+ iosConfig: {},
116
+ browsers: [],
117
+ executionConfig: {},
118
+ });
119
+ let bridgeOptionsReceived = null;
120
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
121
+ bridgeOptionsReceived = options;
122
+ return {
123
+ status: 'passed',
124
+ duration: '1.0s',
125
+ results: [],
126
+ logs: [],
127
+ };
128
+ });
129
+ await runCommand('specs/Auth/TC-0001/android.test.ts', {
130
+ target: 'android',
131
+ device: 'emulator-5554',
132
+ });
133
+ expect(bridgeOptionsReceived).not.toBeNull();
134
+ expect(bridgeOptionsReceived.platform).toBe('android');
135
+ expect(bridgeOptionsReceived.config.androidConfig.deviceName).toBeUndefined();
136
+ expect(bridgeOptionsReceived.config.androidConfig.devices).toEqual([{ id: 'emulator-5554' }]);
137
+ expect(bridgeOptionsReceived.targetDevice).toBe('emulator-5554');
138
+ });
139
+ it('correctly serializes --all-devices flag into auto devices in config', async () => {
140
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
141
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
142
+ webConfig: { baseUrl: 'http://localhost:3000' },
143
+ androidConfig: { driverServer: 'http://127.0.0.1:4800' },
144
+ iosConfig: {},
145
+ browsers: [],
146
+ executionConfig: {},
147
+ });
148
+ let bridgeOptionsReceived = null;
149
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
150
+ bridgeOptionsReceived = options;
151
+ return {
152
+ status: 'passed',
153
+ duration: '1.0s',
154
+ results: [],
155
+ logs: [],
156
+ };
157
+ });
158
+ await runCommand('specs/Auth/TC-0001/android.test.ts', {
159
+ target: 'android',
160
+ allDevices: true,
161
+ });
162
+ expect(bridgeOptionsReceived).not.toBeNull();
163
+ expect(bridgeOptionsReceived.platform).toBe('android');
164
+ expect(bridgeOptionsReceived.config.androidConfig.deviceName).toBeUndefined();
165
+ expect(bridgeOptionsReceived.config.androidConfig.devices).toBe('auto');
166
+ expect(bridgeOptionsReceived.targetDevice).toBe('auto');
167
+ });
168
+ it('correctly serializes --multi-device flag into auto devices in config', async () => {
169
+ vi.spyOn(TypeGenerator, 'writeDeclarationFiles').mockImplementation(() => { });
170
+ vi.spyOn(ConfigLoader, 'loadConfig').mockResolvedValue({
171
+ webConfig: { baseUrl: 'http://localhost:3000' },
172
+ androidConfig: { driverServer: 'http://127.0.0.1:4800' },
173
+ iosConfig: {},
174
+ browsers: [],
175
+ executionConfig: {},
176
+ });
177
+ let bridgeOptionsReceived = null;
178
+ vi.spyOn(RustCoreBridge, 'run').mockImplementation(async (options) => {
179
+ bridgeOptionsReceived = options;
180
+ return {
181
+ status: 'passed',
182
+ duration: '1.0s',
183
+ results: [],
184
+ logs: [],
185
+ };
186
+ });
187
+ await runCommand('specs/Auth/TC-0001/android.test.ts', {
188
+ target: 'android',
189
+ multiDevice: true,
190
+ });
191
+ expect(bridgeOptionsReceived).not.toBeNull();
192
+ expect(bridgeOptionsReceived.platform).toBe('android');
193
+ expect(bridgeOptionsReceived.config.androidConfig.deviceName).toBeUndefined();
194
+ expect(bridgeOptionsReceived.config.androidConfig.devices).toBe('auto');
195
+ expect(bridgeOptionsReceived.targetDevice).toBe('auto');
196
+ });
197
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,155 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
+ import { startWatcher, watchCommand } from '../watch.js';
6
+ async function waitForCondition(fn, timeout = 2500, interval = 25) {
7
+ const start = Date.now();
8
+ while (Date.now() - start < timeout) {
9
+ if (fn())
10
+ return;
11
+ await new Promise((r) => setTimeout(r, interval));
12
+ }
13
+ if (!fn()) {
14
+ throw new Error(`Timed out waiting for file watcher condition after ${timeout}ms`);
15
+ }
16
+ }
17
+ describe('watch command and startWatcher daemon', () => {
18
+ let tmpDir;
19
+ beforeEach(() => {
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spectra-watch-test-'));
21
+ });
22
+ afterEach(() => {
23
+ try {
24
+ fs.rmSync(tmpDir, { recursive: true, force: true });
25
+ }
26
+ catch { }
27
+ });
28
+ it('runs initial generation on startWatcher startup', () => {
29
+ fs.mkdirSync(path.join(tmpDir, 'page-objects', 'AuthPage'), { recursive: true });
30
+ fs.writeFileSync(path.join(tmpDir, 'page-objects', 'AuthPage', 'web.ts'), 'export default class AuthPage {}');
31
+ const handle = startWatcher(tmpDir, 30);
32
+ try {
33
+ const webDtsPath = path.join(tmpDir, '.testspectra', 'types', 'web.d.ts');
34
+ expect(fs.existsSync(webDtsPath)).toBe(true);
35
+ const content = fs.readFileSync(webDtsPath, 'utf-8');
36
+ expect(content).toContain('declare const AuthPage:');
37
+ }
38
+ finally {
39
+ handle.close();
40
+ }
41
+ });
42
+ it('dynamically regenerates types when new page object or step is added during watch', async () => {
43
+ const handle = startWatcher(tmpDir, 30);
44
+ // Allow OS watcher handle to hook in
45
+ await new Promise((resolve) => setTimeout(resolve, 50));
46
+ try {
47
+ // 1. Add page object dynamically
48
+ const poDir = path.join(tmpDir, 'page-objects', 'CheckoutPage');
49
+ fs.mkdirSync(poDir, { recursive: true });
50
+ fs.writeFileSync(path.join(poDir, 'web.ts'), 'export default class CheckoutPage {}');
51
+ // 2. Add step dynamically
52
+ const stepDir = path.join(tmpDir, 'support', 'steps', 'submitOrder');
53
+ fs.mkdirSync(stepDir, { recursive: true });
54
+ fs.writeFileSync(path.join(stepDir, 'web.step.ts'), 'export default async function submitOrder() {}');
55
+ // Poll until watcher updates web.d.ts
56
+ await waitForCondition(() => {
57
+ try {
58
+ const webDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'), 'utf-8');
59
+ return webDts.includes('declare const CheckoutPage:') && webDts.includes('"submitOrder":');
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ });
65
+ const webDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'), 'utf-8');
66
+ expect(webDts).toContain('declare const CheckoutPage:');
67
+ expect(webDts).toContain('"submitOrder":');
68
+ }
69
+ finally {
70
+ handle.close();
71
+ }
72
+ });
73
+ it('dynamically purges types when a file is deleted during watch', async () => {
74
+ const poDir = path.join(tmpDir, 'page-objects', 'TemporaryPage');
75
+ fs.mkdirSync(poDir, { recursive: true });
76
+ const poFile = path.join(poDir, 'web.ts');
77
+ fs.writeFileSync(poFile, 'export default class TemporaryPage {}');
78
+ const handle = startWatcher(tmpDir, 30);
79
+ await new Promise((resolve) => setTimeout(resolve, 50));
80
+ try {
81
+ let webDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'), 'utf-8');
82
+ expect(webDts).toContain('declare const TemporaryPage:');
83
+ // Delete file during watch
84
+ fs.unlinkSync(poFile);
85
+ // Poll until watcher purges TemporaryPage from web.d.ts
86
+ await waitForCondition(() => {
87
+ try {
88
+ const content = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'), 'utf-8');
89
+ return !content.includes('TemporaryPage');
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ });
95
+ webDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'web.d.ts'), 'utf-8');
96
+ expect(webDts).not.toContain('TemporaryPage');
97
+ }
98
+ finally {
99
+ handle.close();
100
+ }
101
+ });
102
+ it('dynamically generates strongly-typed fixture imports on new json file', async () => {
103
+ const handle = startWatcher(tmpDir, 30);
104
+ await new Promise((resolve) => setTimeout(resolve, 50));
105
+ try {
106
+ const fixturesDir = path.join(tmpDir, 'fixtures');
107
+ fs.mkdirSync(fixturesDir, { recursive: true });
108
+ fs.writeFileSync(path.join(fixturesDir, 'accountData.json'), JSON.stringify({ balance: 5000 }));
109
+ await waitForCondition(() => {
110
+ try {
111
+ const fixturesDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'fixtures.d.ts'), 'utf-8');
112
+ return fixturesDts.includes("readonly accountData: typeof import('../../fixtures/accountData.json');");
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ });
118
+ const fixturesDts = fs.readFileSync(path.join(tmpDir, '.testspectra', 'types', 'fixtures.d.ts'), 'utf-8');
119
+ expect(fixturesDts).toContain("readonly accountData: typeof import('../../fixtures/accountData.json');");
120
+ }
121
+ finally {
122
+ handle.close();
123
+ }
124
+ });
125
+ it('ignores file changes in node_modules, .git, and .testspectra directories', async () => {
126
+ const handle = startWatcher(tmpDir, 30);
127
+ await new Promise((resolve) => setTimeout(resolve, 50));
128
+ try {
129
+ const gitDir = path.join(tmpDir, '.git');
130
+ fs.mkdirSync(gitDir, { recursive: true });
131
+ fs.writeFileSync(path.join(gitDir, 'HEAD'), 'ref: refs/heads/main');
132
+ const nodeModulesDir = path.join(tmpDir, 'node_modules', 'dummy');
133
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
134
+ fs.writeFileSync(path.join(nodeModulesDir, 'package.json'), '{}');
135
+ // None of these should cause errors or corrupt generated types
136
+ await new Promise((resolve) => setTimeout(resolve, 80));
137
+ const typesDir = path.join(tmpDir, '.testspectra', 'types');
138
+ expect(fs.existsSync(typesDir)).toBe(true);
139
+ }
140
+ finally {
141
+ handle.close();
142
+ }
143
+ });
144
+ it('invokes watchCommand CLI wrapper without throwing and closes cleanly', async () => {
145
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
146
+ // Create a dummy page object
147
+ fs.mkdirSync(path.join(tmpDir, 'page-objects', 'HomePage'), { recursive: true });
148
+ fs.writeFileSync(path.join(tmpDir, 'page-objects', 'HomePage', 'web.ts'), 'export default class HomePage {}');
149
+ // Run watchCommand with custom cwd
150
+ await watchCommand({ cwd: tmpDir, debounceMs: 30 });
151
+ const typesPath = path.join(tmpDir, '.testspectra', 'types', 'web.d.ts');
152
+ expect(fs.existsSync(typesPath)).toBe(true);
153
+ logSpy.mockRestore();
154
+ });
155
+ });