@testspectra/cli 1.0.70 → 1.1.0-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 (427) 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-BhlW-eXG.css +1 -0
  7. package/demo-app/assets/index-DOtRypCa.js +154 -0
  8. package/demo-app/index.html +2 -2
  9. package/dist/.tsbuildinfo +1 -0
  10. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  11. package/dist/commands/__tests__/doctor.test.js +39 -0
  12. package/dist/commands/__tests__/init.test.d.ts +1 -0
  13. package/dist/commands/__tests__/init.test.js +148 -0
  14. package/dist/commands/__tests__/license.test.d.ts +1 -0
  15. package/dist/commands/__tests__/license.test.js +13 -0
  16. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  17. package/dist/commands/__tests__/refactor.test.js +64 -0
  18. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  19. package/dist/commands/__tests__/run-e2e.test.js +422 -0
  20. package/dist/commands/__tests__/run.test.d.ts +1 -0
  21. package/dist/commands/__tests__/run.test.js +273 -0
  22. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  23. package/dist/commands/__tests__/watch.test.js +155 -0
  24. package/dist/commands/add.js +86 -85
  25. package/dist/commands/demo.js +21 -11
  26. package/dist/commands/devices.d.ts +14 -2
  27. package/dist/commands/devices.js +44 -34
  28. package/dist/commands/doctor.d.ts +3 -1
  29. package/dist/commands/doctor.js +43 -264
  30. package/dist/commands/init.d.ts +1 -1
  31. package/dist/commands/init.js +211 -196
  32. package/dist/commands/install.d.ts +4 -0
  33. package/dist/commands/install.js +59 -0
  34. package/dist/commands/license.d.ts +4 -0
  35. package/dist/commands/license.js +30 -0
  36. package/dist/commands/lint.d.ts +5 -0
  37. package/dist/commands/lint.js +55 -0
  38. package/dist/commands/refactor.d.ts +9 -0
  39. package/dist/commands/refactor.js +172 -0
  40. package/dist/commands/run.d.ts +23 -2
  41. package/dist/commands/run.js +322 -111
  42. package/dist/commands/skills.d.ts +9 -0
  43. package/dist/commands/skills.js +28 -0
  44. package/dist/commands/sync-types.d.ts +4 -0
  45. package/dist/commands/sync-types.js +15 -0
  46. package/dist/commands/test.d.ts +17 -0
  47. package/dist/commands/test.js +76 -0
  48. package/dist/commands/update.js +39 -43
  49. package/dist/commands/watch.d.ts +6 -0
  50. package/dist/commands/watch.js +55 -45
  51. package/dist/config/loader.d.ts +1 -1
  52. package/dist/config/loader.js +40 -25
  53. package/dist/config/schema.d.ts +104 -145
  54. package/dist/config/schema.js +36 -45
  55. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  56. package/dist/generator/__tests__/tsconfig-isolation.test.js +282 -0
  57. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  58. package/dist/generator/__tests__/type-generator.test.js +386 -0
  59. package/dist/generator/index.d.ts +2 -0
  60. package/dist/generator/index.js +2 -0
  61. package/dist/generator/tsconfig-generator.d.ts +27 -0
  62. package/dist/generator/tsconfig-generator.js +419 -0
  63. package/dist/generator/type-generator.d.ts +59 -0
  64. package/dist/generator/type-generator.js +740 -0
  65. package/dist/index.d.ts +9 -5
  66. package/dist/index.js +165 -51
  67. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  68. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  69. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  70. package/dist/lifecycle/hook-discovery.js +60 -0
  71. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  72. package/dist/lifecycle/hook-dispatcher.js +190 -0
  73. package/dist/lifecycle/index.d.ts +3 -0
  74. package/dist/lifecycle/index.js +3 -0
  75. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  76. package/dist/lifecycle/parallel-grouping.js +151 -0
  77. package/dist/linter/__tests__/anonymous-export.test.d.ts +1 -0
  78. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  79. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  80. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  81. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  82. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  83. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  84. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  85. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  86. package/dist/linter/__tests__/step-parity.test.js +372 -0
  87. package/dist/linter/discovery.d.ts +30 -0
  88. package/dist/linter/discovery.js +249 -0
  89. package/dist/linter/extractor.d.ts +67 -0
  90. package/dist/linter/extractor.js +245 -0
  91. package/dist/linter/index.d.ts +7 -0
  92. package/dist/linter/index.js +7 -0
  93. package/dist/linter/parser.d.ts +24 -0
  94. package/dist/linter/parser.js +219 -0
  95. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  96. package/dist/linter/schemas/spec.schema.js +150 -0
  97. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  98. package/dist/linter/schemas/suite.schema.js +83 -0
  99. package/dist/linter/spec-linter.d.ts +23 -0
  100. package/dist/linter/spec-linter.js +630 -0
  101. package/dist/linter/types.d.ts +28 -0
  102. package/dist/linter/types.js +1 -0
  103. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  104. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  105. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  106. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  107. package/dist/mobile/device-pool.d.ts +23 -0
  108. package/dist/mobile/device-pool.js +1 -0
  109. package/dist/mobile/index.d.ts +1 -0
  110. package/dist/mobile/index.js +1 -0
  111. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  112. package/dist/refactor/__tests__/refactor-engine.test.js +873 -0
  113. package/dist/refactor/index.d.ts +1 -0
  114. package/dist/refactor/index.js +1 -0
  115. package/dist/refactor/refactor-engine.d.ts +50 -0
  116. package/dist/refactor/refactor-engine.js +587 -0
  117. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  118. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  119. package/dist/reporter/semantic-formatter.d.ts +6 -0
  120. package/dist/reporter/semantic-formatter.js +206 -0
  121. package/dist/reporter/summary-view.d.ts +12 -0
  122. package/dist/reporter/summary-view.js +19 -0
  123. package/dist/reporter/terminal-stream.d.ts +32 -0
  124. package/dist/reporter/terminal-stream.js +82 -0
  125. package/dist/reporter/types.d.ts +2 -0
  126. package/dist/reporter/types.js +1 -0
  127. package/dist/runner/bridge.d.ts +10 -5
  128. package/dist/runner/bridge.js +101 -45
  129. package/dist/runner/reporter.d.ts +32 -2
  130. package/dist/runner/reporter.js +427 -13
  131. package/dist/server/demo.js +43 -44
  132. package/dist/types/generator.d.ts +1 -17
  133. package/dist/types/generator.js +1 -443
  134. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  135. package/dist/utils/__tests__/api-server.test.js +62 -0
  136. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  137. package/dist/utils/__tests__/demo-server.test.js +35 -0
  138. package/dist/utils/api-server.d.ts +19 -0
  139. package/dist/utils/api-server.js +179 -0
  140. package/dist/utils/demo-server.d.ts +5 -0
  141. package/dist/utils/demo-server.js +46 -36
  142. package/package.json +25 -13
  143. package/templates/default/CLAUDE.md +12 -5
  144. package/templates/default/fixtures/apiData.json +24 -0
  145. package/templates/default/package.json +12 -3
  146. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  147. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  148. package/templates/default/page-objects/MatchersPage/mobile.ts +66 -17
  149. package/templates/default/page-objects/MatchersPage/web.ts +22 -2
  150. package/templates/default/page-objects/NavigationPage/mobile.ts +11 -2
  151. package/templates/default/page-objects/NavigationPage/web.ts +1 -1
  152. package/templates/default/page-objects/PlaygroundPage/mobile.ts +34 -18
  153. package/templates/default/page-objects/PlaygroundPage/web.ts +4 -2
  154. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -0
  155. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -0
  156. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -0
  157. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  158. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  159. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -0
  160. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -0
  161. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -0
  162. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  163. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  164. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  165. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  166. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  167. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  168. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  169. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  170. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  171. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  172. package/templates/default/specs/ApiInterception/suite.md +10 -0
  173. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  174. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  175. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  176. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  177. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  178. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  179. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  180. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  181. package/templates/default/specs/Authentication/suite.md +9 -0
  182. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  183. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
  184. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  185. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  186. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  187. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  188. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  189. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
  190. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  191. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
  192. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +9 -4
  193. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  194. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  195. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  196. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  197. package/templates/default/specs/DeepLink/suite.md +9 -0
  198. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  199. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
  200. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  201. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  202. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
  203. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  204. package/templates/default/specs/ElementMatchers/suite.md +2 -1
  205. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  206. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  207. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  208. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  209. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  210. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  211. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  212. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  213. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  214. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
  215. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +4 -4
  216. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  217. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
  218. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +3 -3
  219. package/templates/default/spectra.config.ts +22 -31
  220. package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
  221. package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
  222. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  223. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  224. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +3 -1
  225. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +3 -3
  226. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +4 -2
  227. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +4 -4
  228. package/templates/default/tsconfig.json +3 -1
  229. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  230. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  231. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  232. package/templates/nx/.nx/workspace-data/d/daemon.log +5366 -0
  233. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  234. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  235. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  236. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  237. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  238. package/templates/nx/.nx/workspace-data/project-graph.json +14389 -0
  239. package/templates/nx/CLAUDE.md +2 -2
  240. package/templates/nx/package.json +6 -4
  241. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -0
  242. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  243. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  244. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -0
  245. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  246. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  247. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  248. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  249. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  250. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  251. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +15 -5
  252. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  253. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +9 -4
  254. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  255. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  256. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  257. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  258. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  259. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  260. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  261. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  262. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +4 -4
  263. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  264. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +3 -3
  265. package/templates/nx/pnpm-lock.yaml +6663 -0
  266. package/templates/nx/pnpm-workspace.yaml +2 -1
  267. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  268. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +13 -1
  269. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +85 -17
  270. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  271. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  272. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  273. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +4 -2
  274. package/templates/nx/shared/testing/project.json +19 -0
  275. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  276. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  277. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  278. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +3 -3
  279. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +2 -2
  280. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +4 -4
  281. package/templates/nx/spectra.config.ts +20 -29
  282. package/templates/nx/tsconfig.json +3 -1
  283. package/templates/react-component/.cursorrules +17 -0
  284. package/templates/react-component/.vscode/extensions.json +5 -0
  285. package/templates/react-component/CLAUDE.md +14 -0
  286. package/templates/react-component/README.md +69 -0
  287. package/templates/react-component/fixtures/component-mock.json +12 -0
  288. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  289. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  290. package/templates/react-component/package.json +27 -0
  291. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  292. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  293. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  294. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  295. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  296. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  297. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  298. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  299. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  300. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  301. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  302. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  303. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  304. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  305. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  306. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  307. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  308. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  309. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  310. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  311. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  312. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  313. package/templates/react-component/spectra.config.ts +27 -0
  314. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  315. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  316. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  317. package/templates/react-component/src/test-utils.tsx +23 -0
  318. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  319. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  320. package/templates/react-component/tsconfig.json +8 -0
  321. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  322. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  323. package/dist/index.cjs +0 -101
  324. package/dist/plugin.cjs +0 -575
  325. package/dist/plugin.d.ts +0 -16
  326. package/dist/plugin.js +0 -184
  327. package/dist/step/index.d.ts +0 -6
  328. package/dist/step/index.js +0 -6
  329. package/dist/step/matchers.d.ts +0 -14
  330. package/dist/step/matchers.js +0 -114
  331. package/dist/step/proto.d.ts +0 -46
  332. package/dist/step/proto.js +0 -114
  333. package/dist/step/runner/collection.d.ts +0 -13
  334. package/dist/step/runner/collection.js +0 -51
  335. package/dist/step/runner/single.d.ts +0 -21
  336. package/dist/step/runner/single.js +0 -138
  337. package/dist/step/spectra.d.ts +0 -34
  338. package/dist/step/spectra.js +0 -156
  339. package/dist/step/types.d.ts +0 -32
  340. package/dist/step/types.js +0 -5
  341. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  342. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  343. package/templates/default/tsconfig.spectra.android.json +0 -46
  344. package/templates/default/tsconfig.spectra.ios.json +0 -46
  345. package/templates/default/tsconfig.spectra.json +0 -22
  346. package/templates/default/tsconfig.spectra.web.json +0 -53
  347. package/templates/nx/fixtures/playgroundData.json +0 -7
  348. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  349. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  350. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  351. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  352. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  353. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  354. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  355. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  356. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  357. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  358. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  359. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  360. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  361. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  362. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  363. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  364. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  365. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  366. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  367. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  368. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  369. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  370. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  371. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  372. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  373. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  374. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  375. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  376. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  377. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  378. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  379. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  380. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  381. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  382. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  383. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  384. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  385. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  386. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  387. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  388. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  389. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  390. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  391. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  392. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  393. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  394. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  395. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  396. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  397. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  398. package/templates/nx/specs/Playground/suite.md +0 -9
  399. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  400. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  401. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  402. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  403. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  404. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  405. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  406. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  407. package/templates/nx/tsconfig.spectra.android.json +0 -39
  408. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  409. package/templates/nx/tsconfig.spectra.json +0 -25
  410. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  411. package/templates/nx/tsconfig.spectra.web.json +0 -45
  412. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  413. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  414. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  415. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  416. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  417. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  418. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  419. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  420. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  421. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  422. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  423. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  424. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  425. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  426. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  427. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -1,134 +1,342 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { ConfigLoader } from "../config/loader.js";
