@testspectra/cli 1.1.7 → 1.1.8-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +47 -0
  2. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  3. package/demo-app/assets/index-DOtRypCa.js +154 -0
  4. package/demo-app/index.html +2 -2
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/commands/__tests__/run-e2e.test.js +254 -13
  7. package/dist/commands/__tests__/run.test.js +92 -16
  8. package/dist/commands/add.js +2 -1
  9. package/dist/commands/demo.js +17 -7
  10. package/dist/commands/init.d.ts +1 -1
  11. package/dist/commands/init.js +8 -2
  12. package/dist/commands/lint.d.ts +5 -0
  13. package/dist/commands/lint.js +55 -0
  14. package/dist/commands/refactor.d.ts +2 -1
  15. package/dist/commands/refactor.js +129 -35
  16. package/dist/commands/run.d.ts +2 -3
  17. package/dist/commands/run.js +93 -28
  18. package/dist/commands/skills.d.ts +9 -0
  19. package/dist/commands/skills.js +28 -0
  20. package/dist/commands/sync-types.js +1 -0
  21. package/dist/commands/test.d.ts +17 -0
  22. package/dist/commands/test.js +104 -0
  23. package/dist/commands/watch.js +9 -3
  24. package/dist/config/loader.js +1 -1
  25. package/dist/config/schema.d.ts +5 -114
  26. package/dist/config/schema.js +1 -17
  27. package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
  28. package/dist/generator/__tests__/type-generator.test.js +39 -1
  29. package/dist/generator/tsconfig-generator.js +44 -16
  30. package/dist/generator/type-generator.d.ts +29 -3
  31. package/dist/generator/type-generator.js +115 -22
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +65 -29
  34. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  35. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  36. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  37. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  38. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  39. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  40. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  41. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  42. package/dist/linter/__tests__/step-parity.test.js +372 -0
  43. package/dist/linter/discovery.d.ts +30 -0
  44. package/dist/linter/discovery.js +249 -0
  45. package/dist/linter/extractor.d.ts +67 -0
  46. package/dist/linter/extractor.js +245 -0
  47. package/dist/linter/index.d.ts +7 -0
  48. package/dist/linter/index.js +7 -0
  49. package/dist/linter/parser.d.ts +24 -0
  50. package/dist/linter/parser.js +219 -0
  51. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  52. package/dist/linter/schemas/spec.schema.js +150 -0
  53. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  54. package/dist/linter/schemas/suite.schema.js +83 -0
  55. package/dist/linter/spec-linter.d.ts +23 -0
  56. package/dist/linter/spec-linter.js +630 -0
  57. package/dist/linter/types.d.ts +28 -0
  58. package/dist/linter/types.js +1 -0
  59. package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
  60. package/dist/refactor/refactor-engine.d.ts +25 -0
  61. package/dist/refactor/refactor-engine.js +374 -42
  62. package/dist/reporter/semantic-formatter.js +22 -2
  63. package/dist/reporter/types.d.ts +1 -1
  64. package/dist/runner/bridge.d.ts +1 -1
  65. package/dist/runner/bridge.js +85 -24
  66. package/dist/runner/reporter.d.ts +2 -0
  67. package/dist/runner/reporter.js +11 -3
  68. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  69. package/dist/utils/__tests__/api-server.test.js +62 -0
  70. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  71. package/dist/utils/__tests__/demo-server.test.js +35 -0
  72. package/dist/utils/api-server.d.ts +19 -0
  73. package/dist/utils/api-server.js +179 -0
  74. package/dist/utils/demo-server.d.ts +5 -0
  75. package/dist/utils/demo-server.js +8 -1
  76. package/package.json +13 -4
  77. package/templates/default/CLAUDE.md +5 -0
  78. package/templates/default/package.json +3 -3
  79. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  80. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  81. package/templates/default/page-objects/MatchersPage/mobile.ts +26 -2
  82. package/templates/default/page-objects/MatchersPage/web.ts +8 -2
  83. package/templates/default/page-objects/NavigationPage/mobile.ts +1 -1
  84. package/templates/default/page-objects/NavigationPage/web.ts +1 -1
  85. package/templates/default/page-objects/PlaygroundPage/mobile.ts +12 -6
  86. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -2
  87. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +45 -3
  88. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +7 -0
  89. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +8 -4
  90. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  91. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  92. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +13 -2
  93. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +7 -0
  94. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +6 -4
  95. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  96. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  97. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  98. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  99. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  100. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  101. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  102. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  103. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  104. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  105. package/templates/default/specs/ApiInterception/suite.md +1 -0
  106. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  107. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  108. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  109. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  110. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  111. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  112. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  113. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  114. package/templates/default/specs/Authentication/suite.md +9 -0
  115. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  116. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
  117. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  118. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  119. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  120. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  121. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  122. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
  123. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  124. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
  125. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  126. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  127. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  128. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  129. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  130. package/templates/default/specs/DeepLink/suite.md +9 -0
  131. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  132. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
  133. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  134. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  135. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
  136. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  137. package/templates/default/specs/ElementMatchers/suite.md +2 -1
  138. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  139. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  140. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  141. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  142. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  143. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  144. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  145. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  146. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  147. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
  148. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  149. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  150. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
  151. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  152. package/templates/default/spectra.config.ts +1 -17
  153. package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
  154. package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
  155. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  156. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  157. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  158. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  159. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  160. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  161. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  162. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  163. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  164. package/templates/nx/.nx/workspace-data/d/daemon.log +5803 -168
  165. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  166. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  167. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  168. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  169. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  170. package/templates/nx/.nx/workspace-data/project-graph.json +14388 -0
  171. package/templates/nx/package.json +3 -3
  172. package/templates/nx/packages/matchers/e2e/project.json +2 -2
  173. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
  174. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
  175. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
  176. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
  177. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  178. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  179. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  180. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
  181. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  182. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  183. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  184. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  185. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  186. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  187. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  188. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  189. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  190. package/templates/nx/packages/playground/e2e/project.json +2 -2
  191. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  192. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  193. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  194. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  195. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
  196. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
  197. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  198. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  199. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  200. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
  201. package/templates/nx/shared/testing/project.json +1 -2
  202. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  203. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  204. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  205. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  206. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  207. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  208. package/templates/nx/spectra.config.ts +1 -17
  209. package/templates/react-component/.cursorrules +17 -0
  210. package/templates/react-component/.vscode/extensions.json +5 -0
  211. package/templates/react-component/CLAUDE.md +14 -0
  212. package/templates/react-component/README.md +69 -0
  213. package/templates/react-component/fixtures/component-mock.json +12 -0
  214. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  215. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  216. package/templates/react-component/package.json +27 -0
  217. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  218. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  219. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  220. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  221. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  222. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  223. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  224. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  225. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  226. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  227. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  228. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  229. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  230. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  231. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  232. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  233. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  234. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  235. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  236. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  237. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  238. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  239. package/templates/react-component/spectra.config.ts +27 -0
  240. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  241. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  242. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  243. package/templates/react-component/src/test-utils.tsx +23 -0
  244. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  245. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  246. package/templates/react-component/tsconfig.json +8 -0
  247. package/bin/.testspectra-runner.hash +0 -1
  248. package/bin/testspectra-runner +0 -0
  249. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  250. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  251. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  252. package/dist/commands/cloud-run.d.ts +0 -13
  253. package/dist/commands/cloud-run.js +0 -120
  254. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  255. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  256. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -215,10 +215,15 @@ export async function initCommand(options = {}) {
215
215
  hint: 'Interactive workspace feature discovery & centralized config',
216
216
  },
