@testspectra/cli 1.0.69 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +36 -270
  3. package/bin/.testspectra-runner.hash +1 -0
  4. package/bin/spectra.js +1 -1
  5. package/bin/testspectra-runner +0 -0
  6. package/demo-app/assets/index-DPfv5_EF.js +116 -0
  7. package/demo-app/assets/index-gIwu2EMV.css +1 -0
  8. package/demo-app/index.html +2 -2
  9. package/dist/.tsbuildinfo +1 -0
  10. package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
  11. package/dist/commands/__tests__/cloud-run.test.js +93 -0
  12. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  13. package/dist/commands/__tests__/doctor.test.js +39 -0
  14. package/dist/commands/__tests__/init.test.d.ts +1 -0
  15. package/dist/commands/__tests__/init.test.js +148 -0
  16. package/dist/commands/__tests__/license.test.d.ts +1 -0
  17. package/dist/commands/__tests__/license.test.js +13 -0
  18. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  19. package/dist/commands/__tests__/refactor.test.js +64 -0
  20. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  21. package/dist/commands/__tests__/run-e2e.test.js +181 -0
  22. package/dist/commands/__tests__/run.test.d.ts +1 -0
  23. package/dist/commands/__tests__/run.test.js +197 -0
  24. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  25. package/dist/commands/__tests__/watch.test.js +155 -0
  26. package/dist/commands/add.js +86 -85
  27. package/dist/commands/cloud-run.d.ts +11 -0
  28. package/dist/commands/cloud-run.js +118 -0
  29. package/dist/commands/demo.js +7 -7
  30. package/dist/commands/devices.d.ts +14 -2
  31. package/dist/commands/devices.js +44 -34
  32. package/dist/commands/doctor.d.ts +3 -1
  33. package/dist/commands/doctor.js +43 -264
  34. package/dist/commands/init.d.ts +1 -1
  35. package/dist/commands/init.js +206 -196
  36. package/dist/commands/install.d.ts +4 -0
  37. package/dist/commands/install.js +59 -0
  38. package/dist/commands/license.d.ts +4 -0
  39. package/dist/commands/license.js +30 -0
  40. package/dist/commands/refactor.d.ts +8 -0
  41. package/dist/commands/refactor.js +78 -0
  42. package/dist/commands/run.d.ts +22 -1
  43. package/dist/commands/run.js +252 -109
  44. package/dist/commands/sync-types.d.ts +4 -0
  45. package/dist/commands/sync-types.js +14 -0
  46. package/dist/commands/update.d.ts +3 -0
  47. package/dist/commands/update.js +109 -0
  48. package/dist/commands/watch.d.ts +6 -0
  49. package/dist/commands/watch.js +47 -43
  50. package/dist/config/loader.d.ts +1 -1
  51. package/dist/config/loader.js +40 -25
  52. package/dist/config/schema.d.ts +117 -49
  53. package/dist/config/schema.js +41 -34
  54. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  55. package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
  56. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  57. package/dist/generator/__tests__/type-generator.test.js +386 -0
  58. package/dist/generator/index.d.ts +2 -0
  59. package/dist/generator/index.js +2 -0
  60. package/dist/generator/tsconfig-generator.d.ts +27 -0
  61. package/dist/generator/tsconfig-generator.js +391 -0
  62. package/dist/generator/type-generator.d.ts +36 -0
  63. package/dist/generator/type-generator.js +644 -0
  64. package/dist/index.d.ts +9 -5
  65. package/dist/index.js +126 -47
  66. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  67. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  68. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  69. package/dist/lifecycle/hook-discovery.js +60 -0
  70. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  71. package/dist/lifecycle/hook-dispatcher.js +190 -0
  72. package/dist/lifecycle/index.d.ts +3 -0
  73. package/dist/lifecycle/index.js +3 -0
  74. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  75. package/dist/lifecycle/parallel-grouping.js +151 -0
  76. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  77. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  78. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  79. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  80. package/dist/mobile/device-pool.d.ts +23 -0
  81. package/dist/mobile/device-pool.js +1 -0
  82. package/dist/mobile/index.d.ts +1 -0
  83. package/dist/mobile/index.js +1 -0
  84. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  85. package/dist/refactor/__tests__/refactor-engine.test.js +175 -0
  86. package/dist/refactor/index.d.ts +1 -0
  87. package/dist/refactor/index.js +1 -0
  88. package/dist/refactor/refactor-engine.d.ts +25 -0
  89. package/dist/refactor/refactor-engine.js +255 -0
  90. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  91. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  92. package/dist/reporter/semantic-formatter.d.ts +6 -0
  93. package/dist/reporter/semantic-formatter.js +191 -0
  94. package/dist/reporter/summary-view.d.ts +12 -0
  95. package/dist/reporter/summary-view.js +19 -0
  96. package/dist/reporter/terminal-stream.d.ts +32 -0
  97. package/dist/reporter/terminal-stream.js +82 -0
  98. package/dist/reporter/types.d.ts +2 -0
  99. package/dist/reporter/types.js +1 -0
  100. package/dist/runner/bridge.d.ts +10 -5
  101. package/dist/runner/bridge.js +69 -44
  102. package/dist/runner/reporter.d.ts +30 -2
  103. package/dist/runner/reporter.js +419 -13
  104. package/dist/server/demo.js +43 -44
  105. package/dist/types/generator.d.ts +1 -17
  106. package/dist/types/generator.js +1 -443
  107. package/dist/utils/demo-server.js +38 -35
  108. package/package.json +20 -12
  109. package/templates/default/CLAUDE.md +7 -5
  110. package/templates/default/fixtures/apiData.json +24 -0
  111. package/templates/default/package.json +13 -4
  112. package/templates/default/page-objects/MatchersPage/mobile.ts +41 -16
  113. package/templates/default/page-objects/MatchersPage/web.ts +14 -0
  114. package/templates/default/page-objects/NavigationPage/mobile.ts +10 -1
  115. package/templates/default/page-objects/PlaygroundPage/mobile.ts +27 -17
  116. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
  117. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +11 -0
  118. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  119. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  120. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +16 -0
  121. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  122. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  123. package/templates/default/specs/ApiInterception/suite.md +9 -0
  124. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  125. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  126. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +15 -1
  127. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  128. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  129. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  130. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  131. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  132. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  133. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  134. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  135. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  136. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  137. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  138. package/templates/default/spectra.config.ts +21 -14
  139. package/templates/default/support/actions/fillCredentials/mobile.action.ts +2 -2
  140. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +2 -0
  141. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  142. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +3 -1
  143. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  144. package/templates/default/tsconfig.json +3 -1
  145. package/templates/nx/.nx/workspace-data/d/daemon.log +168 -0
  146. package/templates/nx/CLAUDE.md +2 -2
  147. package/templates/nx/package.json +7 -5
  148. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +3 -0
  149. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  150. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  151. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +3 -0
  152. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  153. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  154. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  155. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  156. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  157. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  158. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  159. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  160. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  161. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  162. package/templates/nx/pnpm-lock.yaml +6663 -0
  163. package/templates/nx/pnpm-workspace.yaml +2 -1
  164. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  165. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +12 -0
  166. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +14 -0
  167. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
  168. package/templates/nx/shared/testing/project.json +19 -0
  169. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  170. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  171. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  172. package/templates/nx/spectra.config.ts +19 -12
  173. package/templates/nx/tsconfig.json +3 -1
  174. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  175. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  176. package/dist/index.cjs +0 -101
  177. package/dist/plugin.cjs +0 -575
  178. package/dist/plugin.d.ts +0 -16
  179. package/dist/plugin.js +0 -184
  180. package/dist/step/index.d.ts +0 -6
  181. package/dist/step/index.js +0 -6
  182. package/dist/step/matchers.d.ts +0 -14
  183. package/dist/step/matchers.js +0 -114
  184. package/dist/step/proto.d.ts +0 -46
  185. package/dist/step/proto.js +0 -114
  186. package/dist/step/runner/collection.d.ts +0 -13
  187. package/dist/step/runner/collection.js +0 -51
  188. package/dist/step/runner/single.d.ts +0 -21
  189. package/dist/step/runner/single.js +0 -138
  190. package/dist/step/spectra.d.ts +0 -34
  191. package/dist/step/spectra.js +0 -156
  192. package/dist/step/types.d.ts +0 -32
  193. package/dist/step/types.js +0 -5
  194. package/templates/default/tsconfig.spectra.android.json +0 -46
  195. package/templates/default/tsconfig.spectra.ios.json +0 -46
  196. package/templates/default/tsconfig.spectra.json +0 -22
  197. package/templates/default/tsconfig.spectra.web.json +0 -53
  198. package/templates/nx/fixtures/playgroundData.json +0 -7
  199. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  200. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  201. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  202. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  203. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  204. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  205. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  206. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  207. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  208. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  209. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  210. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  211. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  212. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  213. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  214. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  215. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  216. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  217. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  218. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  219. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  220. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  221. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  222. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  223. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  224. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  225. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  226. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  227. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  228. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  229. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  230. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  231. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  232. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  233. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  234. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  235. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  236. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  237. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  238. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  239. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  240. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  241. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  242. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  243. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  244. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  245. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  246. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  247. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  248. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  249. package/templates/nx/specs/Playground/suite.md +0 -9
  250. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  251. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  252. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  253. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  254. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  255. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  256. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  257. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  258. package/templates/nx/tsconfig.spectra.android.json +0 -39
  259. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  260. package/templates/nx/tsconfig.spectra.json +0 -25
  261. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  262. package/templates/nx/tsconfig.spectra.web.json +0 -45
  263. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  264. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  265. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  266. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  267. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  268. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  269. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  270. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  271. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  272. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  273. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  274. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  275. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  276. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  277. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  278. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -0,0 +1,4 @@