4
- import { RustCoreBridge } from "../runner/bridge.js";
5
- import { Reporter } from "../runner/reporter.js";
6
- import { TypeGenerator } from "../types/generator.js";
7
- import { DemoServer } from "../utils/demo-server.js";
8
- export async function runCommand(specPath, options = {}) {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ConfigLoader } from '../config/loader.js';
4
+ import { RustCoreBridge } from '../runner/bridge.js';
5
+ import { Reporter } from '../runner/reporter.js';
6
+ import { TypeGenerator } from '../types/generator.js';
7
+ import { DemoServer } from '../utils/demo-server.js';
8
+ import { DemoApiServer } from '../utils/api-server.js';
9
+ function renderSummaryCard(options) {
10
+ const width = 64;
11
+ const borderColor = options.isPassed ? '\x1b[38;2;52;211;153m' : '\x1b[38;2;248;113;113m';
12
+ const reset = '\x1b[0m';
13
+ const boldWhite = '\x1b[1;37m';
14
+ const dim = '\x1b[90m';
15
+ const cyan = '\x1b[38;2;45;212;191m';
16
+ const yellow = '\x1b[38;2;251;191;36m';
17
+ const green = '\x1b[38;2;52;211;153m';
18
+ const red = '\x1b[38;2;248;113;113m';
19
+ const strip = (s) => s.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
20
+ const visualWidth = (s) => {
21
+ const plain = strip(s);
22
+ let w = 0;
23
+ for (const ch of plain) {
24
+ const code = ch.codePointAt(0) || 0;
25
+ if (code >= 0x1f300 || (code >= 0x2600 && code <= 0x27bf)) {
26
+ w += 2;
27
+ }
28
+ else {
29
+ w += 1;
30
+ }
31
+ }
32
+ return w;
33
+ };
34
+ const row = (label, value) => {
35
+ const rawLen = visualWidth(label) + visualWidth(value);
36
+ const padding = Math.max(1, width - rawLen);
37
+ return ` ${borderColor}│${reset} ${label}${' '.repeat(padding)}${value} ${borderColor}│${reset}`;
38
+ };
39
+ const headerRow = (title) => {
40
+ const rawLen = visualWidth(title);
41
+ const padding = Math.max(1, width - rawLen);
42
+ return ` ${borderColor}│${reset} ${title}${' '.repeat(padding)} ${borderColor}│${reset}`;
43
+ };
44
+ const top = ` ${borderColor}╭${'─'.repeat(width + 4)}╮${reset}`;
45
+ const mid = ` ${borderColor}├${'─'.repeat(width + 4)}┤${reset}`;
46
+ const bot = ` ${borderColor}╰${'─'.repeat(width + 4)}╯${reset}`;
47
+ const statusBadge = options.isPassed ? `${green}✓ PASSED${reset}` : `${red}✗ FAILED${reset}`;
48
+ const passedSuites = options.passedSuites ?? (options.isPassed ? options.suiteCount : 0);
49
+ const failedSuites = options.failedSuites ?? (options.isPassed ? 0 : options.suiteCount);
50
+ const testsPassedText = options.carriedPassed
51
+ ? `${options.passedCount > 0 ? `${green}${options.passedCount} passed${reset}` : `${options.passedCount} passed`} ${dim}(${options.carriedPassed} carried over)${reset}`
52
+ : options.passedCount > 0
53
+ ? `${green}${options.passedCount} passed${reset}`
54
+ : `${options.passedCount} passed`;
55
+ console.log(`\n${top}`);
56
+ console.log(headerRow(`${boldWhite}✨ TESTSPECTRA EXECUTION SUMMARY${reset}`));
57
+ console.log(mid);
58
+ console.log(row(`${dim}Status${reset}`, statusBadge));
59
+ console.log(row(`${dim}Suites${reset}`, `${passedSuites > 0 ? `${green}${passedSuites} passed${reset}` : `${passedSuites} passed`}, ` +
60
+ `${failedSuites > 0 ? `${red}${failedSuites} failed${reset}` : `${failedSuites} failed`} ` +
61
+ `${dim}(${options.suiteCount} total)${reset}`));
62
+ console.log(row(`${dim}Tests${reset}`, `${testsPassedText}, ` +
63
+ `${options.failedCount > 0 ? `${red}${options.failedCount} failed${reset}` : `${options.failedCount} failed`} ` +
64
+ `${dim}(${options.testCount} total)${reset}`));
65
+ console.log(row(`${dim}Platform${reset}`, `${cyan}${options.platform}${reset}`));
66
+ console.log(row(`${dim}Duration${reset}`, `${yellow}${options.duration}${reset}`));
67
+ console.log(row(`${dim}Report${reset}`, `${dim}${options.reportPath}${reset}`));
68
+ if (options.networkReportPath) {
69
+ console.log(row(`${dim}Network${reset}`, `${dim}${options.networkReportPath}${reset}`));
70
+ }
71
+ console.log(`${bot}\n`);
72
+ }
73
+ const TIMING_PHASE_LABELS = {
74
+ web: { boot: '1. Master Chromium Process Boot', handshake: '3. Incognito Context & CDP Handshake' },
75
+ android: { boot: '1. Android Driver Bootstrap (ADB + Agent)', handshake: '3. Device Session Handshake' },
76
+ ios: { boot: '1. iOS Driver Bootstrap (Appium)', handshake: '3. WDA Session Handshake' },
77
+ };
78
+ export function renderTimingBreakdown(performance, platform = 'web') {
79
+ const phaseLabels = TIMING_PHASE_LABELS[platform] ?? TIMING_PHASE_LABELS.web;
80
+ const yellowBold = '\x1b[1;38;2;251;191;36m';
81
+ const reset = '\x1b[0m';
82
+ const borderColor = '\x1b[38;2;75;85;99m';
83
+ const pad = (str, width) => {
84
+ const s = String(str);
85
+ return ' '.repeat(Math.max(0, width - s.length)) + s;
86
+ };
87
+ const padRight = (str, width) => {
88
+ const s = String(str);
89
+ return s + ' '.repeat(Math.max(0, width - s.length));
90
+ };
91
+ const wallClock = performance.duration
92
+ ? performance.duration
93
+ : `${((performance.wallClockDurationMs ?? 0) / 1000).toFixed(2)}s`;
94
+ const totalTests = performance.totalTests ?? 1;
95
+ const totalTestExecMs = performance.totalTestExecutionMs ?? (performance.avgTestExecutionMs ?? 0) * totalTests;
96
+ const avgTestExecMs = performance.avgTestExecutionMs ?? (totalTests > 0 ? Math.round(totalTestExecMs / totalTests) : 0);
97
+ const contextHandshakeMs = (performance.avgContextCreationMs ?? 0) + (performance.avgCdpHandshakeMs ?? 0);
98
+ const testLabel = totalTests === 1 ? '4. Total Test Execution (1 test)' : `4. Total Test Execution (${totalTests} tests)`;
99
+ const testDetail = totalTests <= 1 ? `${pad(totalTestExecMs, 13)} ms` : `${pad(totalTestExecMs, 5)} ms (${pad(avgTestExecMs, 3)}ms/t)`;
100
+ console.log(`⏱️ ${yellowBold}EXECUTION TIMING BREAKDOWN (PHASE-BY-PHASE)${reset}`);
101
+ console.log(`${borderColor}┌──────────────────────────────────────────┬───────────────────┐${reset}`);
102
+ console.log(`${borderColor}│${reset} Execution Phase ${borderColor}│${reset} Duration / Detail ${borderColor}│${reset}`);
103
+ console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
104
+ console.log(`${borderColor}│${reset} ${padRight(phaseLabels.boot, 40)} ${borderColor}│${reset} ${pad(performance.masterBootLatencyMs ?? 0, 13)} ms ${borderColor}│${reset}`);
105
+ console.log(`${borderColor}│${reset} ${padRight('2. Worker Spawning & Runtime Overhead', 40)} ${borderColor}│${reset} ${pad(performance.runnerOverheadMs ?? 0, 13)} ms ${borderColor}│${reset}`);
106
+ console.log(`${borderColor}│${reset} ${padRight(phaseLabels.handshake, 40)} ${borderColor}│${reset} ${pad(contextHandshakeMs, 10)} ms (avg)${borderColor}│${reset}`);
107
+ console.log(`${borderColor}│${reset} ${padRight(testLabel, 40)} ${borderColor}│${reset} ${pad(testDetail, 17)} ${borderColor}│${reset}`);
108
+ console.log(`${borderColor}│${reset} ${padRight('5. Context Teardown / Cleanup', 40)} ${borderColor}│${reset} ${pad(performance.avgTeardownMs ?? 0, 10)} ms (avg)${borderColor}│${reset}`);
109
+ console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
110
+ console.log(`${borderColor}│${reset} Total Wall-Clock Execution Time ${borderColor}│${reset} ${pad(wallClock, 15)} ${borderColor}│${reset}`);
111
+ console.log(`${borderColor}└──────────────────────────────────────────┴───────────────────┘${reset}\n`);
112
+ }
113
+ export async function runCommand(specPaths, options = {}) {
9
114
  const cwd = process.cwd();
10
- const effectiveSpecPath = specPath || options.spec;
115
+ // Opt-in duration logging (TESTSPECTRA_DURATION_LOG): resolve to one absolute JSONL path that
116
+ // both this host process (Rust orchestrator) and every spawned Bun worker append to, and stamp a
117
+ // run id so repeated runs can be told apart. Off by default; toggled purely via the env var.
118
+ if (process.env.TESTSPECTRA_DURATION_LOG) {
119
+ const v = process.env.TESTSPECTRA_DURATION_LOG;
120
+ const abs = v === '1' || v === 'true' ? path.join(cwd, '.testspectra', 'logs', 'duration.jsonl') : path.resolve(cwd, v);
121
+ process.env.TESTSPECTRA_DURATION_LOG = abs;
122
+ if (!process.env.TESTSPECTRA_RUN_ID)
123
+ process.env.TESTSPECTRA_RUN_ID = String(Date.now());
124
+ }
125
+ const effectiveSpecPaths = specPaths && specPaths.length > 0 ? specPaths : options.spec ? [options.spec] : undefined;
11
126
  // Ensure latest ambient types are up-to-date
12
127
  TypeGenerator.writeDeclarationFiles(cwd);
13
- const config = await ConfigLoader.loadConfig(cwd);
14
- let demoServer = null;
15
- if (options.demo) {
16
- demoServer = new DemoServer(5173);
17
- try {
18
- const { url } = await demoServer.start();
19
- config.webConfig.baseUrl = url;
20
- console.log(`\x1b[36m[TestSpectra]\x1b[0m 🌐 Demo Web App automatically started at: \x1b[33m${url}\x1b[0m`);
21
- }
22
- catch (err) {
23
- console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo server: ${err.message}\x1b[0m`);
24
- }
128
+ await TypeGenerator.syncEnvDeclaration(cwd);
129
+ const home = process.env.HOME || process.env.USERPROFILE || '';
130
+ const globalAppDataPath = path.join(home, '.testspectra');
131
+ const appDataPath = options.workdir ? path.resolve(options.workdir) : globalAppDataPath;
132
+ if (!fs.existsSync(appDataPath)) {
133
+ fs.mkdirSync(appDataPath, { recursive: true });
25
134
  }
26
- if (options.headless !== undefined) {
27
- config.webConfig.headlessMode = options.headless;
135
+ const projectAppData = path.join(cwd, '.testspectra');
136
+ const outputJsonPath = options.output
137
+ ? path.resolve(options.output)
138
+ : path.join(projectAppData, 'reports', 'result.json');
139
+ const isFailedMode = options.failed !== undefined;
140
+ let failedTarget;
141
+ if (typeof options.failed === 'string' && options.failed !== 'true') {
142
+ failedTarget = options.failed;
28
143
  }
29
- const platform = options.target || "web";
30
- // Find workspace root for centralized .testspectra (monorepo root or current test project)
31
- let workspaceRoot = cwd;
32
- let cur = cwd;
33
- while (cur !== path.dirname(cur)) {
34
- if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) || fs.existsSync(path.join(cur, "nx.json"))) {
35
- workspaceRoot = cur;
36
- break;
144
+ if (isFailedMode) {
145
+ if (fs.existsSync(outputJsonPath)) {
146
+ try {
147
+ const prev = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
148
+ const failedItems = (prev.suites || []).flatMap((s) => (s.tests || []).filter((t) => t.passed === false || t.status === 'failed'));
149
+ if (failedItems.length === 0) {
150
+ console.log(`\x1b[32m✨ [TestSpectra] No failed tests found in previous test session! Everything is already passing.\x1b[0m\n`);
151
+ return;
152
+ }
153
+ const targetDesc = failedTarget ? `target '${failedTarget}' in` : '';
154
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m 🔄 Re-running ${targetDesc} ${failedItems.length} failed test(s) with session state carry-over...`);
155
+ }
156
+ catch { }
37
157
  }
38
- cur = path.dirname(cur);
39
- }
40
- const appDataPath = options.workdir ? path.resolve(options.workdir) : path.join(workspaceRoot, ".testspectra");
41
- if (!fs.existsSync(appDataPath)) {
42
- fs.mkdirSync(appDataPath, { recursive: true });
43
158
  }
44
- // Determine suite/test case name
45
- let suiteName = "default";
46
- let testCases = [];
47
- if (effectiveSpecPath) {
48
- const relSpec = path.isAbsolute(effectiveSpecPath) ? path.relative(cwd, effectiveSpecPath) : effectiveSpecPath;
49
- const normalized = relSpec.replace(/\\/g, "/");
50
- const parts = normalized.split("/");
51
- if (parts.length >= 2 && (parts[0] === "specs" || parts[0] === "suites")) {
52
- suiteName = parts[1];
159
+ const config = await ConfigLoader.loadConfig(cwd);
160
+ let demoServer = null;
161
+ let apiServer = null;
162
+ try {
163
+ if (options.demo) {
164
+ demoServer = new DemoServer(5173);
165
+ try {
166
+ const { url } = await demoServer.start();
167
+ config.webConfig.baseUrl = url;
168
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m 🌐 Demo Web App automatically started at: \x1b[33m${url}\x1b[0m`);
169
+ }
170
+ catch (err) {
171
+ console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo server: ${err.message}\x1b[0m`);
172
+ }
173
+ apiServer = new DemoApiServer();
174
+ try {
175
+ const { url, port } = await apiServer.start();
176
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m 🌐 Demo API server automatically started at: \x1b[33m${url}\x1b[0m (port ${port}, self-hosted /api/*)`);
177
+ }
178
+ catch (err) {
179
+ console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo API server: ${err.message}\x1b[0m`);
180
+ }
53
181
  }
54
- else {
55
- const parsed = path.parse(normalized);
56
- suiteName = parsed.name.replace(/\.(web|android|ios|common|test|spec)/g, "") || "default";
182
+ if (options.headed) {
183
+ config.webConfig.headless = false;
57
184
  }
58
- testCases.push({
59
- id: normalized,
60
- title: normalized,
61
- executionOrder: 1,
62
- });
63
- }
64
- else {
65
- // 1. Scan specs/ directory (specs/<Suite>/<CaseId>/web.test.ts)
66
- const specsDir = path.join(cwd, "specs");
67
- if (fs.existsSync(specsDir)) {
68
- const suiteEntries = fs.readdirSync(specsDir, { withFileTypes: true });
69
- for (const sEntry of suiteEntries) {
70
- if (sEntry.name.startsWith(".") || !sEntry.isDirectory())
71
- continue;
72
- const currentSuite = sEntry.name;
73
- const suitePath = path.join(specsDir, currentSuite);
74
- const caseEntries = fs.readdirSync(suitePath, { withFileTypes: true });
75
- for (const cEntry of caseEntries) {
76
- if (cEntry.name.startsWith(".") || cEntry.name === "hooks")
77
- continue;
78
- if (cEntry.isDirectory()) {
79
- testCases.push({ id: path.join("specs", currentSuite, cEntry.name).replace(/\\/g, "/"), title: `${currentSuite} / ${cEntry.name}` });
80
- }
81
- else if (cEntry.isFile() && (cEntry.name.endsWith(".test.ts") || cEntry.name.endsWith(".spec.ts"))) {
82
- const id = path.join("specs", currentSuite, cEntry.name).replace(/\\/g, "/");
83
- testCases.push({ id, title: `${currentSuite} / ${cEntry.name}` });
84
- }
85
- }
185
+ else if (options.headless !== undefined) {
186
+ config.webConfig.headless = options.headless;
187
+ }
188
+ const rawDelay = options.stepDelay ?? options.delay;
189
+ if (rawDelay !== undefined) {
190
+ const delayNum = parseInt(String(rawDelay), 10);
191
+ if (!isNaN(delayNum) && delayNum >= 0) {
192
+ config.webConfig.stepDelay = delayNum;
193
+ config.androidConfig.stepDelay = delayNum;
194
+ config.iosConfig.stepDelay = delayNum;
86
195
  }
87
196
  }
88
- // 2. Fallback: Scan suites/ directory
89
- const suitesDir = path.join(cwd, "suites");
90
- if (fs.existsSync(suitesDir)) {
91
- const suiteEntries = fs.readdirSync(suitesDir, { withFileTypes: true });
92
- for (const sEntry of suiteEntries) {
93
- if (sEntry.name.startsWith(".") || !sEntry.isDirectory())
94
- continue;
95
- const currentSuite = sEntry.name;
96
- const suitePath = path.join(suitesDir, currentSuite);
97
- const caseEntries = fs.readdirSync(suitePath, { withFileTypes: true });
98
- for (const cEntry of caseEntries) {
99
- if (cEntry.name.startsWith(".") || cEntry.name === "hooks" || !cEntry.isDirectory())
100
- continue;
101
- testCases.push({ id: path.join("suites", currentSuite, cEntry.name).replace(/\\/g, "/"), title: `${currentSuite} / ${cEntry.name}` });
102
- }
197
+ const platform = options.target || 'web';
198
+ const userConcurrency = options.concurrency ? parseInt(String(options.concurrency), 10) : undefined;
199
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m Starting execution on target \x1b[1m${platform}\x1b[0m...`);
200
+ if (options.devices || options.device) {
201
+ const rawDevices = (options.devices || options.device);
202
+ const devIds = rawDevices.includes(',')
203
+ ? rawDevices
204
+ .split(',')
205
+ .map((s) => s.trim())
206
+ .filter(Boolean)
207
+ : [rawDevices.trim()].filter(Boolean);
208
+ const mappedDevices = devIds.map((id) => ({ id }));
209
+ if (platform === 'ios') {
210
+ config.iosConfig.devices = mappedDevices;
103
211
  }
212
+ else {
213
+ config.androidConfig.devices = mappedDevices;
214
+ }
215
+ const count = devIds.length;
216
+ const desc = devIds.join(', ');
217
+ console.log(`\x1b[36m[TestSpectra Grid]\x1b[0m Mobile device target configured (${count} device(s)): ${desc}`);
104
218
  }
105
- }
106
- if (testCases.length === 0) {
107
- testCases.push({
108
- id: suiteName,
109
- title: suiteName,
110
- executionOrder: 1,
111
- });
112
- }
113
- const outputJsonPath = options.output
114
- ? path.resolve(options.output)
115
- : path.join(appDataPath, "reports/result.json");
116
- console.log(`\x1b[36m[TestSpectra]\x1b[0m Starting execution on target \x1b[1m${platform}\x1b[0m...`);
117
- console.log(`\x1b[36m[TestSpectra]\x1b[0m Base URL: \x1b[33m${config.webConfig.baseUrl}\x1b[0m`);
118
- console.log(`\x1b[36m[TestSpectra]\x1b[0m Suite: ${suiteName} (${testCases.length} case(s))\n`);
119
- const reporter = new Reporter();
120
- try {
219
+ else if (options.allDevices || options.multiDevice) {
220
+ if (platform === 'ios') {
221
+ config.iosConfig.devices = 'auto';
222
+ }
223
+ else {
224
+ config.androidConfig.devices = 'auto';
225
+ }
226
+ console.log('\x1b[36m[TestSpectra Grid]\x1b[0m Multi-device grid enabled (auto-detecting connected devices)');
227
+ }
228
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m Base URL: \x1b[33m${config.webConfig.baseUrl}\x1b[0m\n`);
229
+ const reporter = new Reporter();
230
+ const targetDevice = options.devices || options.device || (options.allDevices || options.multiDevice ? 'auto' : undefined);
121
231
  const result = await RustCoreBridge.run({
122
232
  baseDir: cwd,
123
233
  appDataPath,
124
234
  platform,
125
- suite: suiteName,
126
- testCases,
127
235
  config,
128
- targetDevice: options.device,
236
+ targetDevice,
237
+ licenseKey: options.licenseKey,
129
238
  outputJsonPath,
239
+ specs: effectiveSpecPaths,
240
+ concurrency: userConcurrency,
241
+ failedOnly: isFailedMode,
242
+ failedTarget,
130
243
  }, reporter);
131
- console.log(`\n\x1b[36m[TestSpectra]\x1b[0m Run finished: \x1b[1m${result.status.toUpperCase()}\x1b[0m in ${result.duration}`);
244
+ const isPassed = result.status.toLowerCase() === 'passed';
245
+ const relReport = path.relative(cwd, outputJsonPath);
246
+ const networkReportFile = path.join(path.dirname(outputJsonPath), 'network-resources.json');
247
+ const relNetworkReport = fs.existsSync(networkReportFile) ? path.relative(cwd, networkReportFile) : undefined;
248
+ let carriedOver;
249
+ if (process.env.TESTSPECTRA_CARRIED_OVER) {
250
+ try {
251
+ carriedOver = JSON.parse(process.env.TESTSPECTRA_CARRIED_OVER);
252
+ }
253
+ catch { }
254
+ }
255
+ const carriedPassed = carriedOver?.passed || 0;
256
+ const carriedDurationMs = carriedOver?.durationMs || 0;
257
+ let suiteCount = 1;
258
+ let passedSuites = isPassed ? 1 : 0;
259
+ let failedSuites = isPassed ? 0 : 1;
260
+ let basePassed = result.passedCount ?? (isPassed ? 1 : 0);
261
+ let baseFailed = result.failedCount ?? (isPassed ? 0 : 1);
262
+ const totalPassed = basePassed + carriedPassed;
263
+ const totalFailed = baseFailed;
264
+ const totalTests = basePassed + baseFailed + carriedPassed;
265
+ let finalDuration = result.duration;
266
+ if (carriedDurationMs > 0) {
267
+ const match = result.duration.match(/([\d.]+)\s*(ms|s|m)/);
268
+ let runMs = 0;
269
+ if (match) {
270
+ const val = parseFloat(match[1]);
271
+ const unit = match[2];
272
+ runMs = unit === 's' ? val * 1000 : unit === 'm' ? val * 60000 : val;
273
+ }
274
+ const totalMs = runMs + carriedDurationMs;
275
+ finalDuration = totalMs < 1000 ? `${Math.round(totalMs)}ms` : `${(totalMs / 1000).toFixed(1)}s`;
276
+ }
277
+ const overallPassed = isPassed && totalFailed === 0;
278
+ if (fs.existsSync(outputJsonPath)) {
279
+ try {
280
+ const fullReport = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
281
+ if (fullReport.summary) {
282
+ suiteCount = fullReport.summary.totalSuites ?? 1;
283
+ }
284
+ if (fullReport.suites && Array.isArray(fullReport.suites)) {
285
+ failedSuites = fullReport.suites.filter((s) => (typeof s.failed === 'number' && s.failed > 0) ||
286
+ s.status === 'failed' ||
287
+ s.tests?.some((t) => t.passed === false || t.status === 'failed')).length;
288
+ passedSuites = fullReport.suites.length - failedSuites;
289
+ }
290
+ if (fullReport.performance) {
291
+ renderSummaryCard({
292
+ isPassed: overallPassed,
293
+ statusText: overallPassed ? 'PASSED' : 'FAILED',
294
+ duration: finalDuration,
295
+ platform,
296
+ suiteCount,
297
+ passedSuites,
298
+ failedSuites,
299
+ testCount: totalTests,
300
+ passedCount: totalPassed,
301
+ failedCount: totalFailed,
302
+ carriedPassed,
303
+ reportPath: relReport,
304
+ networkReportPath: relNetworkReport,
305
+ });
306
+ renderTimingBreakdown({
307
+ ...fullReport.performance,
308
+ wallClockDurationMs: fullReport.wallClockDurationMs,
309
+ duration: fullReport.duration,
310
+ totalTests: fullReport.summary?.totalTests ?? totalTests,
311
+ }, platform);
312
+ if (!overallPassed || totalFailed > 0) {
313
+ process.exitCode = 1;
314
+ }
315
+ return;
316
+ }
317
+ }
318
+ catch {
319
+ // ignore
320
+ }
321
+ }
322
+ renderSummaryCard({
323
+ isPassed: overallPassed,
324
+ statusText: overallPassed ? 'PASSED' : 'FAILED',
325
+ duration: finalDuration,
326
+ platform,
327
+ suiteCount,
328
+ passedSuites,
329
+ failedSuites,
330
+ testCount: totalTests,
331
+ passedCount: totalPassed,
332
+ failedCount: totalFailed,
333
+ carriedPassed,
334
+ reportPath: relReport,
335
+ networkReportPath: relNetworkReport,
336
+ });
337
+ if (!overallPassed || totalFailed > 0) {
338
+ process.exitCode = 1;
339
+ }
132
340
  }
133
341
  catch (e) {
134
342
  console.error(`\x1b[31m[TestSpectra] Execution error:\x1b[0m`, e);
@@ -138,5 +346,8 @@ export async function runCommand(specPath, options = {}) {
138
346
  if (demoServer) {
139
347
  await demoServer.stop();
140
348
  }
349
+ if (apiServer) {
350
+ await apiServer.stop();
351
+ }
141
352
  }
142
353
  }
@@ -0,0 +1,9 @@
1
+ export interface SkillsCommandOptions {
2
+ cwd?: string;
3
+ agent?: string;
4
+ }
5
+ export declare function skillsInitCommand(options?: SkillsCommandOptions): Promise<void>;
6
+ export declare function skillsListCommand(options?: SkillsCommandOptions): void;
7
+ export declare function skillsAddCommand(ids?: string[], options?: SkillsCommandOptions): Promise<void>;
8
+ export declare function skillsUpdateCommand(ids?: string[], options?: SkillsCommandOptions): void;
9
+ export declare function skillsRemoveCommand(ids?: string[], options?: SkillsCommandOptions): void;
@@ -0,0 +1,28 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import { initSkills, listSkills, addSkills, updateSkills, removeSkills, getKnownAgentIds, resolveAgentSelection, } from '@testspectra/skills';
4
+ function resolveBaseDir(options = {}) {
5
+ return options.cwd ? path.resolve(options.cwd) : process.cwd();
6
+ }
7
+ function resolveAgentIds(options = {}) {
8
+ const { ids, unknown } = resolveAgentSelection(options.agent);
9
+ if (unknown.length > 0) {
10
+ console.log(chalk.yellow(`⚠️ Unknown --agent value(s): ${unknown.join(', ')}. Known ids: ${getKnownAgentIds().join(', ')}`));
11
+ }
12
+ return ids.length > 0 ? ids : undefined;
13
+ }
14
+ export async function skillsInitCommand(options = {}) {
15
+ await initSkills(resolveBaseDir(options));
16
+ }
17
+ export function skillsListCommand(options = {}) {
18
+ listSkills(resolveBaseDir(options), resolveAgentIds(options));
19
+ }
20
+ export async function skillsAddCommand(ids = [], options = {}) {
21
+ await addSkills(ids, resolveBaseDir(options), resolveAgentIds(options));
22
+ }
23
+ export function skillsUpdateCommand(ids = [], options = {}) {
24
+ updateSkills(ids, resolveBaseDir(options), resolveAgentIds(options));
25
+ }
26
+ export function skillsRemoveCommand(ids = [], options = {}) {
27
+ removeSkills(ids, resolveBaseDir(options), resolveAgentIds(options));
28
+ }
@@ -0,0 +1,4 @@
1
+ export interface SyncTypesCommandOptions {
2
+ cwd?: string;
3
+ }
4
+ export declare function syncTypesCommand(options?: SyncTypesCommandOptions): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import { TypeGenerator } from '../types/generator.js';
4
+ export async function syncTypesCommand(options = {}) {
5
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
6
+ try {
7
+ TypeGenerator.generateAll(cwd);
8
+ await TypeGenerator.syncEnvDeclaration(cwd);
9
+ console.log(chalk.green('✔ Successfully synchronized TestSpectra ambient type declarations in .testspectra/types/'));
10
+ }
11
+ catch (err) {
12
+ console.error(chalk.red(`✖ Failed to synchronize ambient declarations: ${err?.message}`));
13
+ process.exit(1);
14
+ }
15
+ }
@@ -0,0 +1,17 @@
1
+ export interface TestCommandOptions {
2
+ cwd?: string;
3
+ headed?: boolean;
4
+ headless?: boolean;
5
+ stepDelay?: string | number;
6
+ delay?: string | number;
7
+ spec?: string;
8
+ output?: string;
9
+ concurrency?: string | number;
10
+ }
11
+ /**
12
+ * `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
13
+ * component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
14
+ * docs/v2/features/component-testing/react/README.md §2 for why these are two independent
15
+ * runners rather than one extended pipeline.
16
+ */
17
+ export declare function testCommand(options?: TestCommandOptions): Promise<void>;
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import chalk from 'chalk';
3
+ import * as p from '@clack/prompts';
4
+ import { runSystemChecks } from './doctor.js';
5
+ /**
6
+ * Finds the Chrome-for-Testing binary that `spectra doctor` already installs/manages for the
7
+ * E2E CDP driver (`core/driver-cdp`) — same install, same binary. `@testspectra/react` must not
8
+ * contain any browser-discovery logic of its own (see the package-level comment in
9
+ * tools/react/vitest.config.ts): this is the one place that resolves the path, passed down to
10
+ * the bundled Vitest/Playwright runner as a plain argument (env var), so component testing never
11
+ * requires a separate `playwright install` download alongside TestSpectra's own browser.
12
+ */
13
+ async function resolveBrowserExecutable(cwd) {
14
+ try {
15
+ const { dependencies } = await runSystemChecks(cwd);
16
+ const chrome = dependencies.find((d) => d.category === 'web' && (d.name.includes('Chrome') || d.name.includes('Chromium')));
17
+ return chrome?.installed && chrome.location ? chrome.location : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
25
+ * component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
26
+ * docs/v2/features/component-testing/react/README.md §2 for why these are two independent
27
+ * runners rather than one extended pipeline.
28
+ */
29
+ export async function testCommand(options = {}) {
30
+ const cwd = options.cwd ?? process.cwd();
31
+ let reactPkg;
32
+ try {
33
+ reactPkg = await import('@testspectra/react');
34
+ }
35
+ catch {
36
+ p.log.error(chalk.red('✖ @testspectra/react is not installed in this workspace.'));
37
+ p.log.info(chalk.dim('Component testing requires the @testspectra/react package.'));
38
+ p.log.info(chalk.dim(' 👉 Install it: pnpm add -D @testspectra/react'));
39
+ process.exit(1);
40
+ return;
41
+ }
42
+ const browserExecutablePath = await resolveBrowserExecutable(cwd);
43
+ if (!browserExecutablePath) {
44
+ p.log.error(chalk.red('✖ TestSpectra-managed Chrome for Testing was not found.'));
45
+ p.log.info(chalk.dim('Component testing reuses the same browser install as `spectra run` (web).'));
46
+ p.log.info(chalk.dim(' 👉 Install it: spectra doctor --fix'));
47
+ process.exit(1);
48
+ return;
49
+ }
50
+ let stepDelayMs;
51
+ const rawDelay = options.stepDelay ?? options.delay;
52
+ if (rawDelay !== undefined) {
53
+ const delayNum = parseInt(String(rawDelay), 10);
54
+ if (!isNaN(delayNum) && delayNum >= 0) {
55
+ stepDelayMs = delayNum;
56
+ }
57
+ }
58
+ let headless = true;
59
+ if (options.headed) {
60
+ headless = false;
61
+ }
62
+ else if (options.headless !== undefined) {
63
+ headless = options.headless;
64
+ }
65
+ const concurrency = options.concurrency !== undefined ? parseInt(String(options.concurrency), 10) : undefined;
66
+ const ok = await reactPkg.runComponentTests({
67
+ cwd,
68
+ headless,
69
+ stepDelayMs,
70
+ spec: options.spec,
71
+ outputFile: options.output ? path.resolve(cwd, options.output) : undefined,
72
+ concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
73
+ browserExecutablePath,
74
+ });
75
+ process.exit(ok ? 0 : 1);
76
+ }