217
217
  {
218
- label: 'Standard Project',
218
+ label: 'Standard E2E Project',
219
219
  value: 'default',
220
220
  hint: 'Standalone single-suite E2E testing project',
221
221
  },
222
+ {
223
+ label: 'React Component Testing',
224
+ value: 'react-component',
225
+ hint: 'Real-browser React component testing with Vite & Testing Library',
226
+ },
222
227
  ],
223
228
  });
224
229
  if (p.isCancel(templateChoice)) {
@@ -503,6 +508,7 @@ export async function initCommand(options = {}) {
503
508
  copyRecursive(sampleE2eSrc, targetE2eDir, {
504
509
  'playground-e2e': e2eProjectName,
505
510
  'packages/playground/e2e': path.relative(cwd, targetE2eDir).replace(/\\/g, '/'),
511
+ 'testspectra:scope:playground': `testspectra:scope:${featureName}`,
506
512
  'scope:playground': `scope:${featureName}`,
507
513
  });
508
514
  // Adjust project.json
@@ -514,7 +520,7 @@ export async function initCommand(options = {}) {
514
520
  pObj.sourceRoot = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
515
521
  pObj.targets.e2e.options.cwd = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
516
522
  pObj.targets['type-check'].options.cwd = '.';
517
- pObj.tags = [`scope:${featureName}`, 'type:e2e'];
523
+ pObj.tags = ['testspectra:e2e', `testspectra:scope:${featureName}`];
518
524
  fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
519
525
  }
520
526
  catch { }
@@ -0,0 +1,5 @@
1
+ import type { LintOptions } from '../linter/types.js';
2
+ export interface LintCommandOptions extends LintOptions {
3
+ path?: string;
4
+ }
5
+ export declare function lintCommand(targetPath?: string, options?: LintCommandOptions): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import { SpecLinter } from '../linter/spec-linter.js';
4
+ export async function lintCommand(targetPath, options = {}) {
5
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
6
+ try {
7
+ const result = SpecLinter.validateWorkspace(cwd, targetPath);
8
+ if (options.json) {
9
+ console.log(JSON.stringify(result, null, 2));
10
+ if (!result.valid || (options.strict && result.stats.warningCount > 0)) {
11
+ process.exit(1);
12
+ }
13
+ return;
14
+ }
15
+ console.log(chalk.bold('\n🔍 TestSpectra Spec & Code Linter\n'));
16
+ // Group diagnostics by file
17
+ const diagnosticsByFile = new Map();
18
+ for (const diag of result.diagnostics) {
19
+ const relPath = path.relative(cwd, diag.filePath);
20
+ const list = diagnosticsByFile.get(relPath) || [];
21
+ list.push(diag);
22
+ diagnosticsByFile.set(relPath, list);
23
+ }
24
+ if (diagnosticsByFile.size === 0) {
25
+ console.log(chalk.green(` ✓ ${result.stats.totalSpecs} specs and ${result.stats.totalSuites} suites verified.`));
26
+ console.log(chalk.green.bold('\n✓ All spec schemas are valid. 0 lint errors found.\n'));
27
+ return;
28
+ }
29
+ for (const [file, diags] of diagnosticsByFile.entries()) {
30
+ console.log(chalk.bold(` ${file}`));
31
+ for (const diag of diags) {
32
+ const icon = diag.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
33
+ const codeTag = chalk.gray(`[${diag.code}]`);
34
+ const loc = chalk.dim(`:${diag.line}`);
35
+ console.log(` ${icon} ${codeTag} ${diag.message} ${loc}`);
36
+ }
37
+ console.log();
38
+ }
39
+ const { errorCount, warningCount } = result.stats;
40
+ if (errorCount > 0) {
41
+ console.log(chalk.red.bold(`✖ ${errorCount} lint error${errorCount > 1 ? 's' : ''}${warningCount > 0 ? ` and ${warningCount} warning${warningCount > 1 ? 's' : ''}` : ''} found.\n`));
42
+ process.exit(1);
43
+ }
44
+ else if (warningCount > 0) {
45
+ console.log(chalk.yellow.bold(`⚠ ${warningCount} lint warning${warningCount > 1 ? 's' : ''} found.\n`));
46
+ if (options.strict) {
47
+ process.exit(1);
48
+ }
49
+ }
50
+ }
51
+ catch (err) {
52
+ console.error(chalk.red(`\n✖ Fatal error during linting: ${err?.message || err}\n`));
53
+ process.exit(2);
54
+ }
55
+ }
@@ -1,3 +1,4 @@
1
+ import { RefactorResult } from '../refactor/refactor-engine.js';
1
2
  export interface RefactorCommandOptions {
2
3
  cwd?: string;
3
4
  dryRun?: boolean;
@@ -5,4 +6,4 @@ export interface RefactorCommandOptions {
5
6
  from?: string;
6
7
  to?: string;
7
8
  }
8
- export declare function refactorCommand(entityType?: string, oldName?: string, newName?: string, options?: RefactorCommandOptions): Promise<void>;
9
+ export declare function refactorCommand(entityType?: string, oldName?: string, newName?: string, options?: RefactorCommandOptions): Promise<RefactorResult>;
@@ -5,52 +5,142 @@ export async function refactorCommand(entityType, oldName, newName, options = {}
5
5
  let targetType = 'page-object';
6
6
  let fromSymbol = oldName || '';
7
7
  let toSymbol = newName || '';
8
- if (options.from && options.to) {
9
- // Path-based refactoring
10
- if (options.from.includes('page-objects')) {
11
- targetType = 'page-object';
12
- fromSymbol = path.basename(options.from);
13
- toSymbol = path.basename(options.to);
8
+ // 1. Path-based refactoring via --from and --to flags
9
+ if (options.from || options.to) {
10
+ if (!options.from || !options.to) {
11
+ const errorMsg = 'Both --from and --to must be specified for path-based refactoring.';
12
+ const result = {
13
+ status: 'failed',
14
+ entityType: 'page-object',
15
+ oldSymbol: options.from || '',
16
+ newSymbol: options.to || '',
17
+ modifiedFiles: [],
18
+ occurrencesReplaced: 0,
19
+ error: errorMsg,
20
+ };
21
+ if (options.json) {
22
+ console.log(JSON.stringify(result, null, 2));
23
+ }
24
+ else {
25
+ console.error(`\x1b[31m✗ ${errorMsg}\x1b[0m`);
26
+ }
27
+ process.exitCode = 1;
28
+ return result;
14
29
  }
15
- else if (options.from.includes('steps')) {
16
- targetType = 'step';
17
- fromSymbol = path.basename(options.from);
18
- toSymbol = path.basename(options.to);
30
+ const pathResult = await RefactorEngine.renameFromPaths({
31
+ workspaceRoot,
32
+ fromPath: options.from,
33
+ toPath: options.to,
34
+ dryRun: options.dryRun,
35
+ });
36
+ if (options.json) {
37
+ console.log(JSON.stringify(pathResult, null, 2));
38
+ if (pathResult.status === 'failed') {
39
+ process.exitCode = 1;
40
+ }
41
+ return pathResult;
19
42
  }
20
- else if (options.from.includes('actions')) {
21
- targetType = 'action';
22
- fromSymbol = path.basename(options.from);
23
- toSymbol = path.basename(options.to);
43
+ if (pathResult.status === 'success') {
44
+ const modeStr = options.dryRun ? '[Dry-Run] ' : '';
45
+ console.log(`\x1b[32m✓ ${modeStr}Refactored ${pathResult.entityType} "${pathResult.oldSymbol}" → "${pathResult.newSymbol}"\x1b[0m`);
46
+ console.log(` Modified files: ${pathResult.modifiedFiles.length}`);
47
+ console.log(` Occurrences replaced: ${pathResult.occurrencesReplaced}`);
48
+ for (const f of pathResult.modifiedFiles) {
49
+ console.log(` ↳ ${f}`);
50
+ }
24
51
  }
25
- else if (options.from.includes('fixtures')) {
26
- targetType = 'fixture';
27
- fromSymbol = path.parse(options.from).name;
28
- toSymbol = path.parse(options.to).name;
52
+ else {
53
+ console.error(`\x1b[31m✗ Refactoring failed: ${pathResult.error}\x1b[0m`);
54
+ process.exitCode = 1;
29
55
  }
56
+ return pathResult;
30
57
  }
31
- else {
32
- if (!entityType || !oldName || !newName) {
33
- console.error('Usage: spectra refactor <page-object|step|action|fixture> <oldName> <newName>');
34
- process.exit(1);
58
+ // 2. Also support positional path syntax: spectra refactor <fromPath> <toPath>
59
+ if (entityType && oldName && !newName && RefactorEngine.parseEntityFromPath(entityType)) {
60
+ const pathResult = await RefactorEngine.renameFromPaths({
61
+ workspaceRoot,
62
+ fromPath: entityType,
63
+ toPath: oldName,
64
+ dryRun: options.dryRun,
65
+ });
66
+ if (options.json) {
67
+ console.log(JSON.stringify(pathResult, null, 2));
68
+ if (pathResult.status === 'failed') {
69
+ process.exitCode = 1;
70
+ }
71
+ return pathResult;
35
72
  }
36
- const normalized = entityType.toLowerCase().replace(/_/g, '-');
37
- if (['page-object', 'po', 'page'].includes(normalized)) {
38
- targetType = 'page-object';
73
+ if (pathResult.status === 'success') {
74
+ const modeStr = options.dryRun ? '[Dry-Run] ' : '';
75
+ console.log(`\x1b[32m✓ ${modeStr}Refactored ${pathResult.entityType} "${pathResult.oldSymbol}" → "${pathResult.newSymbol}"\x1b[0m`);
76
+ console.log(` Modified files: ${pathResult.modifiedFiles.length}`);
77
+ console.log(` Occurrences replaced: ${pathResult.occurrencesReplaced}`);
78
+ for (const f of pathResult.modifiedFiles) {
79
+ console.log(` ↳ ${f}`);
80
+ }
39
81
  }
40
- else if (['step', 'steps'].includes(normalized)) {
41
- targetType = 'step';
82
+ else {
83
+ console.error(`\x1b[31m✗ Refactoring failed: ${pathResult.error}\x1b[0m`);
84
+ process.exitCode = 1;
42
85
  }
43
- else if (['action', 'actions'].includes(normalized)) {
44
- targetType = 'action';
86
+ return pathResult;
87
+ }
88
+ // 3. Positional syntax: spectra refactor <entityType> <oldName> <newName>
89
+ if (!entityType || !oldName || !newName) {
90
+ const errorMsg = 'Usage: spectra refactor <page-object|step|action|fixture> <oldName> <newName> or spectra refactor --from <path> --to <path>';
91
+ const result = {
92
+ status: 'failed',
93
+ entityType: 'page-object',
94
+ oldSymbol: oldName || '',
95
+ newSymbol: newName || '',
96
+ modifiedFiles: [],
97
+ occurrencesReplaced: 0,
98
+ error: errorMsg,
99
+ };
100
+ if (options.json) {
101
+ console.log(JSON.stringify(result, null, 2));
45
102
  }
46
- else if (['fixture', 'fixtures'].includes(normalized)) {
47
- targetType = 'fixture';
103
+ else {
104
+ console.error(errorMsg);
105
+ }
106
+ process.exitCode = 1;
107
+ return result;
108
+ }
109
+ const normalized = entityType.toLowerCase().replace(/_/g, '-');
110
+ if (['page-object', 'po', 'page', 'pageobject'].includes(normalized)) {
111
+ targetType = 'page-object';
112
+ }
113
+ else if (['step', 'steps'].includes(normalized)) {
114
+ targetType = 'step';
115
+ }
116
+ else if (['action', 'actions'].includes(normalized)) {
117
+ targetType = 'action';
118
+ }
119
+ else if (['fixture', 'fixtures'].includes(normalized)) {
120
+ targetType = 'fixture';
121
+ }
122
+ else {
123
+ const errorMsg = `Unknown refactor entity type: "${entityType}". Valid types: page-object, step, action, fixture.`;
124
+ const result = {
125
+ status: 'failed',
126
+ entityType: 'page-object',
127
+ oldSymbol: oldName,
128
+ newSymbol: newName,
129
+ modifiedFiles: [],
130
+ occurrencesReplaced: 0,
131
+ error: errorMsg,
132
+ };
133
+ if (options.json) {
134
+ console.log(JSON.stringify(result, null, 2));
48
135
  }
49
136
  else {
50
- console.error(`Unknown refactor entity type: "${entityType}". Valid types: page-object, step, action, fixture.`);
51
- process.exit(1);
137
+ console.error(`\x1b[31m✗ ${errorMsg}\x1b[0m`);
52
138
  }
139
+ process.exitCode = 1;
140
+ return result;
53
141
  }
142
+ fromSymbol = oldName;
143
+ toSymbol = newName;
54
144
  const result = await RefactorEngine.renameSymbol({
55
145
  workspaceRoot,
56
146
  type: targetType,
@@ -60,7 +150,10 @@ export async function refactorCommand(entityType, oldName, newName, options = {}
60
150
  });
61
151
  if (options.json) {
62
152
  console.log(JSON.stringify(result, null, 2));
63
- return;
153
+ if (result.status === 'failed') {
154
+ process.exitCode = 1;
155
+ }
156
+ return result;
64
157
  }
65
158
  if (result.status === 'success') {
66
159
  const modeStr = options.dryRun ? '[Dry-Run] ' : '';
@@ -73,6 +166,7 @@ export async function refactorCommand(entityType, oldName, newName, options = {}
73
166
  }
74
167
  else {
75
168
  console.error(`\x1b[31m✗ Refactoring failed: ${result.error}\x1b[0m`);
76
- process.exit(1);
169
+ process.exitCode = 1;
77
170
  }
171
+ return result;
78
172
  }
@@ -12,7 +12,6 @@ export interface RunCommandOptions {
12
12
  workdir?: string;
13
13
  output?: string;
14
14
  demo?: boolean;
15
- baseUrl?: string;
16
15
  failed?: boolean | string;
17
16
  concurrency?: string | number;
18
17
  licenseKey?: string;
@@ -28,5 +27,5 @@ export declare function renderTimingBreakdown(performance: {
28
27
  wallClockDurationMs?: number;
29
28
  totalTests?: number;
30
29
  duration?: string;
31
- }): void;
32
- export declare function runCommand(specPath?: string, options?: RunCommandOptions): Promise<void>;
30
+ }, platform?: string): void;
31
+ export declare function runCommand(specPaths?: string[], options?: RunCommandOptions): Promise<void>;
@@ -5,6 +5,7 @@ import { RustCoreBridge } from '../runner/bridge.js';
5
5
  import { Reporter } from '../runner/reporter.js';
6
6
  import { TypeGenerator } from '../types/generator.js';
7
7
  import { DemoServer } from '../utils/demo-server.js';
8
+ import { DemoApiServer } from '../utils/api-server.js';
8
9
  function renderSummaryCard(options) {
9
10
  const width = 64;
10
11
  const borderColor = options.isPassed ? '\x1b[38;2;52;211;153m' : '\x1b[38;2;248;113;113m';
@@ -46,6 +47,11 @@ function renderSummaryCard(options) {
46
47
  const statusBadge = options.isPassed ? `${green}✓ PASSED${reset}` : `${red}✗ FAILED${reset}`;
47
48
  const passedSuites = options.passedSuites ?? (options.isPassed ? options.suiteCount : 0);
48
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`;
49
55
  console.log(`\n${top}`);
50
56
  console.log(headerRow(`${boldWhite}✨ TESTSPECTRA EXECUTION SUMMARY${reset}`));
51
57
  console.log(mid);
@@ -53,7 +59,7 @@ function renderSummaryCard(options) {
53
59
  console.log(row(`${dim}Suites${reset}`, `${passedSuites > 0 ? `${green}${passedSuites} passed${reset}` : `${passedSuites} passed`}, ` +
54
60
  `${failedSuites > 0 ? `${red}${failedSuites} failed${reset}` : `${failedSuites} failed`} ` +
55
61
  `${dim}(${options.suiteCount} total)${reset}`));
56
- console.log(row(`${dim}Tests${reset}`, `${options.passedCount > 0 ? `${green}${options.passedCount} passed${reset}` : `${options.passedCount} passed`}, ` +
62
+ console.log(row(`${dim}Tests${reset}`, `${testsPassedText}, ` +
57
63
  `${options.failedCount > 0 ? `${red}${options.failedCount} failed${reset}` : `${options.failedCount} failed`} ` +
58
64
  `${dim}(${options.testCount} total)${reset}`));
59
65
  console.log(row(`${dim}Platform${reset}`, `${cyan}${options.platform}${reset}`));
@@ -64,7 +70,13 @@ function renderSummaryCard(options) {
64
70
  }
65
71
  console.log(`${bot}\n`);
66
72
  }
67
- export function renderTimingBreakdown(performance) {
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;
68
80
  const yellowBold = '\x1b[1;38;2;251;191;36m';
69
81
  const reset = '\x1b[0m';
70
82
  const borderColor = '\x1b[38;2;75;85;99m';
@@ -89,20 +101,31 @@ export function renderTimingBreakdown(performance) {
89
101
  console.log(`${borderColor}┌──────────────────────────────────────────┬───────────────────┐${reset}`);
90
102
  console.log(`${borderColor}│${reset} Execution Phase ${borderColor}│${reset} Duration / Detail ${borderColor}│${reset}`);
91
103
  console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
92
- console.log(`${borderColor}│${reset} 1. Master Chromium Process Boot ${borderColor}│${reset} ${pad(performance.masterBootLatencyMs ?? 0, 13)} ms ${borderColor}│${reset}`);
93
- console.log(`${borderColor}│${reset} 2. Worker Spawning & Runtime Overhead ${borderColor}│${reset} ${pad(performance.runnerOverheadMs ?? 0, 13)} ms ${borderColor}│${reset}`);
94
- console.log(`${borderColor}│${reset} 3. Incognito Context & CDP Handshake ${borderColor}│${reset} ${pad(contextHandshakeMs, 10)} ms (avg)${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}`);
95
107
  console.log(`${borderColor}│${reset} ${padRight(testLabel, 40)} ${borderColor}│${reset} ${pad(testDetail, 17)} ${borderColor}│${reset}`);
96
- console.log(`${borderColor}│${reset} 5. Context Teardown / Cleanup ${borderColor}│${reset} ${pad(performance.avgTeardownMs ?? 0, 10)} ms (avg)${borderColor}│${reset}`);
108
+ console.log(`${borderColor}│${reset} ${padRight('5. Context Teardown / Cleanup', 40)} ${borderColor}│${reset} ${pad(performance.avgTeardownMs ?? 0, 10)} ms (avg)${borderColor}│${reset}`);
97
109
  console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
98
110
  console.log(`${borderColor}│${reset} Total Wall-Clock Execution Time ${borderColor}│${reset} ${pad(wallClock, 15)} ${borderColor}│${reset}`);
99
111
  console.log(`${borderColor}└──────────────────────────────────────────┴───────────────────┘${reset}\n`);
100
112
  }
101
- export async function runCommand(specPath, options = {}) {
113
+ export async function runCommand(specPaths, options = {}) {
102
114
  const cwd = process.cwd();
103
- 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;
104
126
  // Ensure latest ambient types are up-to-date
105
127
  TypeGenerator.writeDeclarationFiles(cwd);
128
+ await TypeGenerator.syncEnvDeclaration(cwd);
106
129
  const home = process.env.HOME || process.env.USERPROFILE || '';
107
130
  const globalAppDataPath = path.join(home, '.testspectra');
108
131
  const appDataPath = options.workdir ? path.resolve(options.workdir) : globalAppDataPath;
@@ -135,6 +158,7 @@ export async function runCommand(specPath, options = {}) {
135
158
  }
136
159
  const config = await ConfigLoader.loadConfig(cwd);
137
160
  let demoServer = null;
161
+ let apiServer = null;
138
162
  try {
139
163
  if (options.demo) {
140
164
  demoServer = new DemoServer(5173);
@@ -146,9 +170,14 @@ export async function runCommand(specPath, options = {}) {
146
170
  catch (err) {
147
171
  console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo server: ${err.message}\x1b[0m`);
148
172
  }
149
- }
150
- if (options.baseUrl) {
151
- config.webConfig.baseUrl = options.baseUrl;
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
+ }
152
181
  }
153
182
  if (options.headed) {
154
183
  config.webConfig.headless = false;
@@ -207,7 +236,7 @@ export async function runCommand(specPath, options = {}) {
207
236
  targetDevice,
208
237
  licenseKey: options.licenseKey,
209
238
  outputJsonPath,
210
- spec: effectiveSpecPath,
239
+ specs: effectiveSpecPaths,
211
240
  concurrency: userConcurrency,
212
241
  failedOnly: isFailedMode,
213
242
  failedTarget,
@@ -216,16 +245,41 @@ export async function runCommand(specPath, options = {}) {
216
245
  const relReport = path.relative(cwd, outputJsonPath);
217
246
  const networkReportFile = path.join(path.dirname(outputJsonPath), 'network-resources.json');
218
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;
219
257
  let suiteCount = 1;
220
258
  let passedSuites = isPassed ? 1 : 0;
221
259
  let failedSuites = isPassed ? 0 : 1;
222
- let testCount = (result.passedCount ?? 0) + (result.failedCount ?? 0);
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;
223
278
  if (fs.existsSync(outputJsonPath)) {
224
279
  try {
225
280
  const fullReport = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
226
281
  if (fullReport.summary) {
227
282
  suiteCount = fullReport.summary.totalSuites ?? 1;
228
- testCount = fullReport.summary.totalTests ?? testCount;
229
283
  }
230
284
  if (fullReport.suites && Array.isArray(fullReport.suites)) {
231
285
  failedSuites = fullReport.suites.filter((s) => (typeof s.failed === 'number' && s.failed > 0) ||
@@ -235,16 +289,17 @@ export async function runCommand(specPath, options = {}) {
235
289
  }
236
290
  if (fullReport.performance) {
237
291
  renderSummaryCard({
238
- isPassed,
239
- statusText: isPassed ? 'PASSED' : 'FAILED',
240
- duration: result.duration,
292
+ isPassed: overallPassed,
293
+ statusText: overallPassed ? 'PASSED' : 'FAILED',
294
+ duration: finalDuration,
241
295
  platform,
242
296
  suiteCount,
243
297
  passedSuites,
244
298
  failedSuites,
245
- testCount,
246
- passedCount: result.passedCount ?? (isPassed ? testCount : 0),
247
- failedCount: result.failedCount ?? (isPassed ? 0 : testCount),
299
+ testCount: totalTests,
300
+ passedCount: totalPassed,
301
+ failedCount: totalFailed,
302
+ carriedPassed,
248
303
  reportPath: relReport,
249
304
  networkReportPath: relNetworkReport,
250
305
  });
@@ -252,8 +307,11 @@ export async function runCommand(specPath, options = {}) {
252
307
  ...fullReport.performance,
253
308
  wallClockDurationMs: fullReport.wallClockDurationMs,
254
309
  duration: fullReport.duration,
255
- totalTests: fullReport.summary?.totalTests ?? testCount,
256
- });
310
+ totalTests: fullReport.summary?.totalTests ?? totalTests,
311
+ }, platform);
312
+ if (!overallPassed || totalFailed > 0) {
313
+ process.exitCode = 1;
314
+ }
257
315
  return;
258
316
  }
259
317
  }
@@ -262,19 +320,23 @@ export async function runCommand(specPath, options = {}) {
262
320
  }
263
321
  }
264
322
  renderSummaryCard({
265
- isPassed,
266
- statusText: isPassed ? 'PASSED' : 'FAILED',
267
- duration: result.duration,
323
+ isPassed: overallPassed,
324
+ statusText: overallPassed ? 'PASSED' : 'FAILED',
325
+ duration: finalDuration,
268
326
  platform,
269
327
  suiteCount,
270
328
  passedSuites,
271
329
  failedSuites,
272
- testCount,
273
- passedCount: result.passedCount ?? (isPassed ? testCount : 0),
274
- failedCount: result.failedCount ?? (isPassed ? 0 : testCount),
330
+ testCount: totalTests,
331
+ passedCount: totalPassed,
332
+ failedCount: totalFailed,
333
+ carriedPassed,
275
334
  reportPath: relReport,
276
335
  networkReportPath: relNetworkReport,
277
336
  });
337
+ if (!overallPassed || totalFailed > 0) {
338
+ process.exitCode = 1;
339
+ }
278
340
  }
279
341
  catch (e) {
280
342
  console.error(`\x1b[31m[TestSpectra] Execution error:\x1b[0m`, e);
@@ -284,5 +346,8 @@ export async function runCommand(specPath, options = {}) {
284
346
  if (demoServer) {
285
347
  await demoServer.stop();
286
348
  }
349
+ if (apiServer) {
350
+ await apiServer.stop();
351
+ }
287
352
  }
288
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
+ }
@@ -5,6 +5,7 @@ export async function syncTypesCommand(options = {}) {
5
5
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
6
6
  try {
7
7
  TypeGenerator.generateAll(cwd);
8
+ await TypeGenerator.syncEnvDeclaration(cwd);
8
9
  console.log(chalk.green('✔ Successfully synchronized TestSpectra ambient type declarations in .testspectra/types/'));
9
10
  }
10
11
  catch (err) {
@@ -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>;