1
+ export declare function installCommand(item?: string, options?: {
2
+ force?: boolean;
3
+ cwd?: string;
4
+ }): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { spawn } from 'child_process';
4
+ import { RustCoreBridge } from '../runner/bridge.js';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ export async function installCommand(item, options = {}) {
8
+ const cwd = options.cwd || process.cwd();
9
+ const binPath = RustCoreBridge.resolveBinaryPath();
10
+ const home = process.env.HOME || process.env.USERPROFILE || '';
11
+ const globalDriversDir = path.join(home, '.testspectra', 'drivers');
12
+ const runnerEnv = {
13
+ ...process.env,
14
+ TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
15
+ };
16
+ const args = ['install'];
17
+ if (item) {
18
+ args.push(item);
19
+ }
20
+ return new Promise((resolve) => {
21
+ let child;
22
+ if (binPath === 'cargo') {
23
+ const workspaceRoot = path.resolve(__dirname, '../../..');
24
+ const cargoArgs = [
25
+ 'run',
26
+ '--manifest-path',
27
+ 'core/test-runner/Cargo.toml',
28
+ '--bin',
29
+ 'testspectra-runner',
30
+ '--',
31
+ ...args,
32
+ ];
33
+ child = spawn('cargo', cargoArgs, {
34
+ cwd: workspaceRoot,
35
+ env: runnerEnv,
36
+ stdio: 'inherit',
37
+ });
38
+ }
39
+ else {
40
+ child = spawn(binPath, args, {
41
+ cwd,
42
+ env: runnerEnv,
43
+ stdio: 'inherit',
44
+ });
45
+ }
46
+ child.on('close', (code) => {
47
+ if (code === 0) {
48
+ resolve();
49
+ }
50
+ else {
51
+ process.exit(code ?? 1);
52
+ }
53
+ });
54
+ child.on('error', (err) => {
55
+ console.error(`[TestSpectra Install Error] ${err.message}`);
56
+ process.exit(1);
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,4 @@
1
+ export interface LicenseCommandOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function licenseCommand(action?: string, key?: string, options?: LicenseCommandOptions): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { spawn } from 'child_process';
2
+ import { RustCoreBridge } from '../runner/bridge.js';
3
+ export async function licenseCommand(action = 'status', key, options = {}) {
4
+ const binPath = RustCoreBridge.resolveBinaryPath();
5
+ const normalizedAction = action.toLowerCase().trim();
6
+ const args = ['license', normalizedAction];
7
+ if (key && normalizedAction === 'activate') {
8
+ args.push(key.trim());
9
+ }
10
+ if (options.json) {
11
+ args.push('--json');
12
+ }
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn(binPath, args, {
15
+ stdio: 'inherit',
16
+ env: process.env,
17
+ });
18
+ child.on('close', (code) => {
19
+ if (code === 0) {
20
+ resolve();
21
+ }
22
+ else {
23
+ process.exit(code ?? 1);
24
+ }
25
+ });
26
+ child.on('error', (err) => {
27
+ reject(err);
28
+ });
29
+ });
30
+ }
@@ -0,0 +1,8 @@
1
+ export interface RefactorCommandOptions {
2
+ cwd?: string;
3
+ dryRun?: boolean;
4
+ json?: boolean;
5
+ from?: string;
6
+ to?: string;
7
+ }
8
+ export declare function refactorCommand(entityType?: string, oldName?: string, newName?: string, options?: RefactorCommandOptions): Promise<void>;
@@ -0,0 +1,78 @@
1
+ import path from 'path';
2
+ import { RefactorEngine } from '../refactor/refactor-engine.js';
3
+ export async function refactorCommand(entityType, oldName, newName, options = {}) {
4
+ const workspaceRoot = options.cwd ? path.resolve(options.cwd) : process.cwd();
5
+ let targetType = 'page-object';
6
+ let fromSymbol = oldName || '';
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);
14
+ }
15
+ else if (options.from.includes('steps')) {
16
+ targetType = 'step';
17
+ fromSymbol = path.basename(options.from);
18
+ toSymbol = path.basename(options.to);
19
+ }
20
+ else if (options.from.includes('actions')) {
21
+ targetType = 'action';
22
+ fromSymbol = path.basename(options.from);
23
+ toSymbol = path.basename(options.to);
24
+ }
25
+ else if (options.from.includes('fixtures')) {
26
+ targetType = 'fixture';
27
+ fromSymbol = path.parse(options.from).name;
28
+ toSymbol = path.parse(options.to).name;
29
+ }
30
+ }
31
+ else {
32
+ if (!entityType || !oldName || !newName) {
33
+ console.error('Usage: spectra refactor <page-object|step|action|fixture> <oldName> <newName>');
34
+ process.exit(1);
35
+ }
36
+ const normalized = entityType.toLowerCase().replace(/_/g, '-');
37
+ if (['page-object', 'po', 'page'].includes(normalized)) {
38
+ targetType = 'page-object';
39
+ }
40
+ else if (['step', 'steps'].includes(normalized)) {
41
+ targetType = 'step';
42
+ }
43
+ else if (['action', 'actions'].includes(normalized)) {
44
+ targetType = 'action';
45
+ }
46
+ else if (['fixture', 'fixtures'].includes(normalized)) {
47
+ targetType = 'fixture';
48
+ }
49
+ else {
50
+ console.error(`Unknown refactor entity type: "${entityType}". Valid types: page-object, step, action, fixture.`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+ const result = await RefactorEngine.renameSymbol({
55
+ workspaceRoot,
56
+ type: targetType,
57
+ oldName: fromSymbol,
58
+ newName: toSymbol,
59
+ dryRun: options.dryRun,
60
+ });
61
+ if (options.json) {
62
+ console.log(JSON.stringify(result, null, 2));
63
+ return;
64
+ }
65
+ if (result.status === 'success') {
66
+ const modeStr = options.dryRun ? '[Dry-Run] ' : '';
67
+ console.log(`\x1b[32m✓ ${modeStr}Refactored ${result.entityType} "${result.oldSymbol}" → "${result.newSymbol}"\x1b[0m`);
68
+ console.log(` Modified files: ${result.modifiedFiles.length}`);
69
+ console.log(` Occurrences replaced: ${result.occurrencesReplaced}`);
70
+ for (const f of result.modifiedFiles) {
71
+ console.log(` ↳ ${f}`);
72
+ }
73
+ }
74
+ else {
75
+ console.error(`\x1b[31m✗ Refactoring failed: ${result.error}\x1b[0m`);
76
+ process.exit(1);
77
+ }
78
+ }
@@ -1,10 +1,31 @@
1
1
  export interface RunCommandOptions {
2
2
  spec?: string;
3
- target?: "web" | "android" | "ios" | "common";
3
+ target?: 'web' | 'android' | 'ios' | 'common' | 'mobile';
4
4
  device?: string;
5
+ devices?: string;
6
+ allDevices?: boolean;
7
+ multiDevice?: boolean;
5
8
  headless?: boolean;
9
+ headed?: boolean;
10
+ stepDelay?: string | number;
11
+ delay?: string | number;
6
12
  workdir?: string;
7
13
  output?: string;
8
14
  demo?: boolean;
15
+ failed?: boolean | string;
16
+ concurrency?: string | number;
17
+ licenseKey?: string;
9
18
  }
19
+ export declare function renderTimingBreakdown(performance: {
20
+ masterBootLatencyMs?: number;
21
+ avgContextCreationMs?: number;
22
+ avgCdpHandshakeMs?: number;
23
+ avgTestExecutionMs?: number;
24
+ totalTestExecutionMs?: number;
25
+ runnerOverheadMs?: number;
26
+ avgTeardownMs?: number;
27
+ wallClockDurationMs?: number;
28
+ totalTests?: number;
29
+ duration?: string;
30
+ }): void;
10
31
  export declare function runCommand(specPath?: string, options?: RunCommandOptions): Promise<void>;
@@ -1,134 +1,277 @@
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";
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
+ function renderSummaryCard(options) {
9
+ const width = 64;
10
+ const borderColor = options.isPassed ? '\x1b[38;2;52;211;153m' : '\x1b[38;2;248;113;113m';
11
+ const reset = '\x1b[0m';
12
+ const boldWhite = '\x1b[1;37m';
13
+ const dim = '\x1b[90m';
14
+ const cyan = '\x1b[38;2;45;212;191m';
15
+ const yellow = '\x1b[38;2;251;191;36m';
16
+ const green = '\x1b[38;2;52;211;153m';
17
+ const red = '\x1b[38;2;248;113;113m';
18
+ const strip = (s) => s.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
19
+ const visualWidth = (s) => {
20
+ const plain = strip(s);
21
+ let w = 0;
22
+ for (const ch of plain) {
23
+ const code = ch.codePointAt(0) || 0;
24
+ if (code >= 0x1f300 || (code >= 0x2600 && code <= 0x27bf)) {
25
+ w += 2;
26
+ }
27
+ else {
28
+ w += 1;
29
+ }
30
+ }
31
+ return w;
32
+ };
33
+ const row = (label, value) => {
34
+ const rawLen = visualWidth(label) + visualWidth(value);
35
+ const padding = Math.max(1, width - rawLen);
36
+ return ` ${borderColor}│${reset} ${label}${' '.repeat(padding)}${value} ${borderColor}│${reset}`;
37
+ };
38
+ const headerRow = (title) => {
39
+ const rawLen = visualWidth(title);
40
+ const padding = Math.max(1, width - rawLen);
41
+ return ` ${borderColor}│${reset} ${title}${' '.repeat(padding)} ${borderColor}│${reset}`;
42
+ };
43
+ const top = ` ${borderColor}╭${'─'.repeat(width + 4)}╮${reset}`;
44
+ const mid = ` ${borderColor}├${'─'.repeat(width + 4)}┤${reset}`;
45
+ const bot = ` ${borderColor}╰${'─'.repeat(width + 4)}╯${reset}`;
46
+ const statusBadge = options.isPassed ? `${green}✓ PASSED${reset}` : `${red}✗ FAILED${reset}`;
47
+ const passedSuites = options.passedSuites ?? (options.isPassed ? options.suiteCount : 0);
48
+ const failedSuites = options.failedSuites ?? (options.isPassed ? 0 : options.suiteCount);
49
+ console.log(`\n${top}`);
50
+ console.log(headerRow(`${boldWhite}✨ TESTSPECTRA EXECUTION SUMMARY${reset}`));
51
+ console.log(mid);
52
+ console.log(row(`${dim}Status${reset}`, statusBadge));
53
+ console.log(row(`${dim}Suites${reset}`, `${passedSuites > 0 ? `${green}${passedSuites} passed${reset}` : `${passedSuites} passed`}, ` +
54
+ `${failedSuites > 0 ? `${red}${failedSuites} failed${reset}` : `${failedSuites} failed`} ` +
55
+ `${dim}(${options.suiteCount} total)${reset}`));
56
+ console.log(row(`${dim}Tests${reset}`, `${options.passedCount > 0 ? `${green}${options.passedCount} passed${reset}` : `${options.passedCount} passed`}, ` +
57
+ `${options.failedCount > 0 ? `${red}${options.failedCount} failed${reset}` : `${options.failedCount} failed`} ` +
58
+ `${dim}(${options.testCount} total)${reset}`));
59
+ console.log(row(`${dim}Platform${reset}`, `${cyan}${options.platform}${reset}`));
60
+ console.log(row(`${dim}Duration${reset}`, `${yellow}${options.duration}${reset}`));
61
+ console.log(row(`${dim}Report${reset}`, `${dim}${options.reportPath}${reset}`));
62
+ if (options.networkReportPath) {
63
+ console.log(row(`${dim}Network${reset}`, `${dim}${options.networkReportPath}${reset}`));
64
+ }
65
+ console.log(`${bot}\n`);
66
+ }
67
+ export function renderTimingBreakdown(performance) {
68
+ const yellowBold = '\x1b[1;38;2;251;191;36m';
69
+ const reset = '\x1b[0m';
70
+ const borderColor = '\x1b[38;2;75;85;99m';
71
+ const pad = (str, width) => {
72
+ const s = String(str);
73
+ return ' '.repeat(Math.max(0, width - s.length)) + s;
74
+ };
75
+ const padRight = (str, width) => {
76
+ const s = String(str);
77
+ return s + ' '.repeat(Math.max(0, width - s.length));
78
+ };
79
+ const wallClock = performance.duration
80
+ ? performance.duration
81
+ : `${((performance.wallClockDurationMs ?? 0) / 1000).toFixed(2)}s`;
82
+ const totalTests = performance.totalTests ?? 1;
83
+ const totalTestExecMs = performance.totalTestExecutionMs ?? (performance.avgTestExecutionMs ?? 0) * totalTests;
84
+ const avgTestExecMs = performance.avgTestExecutionMs ?? (totalTests > 0 ? Math.round(totalTestExecMs / totalTests) : 0);
85
+ const contextHandshakeMs = (performance.avgContextCreationMs ?? 0) + (performance.avgCdpHandshakeMs ?? 0);
86
+ const testLabel = totalTests === 1 ? '4. Total Test Execution (1 test)' : `4. Total Test Execution (${totalTests} tests)`;
87
+ const testDetail = totalTests <= 1 ? `${pad(totalTestExecMs, 13)} ms` : `${pad(totalTestExecMs, 5)} ms (${pad(avgTestExecMs, 3)}ms/t)`;
88
+ console.log(`⏱️ ${yellowBold}EXECUTION TIMING BREAKDOWN (PHASE-BY-PHASE)${reset}`);
89
+ console.log(`${borderColor}┌──────────────────────────────────────────┬───────────────────┐${reset}`);
90
+ console.log(`${borderColor}│${reset} Execution Phase ${borderColor}│${reset} Duration / Detail ${borderColor}│${reset}`);
91
+ 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}`);
95
+ 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}`);
97
+ console.log(`${borderColor}├──────────────────────────────────────────┼───────────────────┤${reset}`);
98
+ console.log(`${borderColor}│${reset} Total Wall-Clock Execution Time ${borderColor}│${reset} ${pad(wallClock, 15)} ${borderColor}│${reset}`);
99
+ console.log(`${borderColor}└──────────────────────────────────────────┴───────────────────┘${reset}\n`);
100
+ }
8
101
  export async function runCommand(specPath, options = {}) {
9
102
  const cwd = process.cwd();
10
103
  const effectiveSpecPath = specPath || options.spec;
11
104
  // Ensure latest ambient types are up-to-date
12
105
  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
- }
106
+ const home = process.env.HOME || process.env.USERPROFILE || '';
107
+ const globalAppDataPath = path.join(home, '.testspectra');
108
+ const appDataPath = options.workdir ? path.resolve(options.workdir) : globalAppDataPath;
109
+ if (!fs.existsSync(appDataPath)) {
110
+ fs.mkdirSync(appDataPath, { recursive: true });
25
111
  }
26
- if (options.headless !== undefined) {
27
- config.webConfig.headlessMode = options.headless;
112
+ const projectAppData = path.join(cwd, '.testspectra');
113
+ const outputJsonPath = options.output
114
+ ? path.resolve(options.output)
115
+ : path.join(projectAppData, 'reports', 'result.json');
116
+ const isFailedMode = options.failed !== undefined;
117
+ let failedTarget;
118
+ if (typeof options.failed === 'string' && options.failed !== 'true') {
119
+ failedTarget = options.failed;
28
120
  }
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;
121
+ if (isFailedMode) {
122
+ if (fs.existsSync(outputJsonPath)) {
123
+ try {
124
+ const prev = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
125
+ const failedItems = (prev.suites || []).flatMap((s) => (s.tests || []).filter((t) => t.passed === false || t.status === 'failed'));
126
+ if (failedItems.length === 0) {
127
+ console.log(`\x1b[32m✨ [TestSpectra] No failed tests found in previous test session! Everything is already passing.\x1b[0m\n`);
128
+ return;
129
+ }
130
+ const targetDesc = failedTarget ? `target '${failedTarget}' in` : '';
131
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m 🔄 Re-running ${targetDesc} ${failedItems.length} failed test(s) with session state carry-over...`);
132
+ }
133
+ catch { }
37
134
  }
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
135
  }
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];
136
+ const config = await ConfigLoader.loadConfig(cwd);
137
+ let demoServer = null;
138
+ try {
139
+ if (options.demo) {
140
+ demoServer = new DemoServer(5173);
141
+ try {
142
+ const { url } = await demoServer.start();
143
+ config.webConfig.baseUrl = url;
144
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m 🌐 Demo Web App automatically started at: \x1b[33m${url}\x1b[0m`);
145
+ }
146
+ catch (err) {
147
+ console.warn(`\x1b[33m[TestSpectra] Warning: Could not start built-in demo server: ${err.message}\x1b[0m`);
148
+ }
53
149
  }
54
- else {
55
- const parsed = path.parse(normalized);
56
- suiteName = parsed.name.replace(/\.(web|android|ios|common|test|spec)/g, "") || "default";
150
+ if (options.headed) {
151
+ config.webConfig.headless = false;
57
152
  }
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
- }
153
+ else if (options.headless !== undefined) {
154
+ config.webConfig.headless = options.headless;
155
+ }
156
+ const rawDelay = options.stepDelay ?? options.delay;
157
+ if (rawDelay !== undefined) {
158
+ const delayNum = parseInt(String(rawDelay), 10);
159
+ if (!isNaN(delayNum) && delayNum >= 0) {
160
+ config.webConfig.stepDelay = delayNum;
161
+ config.androidConfig.stepDelay = delayNum;
162
+ config.iosConfig.stepDelay = delayNum;
86
163
  }
87
164
  }
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
- }
165
+ const platform = options.target || 'web';
166
+ const userConcurrency = options.concurrency ? parseInt(String(options.concurrency), 10) : undefined;
167
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m Starting execution on target \x1b[1m${platform}\x1b[0m...`);
168
+ if (options.devices || options.device) {
169
+ const rawDevices = (options.devices || options.device);
170
+ const devIds = rawDevices.includes(',')
171
+ ? rawDevices
172
+ .split(',')
173
+ .map((s) => s.trim())
174
+ .filter(Boolean)
175
+ : [rawDevices.trim()].filter(Boolean);
176
+ const mappedDevices = devIds.map((id) => ({ id }));
177
+ if (platform === 'ios') {
178
+ config.iosConfig.devices = mappedDevices;
179
+ }
180
+ else {
181
+ config.androidConfig.devices = mappedDevices;
103
182
  }
183
+ const count = devIds.length;
184
+ const desc = devIds.join(', ');
185
+ console.log(`\x1b[36m[TestSpectra Grid]\x1b[0m Mobile device target configured (${count} device(s)): ${desc}`);
104
186
  }
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 {
187
+ else if (options.allDevices || options.multiDevice) {
188
+ if (platform === 'ios') {
189
+ config.iosConfig.devices = 'auto';
190
+ }
191
+ else {
192
+ config.androidConfig.devices = 'auto';
193
+ }
194
+ console.log('\x1b[36m[TestSpectra Grid]\x1b[0m Multi-device grid enabled (auto-detecting connected devices)');
195
+ }
196
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m Base URL: \x1b[33m${config.webConfig.baseUrl}\x1b[0m\n`);
197
+ const reporter = new Reporter();
198
+ const targetDevice = options.devices || options.device || (options.allDevices || options.multiDevice ? 'auto' : undefined);
121
199
  const result = await RustCoreBridge.run({
122
200
  baseDir: cwd,
123
201
  appDataPath,
124
202
  platform,
125
- suite: suiteName,
126
- testCases,
127
203
  config,
128
- targetDevice: options.device,
204
+ targetDevice,
205
+ licenseKey: options.licenseKey,
129
206
  outputJsonPath,
207
+ spec: effectiveSpecPath,
208
+ concurrency: userConcurrency,
209
+ failedOnly: isFailedMode,
210
+ failedTarget,
130
211
  }, reporter);
131
- console.log(`\n\x1b[36m[TestSpectra]\x1b[0m Run finished: \x1b[1m${result.status.toUpperCase()}\x1b[0m in ${result.duration}`);
212
+ const isPassed = result.status.toLowerCase() === 'passed';
213
+ const relReport = path.relative(cwd, outputJsonPath);
214
+ const networkReportFile = path.join(path.dirname(outputJsonPath), 'network-resources.json');
215
+ const relNetworkReport = fs.existsSync(networkReportFile) ? path.relative(cwd, networkReportFile) : undefined;
216
+ let suiteCount = 1;
217
+ let passedSuites = isPassed ? 1 : 0;
218
+ let failedSuites = isPassed ? 0 : 1;
219
+ let testCount = (result.passedCount ?? 0) + (result.failedCount ?? 0);
220
+ if (fs.existsSync(outputJsonPath)) {
221
+ try {
222
+ const fullReport = JSON.parse(fs.readFileSync(outputJsonPath, 'utf8'));
223
+ if (fullReport.summary) {
224
+ suiteCount = fullReport.summary.totalSuites ?? 1;
225
+ testCount = fullReport.summary.totalTests ?? testCount;
226
+ }
227
+ if (fullReport.suites && Array.isArray(fullReport.suites)) {
228
+ failedSuites = fullReport.suites.filter((s) => (typeof s.failed === 'number' && s.failed > 0) ||
229
+ s.status === 'failed' ||
230
+ s.tests?.some((t) => t.passed === false || t.status === 'failed')).length;
231
+ passedSuites = fullReport.suites.length - failedSuites;
232
+ }
233
+ if (fullReport.performance) {
234
+ renderSummaryCard({
235
+ isPassed,
236
+ statusText: isPassed ? 'PASSED' : 'FAILED',
237
+ duration: result.duration,
238
+ platform,
239
+ suiteCount,
240
+ passedSuites,
241
+ failedSuites,
242
+ testCount,
243
+ passedCount: result.passedCount ?? (isPassed ? testCount : 0),
244
+ failedCount: result.failedCount ?? (isPassed ? 0 : testCount),
245
+ reportPath: relReport,
246
+ networkReportPath: relNetworkReport,
247
+ });
248
+ renderTimingBreakdown({
249
+ ...fullReport.performance,
250
+ wallClockDurationMs: fullReport.wallClockDurationMs,
251
+ duration: fullReport.duration,
252
+ totalTests: fullReport.summary?.totalTests ?? testCount,
253
+ });
254
+ return;
255
+ }
256
+ }
257
+ catch {
258
+ // ignore
259
+ }
260
+ }
261
+ renderSummaryCard({
262
+ isPassed,
263
+ statusText: isPassed ? 'PASSED' : 'FAILED',
264
+ duration: result.duration,
265
+ platform,
266
+ suiteCount,
267
+ passedSuites,
268
+ failedSuites,
269
+ testCount,
270
+ passedCount: result.passedCount ?? (isPassed ? testCount : 0),
271
+ failedCount: result.failedCount ?? (isPassed ? 0 : testCount),
272
+ reportPath: relReport,
273
+ networkReportPath: relNetworkReport,
274
+ });
132
275
  }
133
276
  catch (e) {
134
277
  console.error(`\x1b[31m[TestSpectra] Execution error:\x1b[0m`, e);
@@ -0,0 +1,4 @@
1
+ export interface SyncTypesCommandOptions {
2
+ cwd?: string;
3
+ }
4
+ export declare function syncTypesCommand(options?: SyncTypesCommandOptions): Promise<void>;
@@ -0,0 +1,14 @@
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
+ console.log(chalk.green('✔ Successfully synchronized TestSpectra ambient type declarations in .testspectra/types/'));
9
+ }
10
+ catch (err) {
11
+ console.error(chalk.red(`✖ Failed to synchronize ambient declarations: ${err?.message}`));
12
+ process.exit(1);
13
+ }
14
+ }
@@ -0,0 +1,3 @@
1
+ export declare function updateCommand(options?: {
2
+ check?: boolean;
3
+ }): Promise<void>;