@testspectra/cli 1.0.59 → 1.0.63

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 (312) hide show
  1. package/README.md +33 -32
  2. package/bin/testspectra-runner +0 -0
  3. package/dist/commands/add.js +7 -13
  4. package/dist/commands/init.js +23 -23
  5. package/dist/commands/run.d.ts +1 -0
  6. package/dist/commands/run.js +22 -13
  7. package/dist/index.js +1 -0
  8. package/dist/server/demo.d.ts +8 -0
  9. package/dist/server/demo.js +104 -0
  10. package/dist/types/generator.d.ts +1 -1
  11. package/dist/types/generator.js +50 -15
  12. package/package.json +2 -2
  13. package/templates/default/.cursorrules +18 -0
  14. package/templates/default/CLAUDE.md +9 -0
  15. package/templates/default/fixtures/playgroundData.json +7 -0
  16. package/templates/default/global-hooks/after/mobile.hook.ts +3 -0
  17. package/templates/default/global-hooks/after/web.hook.ts +3 -0
  18. package/templates/default/global-hooks/before/mobile.hook.ts +3 -0
  19. package/templates/default/global-hooks/before/web.hook.ts +3 -0
  20. package/templates/default/package.json +5 -1
  21. package/templates/default/page-objects/MatchersPage/mobile.ts +21 -0
  22. package/templates/default/page-objects/MatchersPage/web.ts +23 -0
  23. package/templates/default/page-objects/NavigationPage/mobile.ts +5 -0
  24. package/templates/default/page-objects/NavigationPage/web.ts +8 -0
  25. package/templates/default/page-objects/PlaygroundPage/mobile.ts +26 -0
  26. package/templates/default/page-objects/PlaygroundPage/web.ts +29 -0
  27. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -0
  28. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +12 -0
  29. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +5 -0
  30. package/templates/default/specs/BrowserContext/hooks/after/mobile.hook.ts +1 -0
  31. package/templates/default/specs/BrowserContext/hooks/after/web.hook.ts +1 -0
  32. package/templates/default/specs/BrowserContext/hooks/afterEach/mobile.hook.ts +1 -0
  33. package/templates/default/specs/BrowserContext/hooks/afterEach/web.hook.ts +1 -0
  34. package/templates/default/specs/BrowserContext/hooks/before/mobile.hook.ts +1 -0
  35. package/templates/default/specs/BrowserContext/hooks/before/web.hook.ts +1 -0
  36. package/templates/default/specs/BrowserContext/hooks/beforeEach/mobile.hook.ts +1 -0
  37. package/templates/default/specs/BrowserContext/hooks/beforeEach/web.hook.ts +1 -0
  38. package/templates/default/specs/BrowserContext/suite.md +9 -0
  39. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -0
  40. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +12 -0
  41. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +7 -0
  42. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -0
  43. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +12 -0
  44. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +6 -0
  45. package/templates/default/specs/ContentMatchers/hooks/after/mobile.hook.ts +1 -0
  46. package/templates/default/specs/ContentMatchers/hooks/after/web.hook.ts +1 -0
  47. package/templates/default/specs/ContentMatchers/hooks/afterEach/mobile.hook.ts +1 -0
  48. package/templates/default/specs/ContentMatchers/hooks/afterEach/web.hook.ts +1 -0
  49. package/templates/default/specs/ContentMatchers/hooks/before/mobile.hook.ts +1 -0
  50. package/templates/default/specs/ContentMatchers/hooks/before/web.hook.ts +1 -0
  51. package/templates/default/specs/ContentMatchers/hooks/beforeEach/mobile.hook.ts +1 -0
  52. package/templates/default/specs/ContentMatchers/hooks/beforeEach/web.hook.ts +1 -0
  53. package/templates/default/specs/ContentMatchers/suite.md +9 -0
  54. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -0
  55. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +12 -0
  56. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +5 -0
  57. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -0
  58. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +12 -0
  59. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +8 -0
  60. package/templates/default/specs/ElementMatchers/hooks/after/mobile.hook.ts +1 -0
  61. package/templates/default/specs/ElementMatchers/hooks/after/web.hook.ts +1 -0
  62. package/templates/default/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +1 -0
  63. package/templates/default/specs/ElementMatchers/hooks/afterEach/web.hook.ts +1 -0
  64. package/templates/default/specs/ElementMatchers/hooks/before/mobile.hook.ts +1 -0
  65. package/templates/default/specs/ElementMatchers/hooks/before/web.hook.ts +1 -0
  66. package/templates/default/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +1 -0
  67. package/templates/default/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +1 -0
  68. package/templates/default/specs/ElementMatchers/suite.md +9 -0
  69. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -0
  70. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +12 -0
  71. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +6 -0
  72. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -0
  73. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +12 -0
  74. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -0
  75. package/templates/default/specs/Playground/hooks/after/mobile.hook.ts +1 -0
  76. package/templates/default/specs/Playground/hooks/after/web.hook.ts +1 -0
  77. package/templates/default/specs/Playground/hooks/afterEach/mobile.hook.ts +1 -0
  78. package/templates/default/specs/Playground/hooks/afterEach/web.hook.ts +1 -0
  79. package/templates/default/specs/Playground/hooks/before/mobile.hook.ts +1 -0
  80. package/templates/default/specs/Playground/hooks/before/web.hook.ts +1 -0
  81. package/templates/default/specs/Playground/hooks/beforeEach/mobile.hook.ts +1 -0
  82. package/templates/default/specs/Playground/hooks/beforeEach/web.hook.ts +1 -0
  83. package/templates/default/specs/Playground/suite.md +9 -0
  84. package/templates/default/spectra.config.ts +1 -1
  85. package/templates/default/support/actions/fillCredentials/mobile.action.ts +4 -0
  86. package/templates/default/support/actions/fillCredentials/web.action.ts +4 -0
  87. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +3 -0
  88. package/templates/default/support/steps/togglePlaygroundStates/step.md +7 -0
  89. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +5 -0
  90. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +4 -0
  91. package/templates/default/support/steps/verifyPlaygroundState/step.md +7 -0
  92. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +6 -0
  93. package/templates/default/tsconfig.spectra.android.json +7 -4
  94. package/templates/default/tsconfig.spectra.ios.json +7 -4
  95. package/templates/default/tsconfig.spectra.web.json +10 -6
  96. package/templates/nx/.cursorrules +18 -0
  97. package/templates/nx/CLAUDE.md +10 -0
  98. package/templates/nx/fixtures/playgroundData.json +7 -0
  99. package/templates/nx/global-hooks/after/mobile.hook.ts +3 -0
  100. package/templates/nx/global-hooks/after/web.hook.ts +3 -0
  101. package/templates/nx/global-hooks/before/mobile.hook.ts +3 -0
  102. package/templates/nx/global-hooks/before/web.hook.ts +3 -0
  103. package/templates/nx/package.json +1 -1
  104. package/templates/nx/{modules/auth → packages/matchers}/e2e/project.json +8 -5
  105. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -0
  106. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +12 -0
  107. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +5 -0
  108. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/after/mobile.hook.ts +1 -0
  109. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/after/web.hook.ts +1 -0
  110. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/afterEach/mobile.hook.ts +1 -0
  111. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/afterEach/web.hook.ts +1 -0
  112. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/before/mobile.hook.ts +1 -0
  113. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/before/web.hook.ts +1 -0
  114. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/beforeEach/mobile.hook.ts +1 -0
  115. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/hooks/beforeEach/web.hook.ts +1 -0
  116. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/suite.md +9 -0
  117. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -0
  118. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/spec.md +12 -0
  119. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +7 -0
  120. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -0
  121. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +12 -0
  122. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +6 -0
  123. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/after/mobile.hook.ts +1 -0
  124. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/after/web.hook.ts +1 -0
  125. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/afterEach/mobile.hook.ts +1 -0
  126. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/afterEach/web.hook.ts +1 -0
  127. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/before/mobile.hook.ts +1 -0
  128. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/before/web.hook.ts +1 -0
  129. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/beforeEach/mobile.hook.ts +1 -0
  130. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/hooks/beforeEach/web.hook.ts +1 -0
  131. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/suite.md +9 -0
  132. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -0
  133. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +12 -0
  134. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +5 -0
  135. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -0
  136. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +12 -0
  137. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +8 -0
  138. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/after/mobile.hook.ts +1 -0
  139. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/after/web.hook.ts +1 -0
  140. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +1 -0
  141. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/afterEach/web.hook.ts +1 -0
  142. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/before/mobile.hook.ts +1 -0
  143. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/before/web.hook.ts +1 -0
  144. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +1 -0
  145. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +1 -0
  146. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/suite.md +9 -0
  147. package/templates/nx/packages/playground/e2e/project.json +37 -0
  148. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -0
  149. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/spec.md +12 -0
  150. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +6 -0
  151. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -0
  152. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/spec.md +12 -0
  153. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -0
  154. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/after/mobile.hook.ts +1 -0
  155. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/after/web.hook.ts +1 -0
  156. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/afterEach/mobile.hook.ts +1 -0
  157. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/afterEach/web.hook.ts +1 -0
  158. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/before/mobile.hook.ts +1 -0
  159. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/before/web.hook.ts +1 -0
  160. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/beforeEach/mobile.hook.ts +1 -0
  161. package/templates/nx/packages/playground/e2e/specs/Playground/hooks/beforeEach/web.hook.ts +1 -0
  162. package/templates/nx/packages/playground/e2e/specs/Playground/suite.md +9 -0
  163. package/templates/nx/page-objects/MatchersPage/mobile.ts +21 -0
  164. package/templates/nx/page-objects/MatchersPage/web.ts +23 -0
  165. package/templates/nx/page-objects/NavigationPage/mobile.ts +5 -0
  166. package/templates/nx/page-objects/NavigationPage/web.ts +8 -0
  167. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +26 -0
  168. package/templates/nx/page-objects/PlaygroundPage/web.ts +29 -0
  169. package/templates/nx/shared/testing/fixtures/playgroundData.json +7 -0
  170. package/templates/nx/shared/testing/global-hooks/after/mobile.hook.ts +3 -0
  171. package/templates/nx/shared/testing/global-hooks/after/web.hook.ts +3 -0
  172. package/templates/nx/shared/testing/global-hooks/before/mobile.hook.ts +3 -0
  173. package/templates/nx/shared/testing/global-hooks/before/web.hook.ts +3 -0
  174. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +21 -0
  175. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +23 -0
  176. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +5 -0
  177. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +8 -0
  178. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +26 -0
  179. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +29 -0
  180. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +4 -0
  181. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +4 -0
  182. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +3 -0
  183. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/step.md +7 -0
  184. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +5 -0
  185. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +4 -0
  186. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/step.md +7 -0
  187. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +6 -0
  188. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -0
  189. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +12 -0
  190. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +5 -0
  191. package/templates/nx/specs/BrowserContext/hooks/after/mobile.hook.ts +1 -0
  192. package/templates/nx/specs/BrowserContext/hooks/after/web.hook.ts +1 -0
  193. package/templates/nx/specs/BrowserContext/hooks/afterEach/mobile.hook.ts +1 -0
  194. package/templates/nx/specs/BrowserContext/hooks/afterEach/web.hook.ts +1 -0
  195. package/templates/nx/specs/BrowserContext/hooks/before/mobile.hook.ts +1 -0
  196. package/templates/nx/specs/BrowserContext/hooks/before/web.hook.ts +1 -0
  197. package/templates/nx/specs/BrowserContext/hooks/beforeEach/mobile.hook.ts +1 -0
  198. package/templates/nx/specs/BrowserContext/hooks/beforeEach/web.hook.ts +1 -0
  199. package/templates/nx/specs/BrowserContext/suite.md +9 -0
  200. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -0
  201. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +12 -0
  202. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +7 -0
  203. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -0
  204. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +12 -0
  205. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +6 -0
  206. package/templates/nx/specs/ContentMatchers/hooks/after/mobile.hook.ts +1 -0
  207. package/templates/nx/specs/ContentMatchers/hooks/after/web.hook.ts +1 -0
  208. package/templates/nx/specs/ContentMatchers/hooks/afterEach/mobile.hook.ts +1 -0
  209. package/templates/nx/specs/ContentMatchers/hooks/afterEach/web.hook.ts +1 -0
  210. package/templates/nx/specs/ContentMatchers/hooks/before/mobile.hook.ts +1 -0
  211. package/templates/nx/specs/ContentMatchers/hooks/before/web.hook.ts +1 -0
  212. package/templates/nx/specs/ContentMatchers/hooks/beforeEach/mobile.hook.ts +1 -0
  213. package/templates/nx/specs/ContentMatchers/hooks/beforeEach/web.hook.ts +1 -0
  214. package/templates/nx/specs/ContentMatchers/suite.md +9 -0
  215. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -0
  216. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +12 -0
  217. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +5 -0
  218. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -0
  219. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +12 -0
  220. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +8 -0
  221. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +1 -0
  222. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +1 -0
  223. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +1 -0
  224. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +1 -0
  225. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +1 -0
  226. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +1 -0
  227. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +1 -0
  228. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +1 -0
  229. package/templates/nx/specs/ElementMatchers/suite.md +9 -0
  230. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -0
  231. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +12 -0
  232. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +6 -0
  233. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -0
  234. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +12 -0
  235. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -0
  236. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +1 -0
  237. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +1 -0
  238. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +1 -0
  239. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +1 -0
  240. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +1 -0
  241. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +1 -0
  242. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +1 -0
  243. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +1 -0
  244. package/templates/nx/specs/Playground/suite.md +9 -0
  245. package/templates/nx/spectra.config.ts +1 -1
  246. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +4 -0
  247. package/templates/nx/support/actions/fillCredentials/web.action.ts +4 -0
  248. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +3 -0
  249. package/templates/nx/support/steps/togglePlaygroundStates/step.md +7 -0
  250. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +5 -0
  251. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +4 -0
  252. package/templates/nx/support/steps/verifyPlaygroundState/step.md +7 -0
  253. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +6 -0
  254. package/templates/default/actions/applyDiscount/mobile.action.ts +0 -6
  255. package/templates/default/actions/applyDiscount/web.action.ts +0 -6
  256. package/templates/default/actions/verifyOtp/mobile.action.ts +0 -6
  257. package/templates/default/actions/verifyOtp/web.action.ts +0 -6
  258. package/templates/default/fixtures/couponCode.json +0 -1
  259. package/templates/default/fixtures/searchQuery.json +0 -1
  260. package/templates/default/fixtures/userData.json +0 -1
  261. package/templates/default/global-hooks/before.web.hook.ts +0 -4
  262. package/templates/default/page-objects/LoginPage/mobile.ts +0 -32
  263. package/templates/default/page-objects/LoginPage/web.ts +0 -32
  264. package/templates/default/page-objects/ProductPage/mobile.ts +0 -39
  265. package/templates/default/page-objects/ProductPage/web.ts +0 -38
  266. package/templates/default/page-objects/SettingsPage/common.ts +0 -26
  267. package/templates/default/specs/Auth/TC-AUTH-01/android.test.ts +0 -6
  268. package/templates/default/specs/Auth/TC-AUTH-01/ios.test.ts +0 -6
  269. package/templates/default/specs/Auth/TC-AUTH-01/web.test.ts +0 -13
  270. package/templates/default/specs/Auth/TC-AUTH-02/android.test.ts +0 -5
  271. package/templates/default/specs/Auth/TC-AUTH-02/web.test.ts +0 -5
  272. package/templates/default/specs/Auth/hooks/before.android.hook.ts +0 -3
  273. package/templates/default/specs/Auth/hooks/before.ios.hook.ts +0 -3
  274. package/templates/default/specs/Auth/hooks/before.web.hook.ts +0 -3
  275. package/templates/default/specs/Product/TC-PROD-01/mobile.test.ts +0 -8
  276. package/templates/default/specs/Product/TC-PROD-01/web.test.ts +0 -8
  277. package/templates/default/specs/Product/TC-PROD-02/mobile.test.ts +0 -6
  278. package/templates/default/specs/Product/TC-PROD-02/web.test.ts +0 -6
  279. package/templates/default/specs/Product/hooks/before.web.hook.ts +0 -3
  280. package/templates/default/specs/Settings/TC-SET-01/common.test.ts +0 -8
  281. package/templates/default/steps/loginUser/mobile.step.ts +0 -4
  282. package/templates/default/steps/loginUser/web.step.ts +0 -4
  283. package/templates/default/steps/searchProduct/mobile.step.ts +0 -5
  284. package/templates/default/steps/searchProduct/web.step.ts +0 -5
  285. package/templates/nx/modules/auth/e2e/actions/verifyOtp/mobile.action.ts +0 -6
  286. package/templates/nx/modules/auth/e2e/actions/verifyOtp/web.action.ts +0 -6
  287. package/templates/nx/modules/auth/e2e/fixtures/userData.json +0 -1
  288. package/templates/nx/modules/auth/e2e/global-hooks/before.web.hook.ts +0 -4
  289. package/templates/nx/modules/auth/e2e/package.json +0 -19
  290. package/templates/nx/modules/auth/e2e/page-objects/LoginPage/mobile.ts +0 -32
  291. package/templates/nx/modules/auth/e2e/page-objects/LoginPage/web.ts +0 -32
  292. package/templates/nx/modules/auth/e2e/page-objects/SettingsPage/common.ts +0 -26
  293. package/templates/nx/modules/auth/e2e/specs/Auth/TC-AUTH-01/android.test.ts +0 -6
  294. package/templates/nx/modules/auth/e2e/specs/Auth/TC-AUTH-01/ios.test.ts +0 -6
  295. package/templates/nx/modules/auth/e2e/specs/Auth/TC-AUTH-01/web.test.ts +0 -19
  296. package/templates/nx/modules/auth/e2e/specs/Auth/TC-AUTH-02/android.test.ts +0 -5
  297. package/templates/nx/modules/auth/e2e/specs/Auth/TC-AUTH-02/web.test.ts +0 -5
  298. package/templates/nx/modules/auth/e2e/specs/Auth/hooks/before.android.hook.ts +0 -3
  299. package/templates/nx/modules/auth/e2e/specs/Auth/hooks/before.ios.hook.ts +0 -3
  300. package/templates/nx/modules/auth/e2e/specs/Auth/hooks/before.web.hook.ts +0 -3
  301. package/templates/nx/modules/auth/e2e/specs/Settings/TC-SET-01/common.test.ts +0 -8
  302. package/templates/nx/modules/auth/e2e/steps/loginUser/mobile.step.ts +0 -4
  303. package/templates/nx/modules/auth/e2e/steps/loginUser/web.step.ts +0 -4
  304. package/templates/nx/modules/auth/package.json +0 -7
  305. package/templates/nx/modules/auth/src/index.ts +0 -3
  306. package/templates/nx/modules/catalog/package.json +0 -7
  307. package/templates/nx/modules/catalog/src/index.ts +0 -6
  308. package/templates/nx/shared/testing/actions/dismissBanner/common.action.ts +0 -6
  309. package/templates/nx/shared/testing/fixtures/appConfig.json +0 -4
  310. package/templates/nx/shared/testing/package.json +0 -8
  311. package/templates/nx/shared/testing/page-objects/NavigationBar/common.ts +0 -13
  312. package/templates/nx/shared/testing/steps/loginAsAdmin/common.step.ts +0 -4
package/README.md CHANGED
@@ -179,35 +179,36 @@ export default defineConfig({
179
179
  ├── tsconfig.android.json # Android platform TypeScript scope
180
180
  ├── tsconfig.ios.json # iOS platform TypeScript scope
181
181
  ├── page-objects/ # Page Object Model folders
182
- │ └── LoginPage/
182
+ │ └── PlaygroundPage/
183
183
  │ ├── web.ts
184
184
  │ └── mobile.ts
185
- ├── actions/ # Reusable atomic actions
186
- │ └── verifyOtp/
187
- │ ├── web.action.ts
188
- │ └── mobile.action.ts
189
- ├── steps/ # High-level business flows
190
- │ └── loginUser/
191
- │ ├── web.step.ts
192
- │ └── mobile.step.ts
185
+ ├── support/
186
+ │ ├── actions/ # Reusable atomic actions
187
+ │ │ └── fillCredentials/
188
+ │ │ ├── web.action.ts
189
+ │ │ └── mobile.action.ts
190
+ │ └── steps/ # High-level business flows
191
+ │ └── verifyPlaygroundState/
192
+ │ ├── web.step.ts
193
+ │ └── mobile.step.ts
193
194
  ├── global-hooks/ # Global lifecycle hooks
194
- │ └── before.web.hook.ts
195
+ │ └── before/
196
+ │ └── web.hook.ts
195
197
  ├── specs/ # Layered zero-import test cases
196
- │ └── Auth/
197
- │ └── TC-AUTH-01/
198
+ │ └── Playground/
199
+ │ └── TC-0001-form-controls/
198
200
  │ ├── web.test.ts
199
- │ ├── android.test.ts
200
- │ └── ios.test.ts
201
+ │ └── mobile.test.ts
201
202
  ├── fixtures/ # Platform-agnostic test data
202
- │ └── userData.json
203
- └── .testspectra/ # Auto-generated ambient typings & cache
204
- └── types/
205
- ├── fixtures.d.ts
206
- ├── web.d.ts
207
- ├── android.d.ts
208
- ├── ios.d.ts
209
- ├── mobile.d.ts
210
- └── common.d.ts
203
+ │ └── playgroundData.json
204
+ ├── .testspectra/ # Auto-generated ambient typings & cache
205
+ │ └── types/
206
+ │ ├── fixtures.d.ts
207
+ │ ├── web.d.ts
208
+ │ ├── android.d.ts
209
+ │ ├── ios.d.ts
210
+ │ ├── mobile.d.ts
211
+ │ └── common.d.ts
211
212
  ```
212
213
 
213
214
  ---
@@ -230,19 +231,19 @@ TestSpectra features an enterprise-grade architecture for large monorepos: **"Ce
230
231
  ┌──────────────────────────────┴──────────────────────────────┐
231
232
  ▼ ▼
232
233
  ┌───────────────────────────────┐ ┌───────────────────────────────┐
233
- │ shared/testing/ (Library) │ │ packages/features/auth/e2e/ │
234
+ │ shared/testing/ (Library) │ │ packages/playground/e2e/ │
234
235
  ├───────────────────────────────┤ ├───────────────────────────────┤
235
236
  │ - Shared Page Objects │ ◄─── Auto-Scanned Globals ──│ - Feature Specs (specs/) │
236
- │ - Shared Steps (loginAsAdmin) │ (100% Zero-Import!) │ - Feature Page Objects (POMs) │
237
- │ - Shared Actions (dismissBtn) │ │ - project.json (Nx Target) │
238
- │ - Shared Fixtures (appConfig) │ │ - tsconfig.web/android/ios │
237
+ │ - Shared Steps (verifyState) │ (100% Zero-Import!) │ - Feature Page Objects (POMs) │
238
+ │ - Shared Actions (fillCreds) │ │ - project.json (Nx Target) │
239
+ │ - Shared Fixtures (playData) │ │ - tsconfig.web/android/ios │
239
240
  └───────────────────────────────┘ └───────────────────────────────┘
240
241
  ```
241
242
 
242
243
  ### Key Pillars of Monorepo Integration:
243
244
 
244
245
  1. **Interactive Workspace Auto-Discovery (`spectra init`)**:
245
- - Parses `pnpm-workspace.yaml` (e.g. `packages/features/*`, `modules/**`).
246
+ - Parses `pnpm-workspace.yaml` (e.g. `packages/*`, `modules/**`).
246
247
  - Scans sub-directories for existing `project.json` or `package.json`.
247
248
  - Prompts the user with an interactive multi-select checkbox to choose which features will receive E2E testing.
248
249
  - Prompts for customizable E2E folder names (e.g. `e2e`, `test/e2e`) and the shared testing library path (`shared/testing`).
@@ -253,18 +254,18 @@ TestSpectra features an enterprise-grade architecture for large monorepos: **"Ce
253
254
  - Driver caches, binaries, and logs reside in `<root>/.testspectra/`, preventing repository clutter across individual feature folders.
254
255
 
255
256
  3. **Global Zero-Import Consumption of Shared Library**:
256
- - Entities placed inside `shared/testing/page-objects/`, `shared/testing/steps/`, `shared/testing/actions/`, and `shared/testing/fixtures/` are automatically aggregated into ambient declarations.
257
- - Feature test specs consume shared Page Objects (`NavigationBar.goToHome()`), steps (`Step.loginAsAdmin()`), actions (`Spectra.dismissBanner()`), and fixtures (`Fixture.appConfig`) **without a single line of `import` statement**.
257
+ - Entities placed inside `shared/testing/page-objects/`, `shared/testing/support/steps/`, `shared/testing/support/actions/`, and `shared/testing/fixtures/` are automatically aggregated into ambient declarations.
258
+ - Feature test specs consume shared Page Objects (`PlaygroundPage.open()`), steps (`Step.verifyPlaygroundState()`), actions (`Spectra.fillCredentials()`), and fixtures (`Fixture.playgroundData`) **without a single line of `import` statement**.
258
259
 
259
260
  4. **Nx Target Execution & Affected Caching**:
260
261
  - Each feature E2E folder includes an Nx `project.json`:
261
262
  ```json
262
263
  {
263
- "name": "auth-e2e",
264
+ "name": "playground-e2e",
264
265
  "targets": {
265
266
  "e2e": {
266
267
  "executor": "nx:run-commands",
267
- "options": { "command": "spectra run", "cwd": "modules/auth/e2e" },
268
+ "options": { "command": "spectra run", "cwd": "packages/playground/e2e" },
268
269
  "configurations": {
269
270
  "android": { "command": "spectra run --target android" },
270
271
  "ios": { "command": "spectra run --target ios" },
Binary file
@@ -130,7 +130,7 @@ export async function addCommand(moduleNameArg, options = {}) {
130
130
  if (!fs.existsSync(templateDir)) {
131
131
  templateDir = path.resolve(__dirname, "../../../templates/nx");
132
132
  }
133
- const sampleE2eSrc = path.join(templateDir, "modules/auth/e2e");
133
+ const sampleE2eSrc = path.join(templateDir, "packages/playground/e2e");
134
134
  if (!fs.existsSync(sampleE2eSrc)) {
135
135
  p.cancel(chalk.red(`Template directory not found at: ${sampleE2eSrc}`));
136
136
  return;
@@ -151,18 +151,12 @@ export async function addCommand(moduleNameArg, options = {}) {
151
151
  cur = rootDir;
152
152
  while (cur !== path.dirname(cur)) {
153
153
  if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml"))) {
154
- try {
155
- const wsContent = fs.readFileSync(path.join(cur, "pnpm-workspace.yaml"), "utf-8");
156
- if (wsContent.includes("cli")) {
157
- isInternalWorkspace = true;
158
- break;
159
- }
160
- }
161
- catch { }
154
+ isInternalWorkspace = true;
155
+ break;
162
156
  }
163
157
  cur = path.dirname(cur);
164
158
  }
165
- const cliDepVersion = isInternalWorkspace ? "workspace:*" : cliVersion;
159
+ const cliDepVersion = isInternalWorkspace ? "workspace:*" : (cliVersion.startsWith("^") ? cliVersion : `^${cliVersion}`);
166
160
  function copyRecursive(src, dest, replacements = {}) {
167
161
  const base = path.basename(src);
168
162
  if (base === "node_modules" || base === "dist" || base === ".testspectra" || base === ".git") {
@@ -289,9 +283,9 @@ export async function addCommand(moduleNameArg, options = {}) {
289
283
  const s = p.spinner();
290
284
  s.start(`Scaffolding ${e2eProjectName} in ./${path.relative(rootDir, targetE2eDir)}...`);
291
285
  copyRecursive(sampleE2eSrc, targetE2eDir, {
292
- "auth-e2e": e2eProjectName,
293
- "modules/auth/e2e": path.relative(rootDir, targetE2eDir).replace(/\\/g, "/"),
294
- "scope:auth": `scope:${featureName}`,
286
+ "playground-e2e": e2eProjectName,
287
+ "packages/playground/e2e": path.relative(rootDir, targetE2eDir).replace(/\\/g, "/"),
288
+ "scope:playground": `scope:${featureName}`,
295
289
  });
296
290
  // Adjust project.json
297
291
  const projJsonPath = path.join(targetE2eDir, "project.json");
@@ -256,19 +256,13 @@ export async function initCommand(options = {}) {
256
256
  let cur = cwd;
257
257
  while (cur !== path.dirname(cur)) {
258
258
  if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml"))) {
259
- try {
260
- const wsContent = fs.readFileSync(path.join(cur, "pnpm-workspace.yaml"), "utf-8");
261
- if (wsContent.includes("cli")) {
262
- isInternalWorkspace = true;
263
- break;
264
- }
265
- }
266
- catch { }
259
+ isInternalWorkspace = true;
260
+ break;
267
261
  }
268
262
  cur = path.dirname(cur);
269
263
  }
270
264
  const projectName = path.basename(cwd);
271
- const cliDepVersion = isInternalWorkspace ? "workspace:*" : cliVersion;
265
+ const cliDepVersion = isInternalWorkspace ? "workspace:*" : (cliVersion.startsWith("^") ? cliVersion : `^${cliVersion}`);
272
266
  // Helper: Copy directory contents recursively
273
267
  function copyRecursive(src, dest, replacements = {}) {
274
268
  const base = path.basename(src);
@@ -344,7 +338,7 @@ export async function initCommand(options = {}) {
344
338
  p.log.warn(chalk.yellow("No feature packages found in workspace. You can enter feature names to scaffold."));
345
339
  const featureNamesInput = await p.text({
346
340
  message: "Enter feature module names (comma-separated), or leave blank to skip:",
347
- placeholder: "auth, catalog, payment",
341
+ placeholder: "playground, matchers, navigation",
348
342
  defaultValue: "",
349
343
  });
350
344
  if (p.isCancel(featureNamesInput)) {
@@ -356,14 +350,14 @@ export async function initCommand(options = {}) {
356
350
  // Build relPaths from feature names using a sensible default location
357
351
  const featureBaseInput = await p.text({
358
352
  message: "Enter base directory for feature modules:",
359
- placeholder: "packages/features",
360
- defaultValue: "packages/features",
353
+ placeholder: "packages",
354
+ defaultValue: "packages",
361
355
  });
362
356
  if (p.isCancel(featureBaseInput)) {
363
357
  p.cancel("Setup cancelled.");
364
358
  return;
365
359
  }
366
- const featureBase = featureBaseInput.trim() || "packages/features";
360
+ const featureBase = featureBaseInput.trim() || "packages";
367
361
  chosenProjects = raw.split(",").map((n) => `${featureBase}/${n.trim()}`).filter(Boolean);
368
362
  }
369
363
  }
@@ -442,6 +436,14 @@ export async function initCommand(options = {}) {
442
436
  rootPkg.devDependencies["@testspectra/matchers"] = cliDepVersion;
443
437
  if (!rootPkg.devDependencies["typescript"])
444
438
  rootPkg.devDependencies["typescript"] = "^5.4.5";
439
+ if (!rootPkg.devDependencies["@wdio/cli"])
440
+ rootPkg.devDependencies["@wdio/cli"] = "^9.2.8";
441
+ if (!rootPkg.devDependencies["@wdio/local-runner"])
442
+ rootPkg.devDependencies["@wdio/local-runner"] = "^9.2.8";
443
+ if (!rootPkg.devDependencies["@wdio/spec-reporter"])
444
+ rootPkg.devDependencies["@wdio/spec-reporter"] = "^9.2.8";
445
+ if (!rootPkg.devDependencies["@wdio/devtools-service"])
446
+ rootPkg.devDependencies["@wdio/devtools-service"] = "^9.2.8";
445
447
  if (!rootPkg.devDependencies["@wdio/globals"])
446
448
  rootPkg.devDependencies["@wdio/globals"] = "^9.2.8";
447
449
  if (!rootPkg.devDependencies["@wdio/mocha-framework"])
@@ -465,16 +467,16 @@ export async function initCommand(options = {}) {
465
467
  copyRecursive(sharedTestingSrc, sharedTestingAbsPath);
466
468
  }
467
469
  // 4. Distribute E2E Folders to Selected Feature Modules
468
- const sampleE2eSrc = path.join(templateDir, "modules/auth/e2e");
470
+ const sampleE2eSrc = path.join(templateDir, "packages/playground/e2e");
469
471
  const featureE2eDirs = [];
470
472
  for (const projRel of chosenProjects) {
471
473
  const targetE2eDir = path.join(cwd, projRel, e2eFolderName);
472
474
  const featureName = path.basename(projRel);
473
475
  const e2eProjectName = `${featureName}-${e2eFolderName}`;
474
476
  copyRecursive(sampleE2eSrc, targetE2eDir, {
475
- "auth-e2e": e2eProjectName,
476
- "modules/auth/e2e": path.relative(cwd, targetE2eDir).replace(/\\/g, "/"),
477
- "scope:auth": `scope:${featureName}`,
477
+ "playground-e2e": e2eProjectName,
478
+ "packages/playground/e2e": path.relative(cwd, targetE2eDir).replace(/\\/g, "/"),
479
+ "scope:playground": `scope:${featureName}`,
478
480
  });
479
481
  // Adjust project.json
480
482
  const projJsonPath = path.join(targetE2eDir, "project.json");
@@ -704,12 +706,10 @@ pnpm type-check
704
706
  const s = p.spinner();
705
707
  s.start("Scaffolding standalone TestSpectra project...");
706
708
  copyRecursive(templateDir, cwd);
707
- if (!isInternalWorkspace) {
708
- const pnpmWorkspacePath = path.join(cwd, "pnpm-workspace.yaml");
709
- if (!fs.existsSync(pnpmWorkspacePath) || options.force) {
710
- const pnpmWsContent = `allowBuilds:\n chromedriver: true\n edgedriver: true\n geckodriver: true\n`;
711
- fs.writeFileSync(pnpmWorkspacePath, pnpmWsContent, "utf-8");
712
- }
709
+ const standaloneWsPath = path.join(cwd, "pnpm-workspace.yaml");
710
+ if (!fs.existsSync(standaloneWsPath) || options.force) {
711
+ const pnpmWsContent = `allowBuilds:\n chromedriver: true\n edgedriver: true\n geckodriver: true\n`;
712
+ fs.writeFileSync(standaloneWsPath, pnpmWsContent, "utf-8");
713
713
  }
714
714
  TypeGenerator.writeDeclarationFiles(cwd);
715
715
  ensureVsCodeExtensions(cwd);
@@ -1,4 +1,5 @@
1
1
  export interface RunCommandOptions {
2
+ spec?: string;
2
3
  target?: "web" | "android" | "ios" | "common";
3
4
  device?: string;
4
5
  headless?: boolean;
@@ -6,6 +6,7 @@ import { Reporter } from "../runner/reporter.js";
6
6
  import { TypeGenerator } from "../types/generator.js";
7
7
  export async function runCommand(specPath, options = {}) {
8
8
  const cwd = process.cwd();
9
+ const effectiveSpecPath = specPath || options.spec;
9
10
  // Ensure latest ambient types are up-to-date
10
11
  TypeGenerator.writeDeclarationFiles(cwd);
11
12
  const config = await ConfigLoader.loadConfig(cwd);
@@ -13,11 +14,11 @@ export async function runCommand(specPath, options = {}) {
13
14
  config.webConfig.headlessMode = options.headless;
14
15
  }
15
16
  const platform = options.target || "web";
16
- // Find workspace root for centralized .testspectra
17
+ // Find workspace root for centralized .testspectra (monorepo root or current test project)
17
18
  let workspaceRoot = cwd;
18
19
  let cur = cwd;
19
20
  while (cur !== path.dirname(cur)) {
20
- if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) || fs.existsSync(path.join(cur, "nx.json")) || fs.existsSync(path.join(cur, "spectra.config.ts")) || fs.existsSync(path.join(cur, "testspectra.config.ts"))) {
21
+ if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) || fs.existsSync(path.join(cur, "nx.json"))) {
21
22
  workspaceRoot = cur;
22
23
  break;
23
24
  }
@@ -30,18 +31,25 @@ export async function runCommand(specPath, options = {}) {
30
31
  // Determine suite/test case name
31
32
  let suiteName = "default";
32
33
  let testCases = [];
33
- if (specPath) {
34
- const parsed = path.parse(specPath);
35
- const baseName = parsed.name.replace(/\.(web|android|ios|common|test|spec)/g, "");
36
- suiteName = baseName;
34
+ if (effectiveSpecPath) {
35
+ const relSpec = path.isAbsolute(effectiveSpecPath) ? path.relative(cwd, effectiveSpecPath) : effectiveSpecPath;
36
+ const normalized = relSpec.replace(/\\/g, "/");
37
+ const parts = normalized.split("/");
38
+ if (parts.length >= 2 && (parts[0] === "specs" || parts[0] === "suites")) {
39
+ suiteName = parts[1];
40
+ }
41
+ else {
42
+ const parsed = path.parse(normalized);
43
+ suiteName = parsed.name.replace(/\.(web|android|ios|common|test|spec)/g, "") || "default";
44
+ }
37
45
  testCases.push({
38
- id: baseName,
39
- title: baseName,
46
+ id: normalized,
47
+ title: normalized,
40
48
  executionOrder: 1,
41
49
  });
42
50
  }
43
51
  else {
44
- // 1. Scan specs/ directory (Option A: specs/<Suite>/<CaseId>/web.test.ts)
52
+ // 1. Scan specs/ directory (specs/<Suite>/<CaseId>/web.test.ts)
45
53
  const specsDir = path.join(cwd, "specs");
46
54
  if (fs.existsSync(specsDir)) {
47
55
  const suiteEntries = fs.readdirSync(specsDir, { withFileTypes: true });
@@ -55,11 +63,11 @@ export async function runCommand(specPath, options = {}) {
55
63
  if (cEntry.name.startsWith(".") || cEntry.name === "hooks")
56
64
  continue;
57
65
  if (cEntry.isDirectory()) {
58
- testCases.push({ id: cEntry.name, title: `${currentSuite} / ${cEntry.name}` });
66
+ testCases.push({ id: path.join("specs", currentSuite, cEntry.name).replace(/\\/g, "/"), title: `${currentSuite} / ${cEntry.name}` });
59
67
  }
60
68
  else if (cEntry.isFile() && (cEntry.name.endsWith(".test.ts") || cEntry.name.endsWith(".spec.ts"))) {
61
- const id = cEntry.name.replace(/\.(web|android|ios|common|all|mobile|test|spec)\.ts$/g, "");
62
- testCases.push({ id, title: `${currentSuite} / ${id}` });
69
+ const id = path.join("specs", currentSuite, cEntry.name).replace(/\\/g, "/");
70
+ testCases.push({ id, title: `${currentSuite} / ${cEntry.name}` });
63
71
  }
64
72
  }
65
73
  }
@@ -77,7 +85,7 @@ export async function runCommand(specPath, options = {}) {
77
85
  for (const cEntry of caseEntries) {
78
86
  if (cEntry.name.startsWith(".") || cEntry.name === "hooks" || !cEntry.isDirectory())
79
87
  continue;
80
- testCases.push({ id: cEntry.name, title: `${currentSuite} / ${cEntry.name}` });
88
+ testCases.push({ id: path.join("suites", currentSuite, cEntry.name).replace(/\\/g, "/"), title: `${currentSuite} / ${cEntry.name}` });
81
89
  }
82
90
  }
83
91
  }
@@ -93,6 +101,7 @@ export async function runCommand(specPath, options = {}) {
93
101
  ? path.resolve(options.output)
94
102
  : path.join(appDataPath, "reports/result.json");
95
103
  console.log(`\x1b[36m[TestSpectra]\x1b[0m Starting execution on target \x1b[1m${platform}\x1b[0m...`);
104
+ console.log(`\x1b[36m[TestSpectra]\x1b[0m Base URL: \x1b[33m${config.webConfig.baseUrl}\x1b[0m`);
96
105
  console.log(`\x1b[36m[TestSpectra]\x1b[0m Suite: ${suiteName} (${testCases.length} case(s))\n`);
97
106
  const reporter = new Reporter();
98
107
  try {
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ export function createCliProgram() {
45
45
  program
46
46
  .command("run [path]")
47
47
  .description("Execute test suite or individual test case")
48
+ .option("-s, --spec <path>", "Spec file path to execute")
48
49
  .option("-t, --target <target>", "Target platform: web, android, ios, common", "web")
49
50
  .option("-d, --device <device>", "Target device name or UDID")
50
51
  .option("--headless", "Run in headless mode")
@@ -0,0 +1,8 @@
1
+ export declare class DemoAppServer {
2
+ private server;
3
+ port: number;
4
+ url: string;
5
+ static findDemoAppDir(cwd: string): string | null;
6
+ start(cwd: string): Promise<string>;
7
+ stop(): void;
8
+ }
@@ -0,0 +1,104 @@
1
+ import http from "http";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { execSync } from "child_process";
6
+ import chalk from "chalk";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ export class DemoAppServer {
10
+ server = null;
11
+ port = 0;
12
+ url = "";
13
+ static findDemoAppDir(cwd) {
14
+ // 1. Check upward from cwd
15
+ let cur = cwd;
16
+ while (cur !== path.dirname(cur)) {
17
+ const cand = path.join(cur, "apps", "demo-web-app");
18
+ if (fs.existsSync(cand) && fs.existsSync(path.join(cand, "package.json"))) {
19
+ return cand;
20
+ }
21
+ cur = path.dirname(cur);
22
+ }
23
+ // 2. Check relative to CLI package location
24
+ const relCand = path.resolve(__dirname, "../../../apps/demo-web-app");
25
+ if (fs.existsSync(relCand) && fs.existsSync(path.join(relCand, "package.json"))) {
26
+ return relCand;
27
+ }
28
+ return null;
29
+ }
30
+ async start(cwd) {
31
+ const demoDir = DemoAppServer.findDemoAppDir(cwd);
32
+ if (!demoDir) {
33
+ throw new Error("Could not locate 'apps/demo-web-app'. Ensure you are running within the TestSpectra workspace.");
34
+ }
35
+ const distDir = path.join(demoDir, "dist");
36
+ if (!fs.existsSync(distDir) || !fs.existsSync(path.join(distDir, "index.html"))) {
37
+ console.log(chalk.cyan("[TestSpectra] ") +
38
+ chalk.dim("Building demo web app bundle (apps/demo-web-app)..."));
39
+ execSync("pnpm --filter @testspectra/demo-web-app build", {
40
+ cwd: path.resolve(demoDir, "../.."),
41
+ stdio: "inherit",
42
+ });
43
+ }
44
+ const mimeTypes = {
45
+ ".html": "text/html; charset=utf-8",
46
+ ".js": "text/javascript; charset=utf-8",
47
+ ".css": "text/css; charset=utf-8",
48
+ ".json": "application/json",
49
+ ".svg": "image/svg+xml",
50
+ ".png": "image/png",
51
+ ".jpg": "image/jpeg",
52
+ ".jpeg": "image/jpeg",
53
+ ".ico": "image/x-icon",
54
+ ".woff": "font/woff",
55
+ ".woff2": "font/woff2",
56
+ ".ttf": "font/ttf",
57
+ };
58
+ return new Promise((resolve, reject) => {
59
+ this.server = http.createServer((req, res) => {
60
+ const rawUrl = req.url || "/";
61
+ const cleanPath = rawUrl.split("?")[0].split("#")[0];
62
+ let filePath = path.join(distDir, cleanPath === "/" ? "index.html" : cleanPath);
63
+ // Fallback to index.html for SPA client-side routing
64
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
65
+ filePath = path.join(distDir, "index.html");
66
+ }
67
+ const ext = path.extname(filePath).toLowerCase();
68
+ const contentType = mimeTypes[ext] || "application/octet-stream";
69
+ fs.readFile(filePath, (err, content) => {
70
+ if (err) {
71
+ res.writeHead(500, { "Content-Type": "text/plain" });
72
+ res.end("Internal Server Error");
73
+ }
74
+ else {
75
+ res.writeHead(200, {
76
+ "Content-Type": contentType,
77
+ "Cache-Control": "no-cache",
78
+ "Access-Control-Allow-Origin": "*",
79
+ });
80
+ res.end(content);
81
+ }
82
+ });
83
+ });
84
+ this.server.listen(0, "127.0.0.1", () => {
85
+ const addr = this.server?.address();
86
+ if (addr && typeof addr === "object") {
87
+ this.port = addr.port;
88
+ this.url = `http://127.0.0.1:${this.port}`;
89
+ resolve(this.url);
90
+ }
91
+ else {
92
+ reject(new Error("Failed to bind demo preview server address"));
93
+ }
94
+ });
95
+ this.server.on("error", reject);
96
+ });
97
+ }
98
+ stop() {
99
+ if (this.server) {
100
+ this.server.close();
101
+ this.server = null;
102
+ }
103
+ }
104
+ }
@@ -12,6 +12,6 @@ export declare class TypeGenerator {
12
12
  static getPageObjectsDirsForScope(scopeDir: string, sharedDir?: string): string[];
13
13
  static getEntityDirsForScope(entityType: "actions" | "steps" | "fixtures", scopeDir: string, sharedDir?: string): string[];
14
14
  static generateDeclarationsForScope(cwd: string, outputTypesDir: string, scopeDir: string, sharedDir?: string): Record<string, string>;
15
- static generateFixturesDeclarationForScope(scopeDir: string, sharedDir?: string): string;
15
+ static generateFixturesDeclarationForScope(cwd: string, outputTypesDir: string, scopeDir: string, sharedDir?: string): string;
16
16
  static writeDeclarationFiles(cwd: string): void;
17
17
  }
@@ -2,10 +2,10 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  const PLATFORM_HIERARCHY = {
4
4
  web: ["web", "common"],
5
- ios: ["ios", "mobile", "common"],
6
- android: ["android", "mobile", "common"],
7
- mobile: ["mobile", "common"],
8
- common: ["common"],
5
+ ios: ["ios", "mobile", "common", "web"],
6
+ android: ["android", "mobile", "common", "web"],
7
+ mobile: ["mobile", "common", "android", "ios", "web"],
8
+ common: ["common", "web", "mobile", "android", "ios"],
9
9
  };
10
10
  export class TypeGenerator {
11
11
  /**
@@ -21,7 +21,12 @@ export class TypeGenerator {
21
21
  path.join(cwd, "testing"),
22
22
  ];
23
23
  for (const cand of sharedCandidates) {
24
- if (fs.existsSync(cand) && (fs.existsSync(path.join(cand, "page-objects")) || fs.existsSync(path.join(cand, "steps")) || fs.existsSync(path.join(cand, "actions")))) {
24
+ if (fs.existsSync(cand) &&
25
+ (fs.existsSync(path.join(cand, "page-objects")) ||
26
+ fs.existsSync(path.join(cand, "support")) ||
27
+ fs.existsSync(path.join(cand, "steps")) ||
28
+ fs.existsSync(path.join(cand, "actions")) ||
29
+ fs.existsSync(path.join(cand, "fixtures")))) {
25
30
  if (!scopes.some((s) => s.dir === cand)) {
26
31
  scopes.push({ name: "shared", dir: cand, isShared: true });
27
32
  }
@@ -124,9 +129,15 @@ export class TypeGenerator {
124
129
  scanDir(scopeDir, 6);
125
130
  }
126
131
  else {
127
- const candidates = [path.join(scopeDir, entityType)];
132
+ const candidates = [
133
+ path.join(scopeDir, entityType),
134
+ path.join(scopeDir, "support", entityType),
135
+ path.join(scopeDir, "support", "testing", entityType),
136
+ ];
128
137
  if (sharedDir) {
129
138
  candidates.push(path.join(sharedDir, entityType));
139
+ candidates.push(path.join(sharedDir, "support", entityType));
140
+ candidates.push(path.join(sharedDir, "support", "testing", entityType));
130
141
  }
131
142
  for (const c of candidates) {
132
143
  if (fs.existsSync(c) && !dirs.includes(c))
@@ -244,7 +255,11 @@ export class TypeGenerator {
244
255
  if (matchedFile) {
245
256
  if (!matchedFile.startsWith("."))
246
257
  matchedFile = `./${matchedFile}`;
247
- content += ` ${actionName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
258
+ const camel = actionName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
259
+ content += ` "${actionName}": (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
260
+ if (camel !== actionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
261
+ content += ` ${camel}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
262
+ }
248
263
  declaredActions.add(actionName);
249
264
  }
250
265
  }
@@ -258,7 +273,11 @@ export class TypeGenerator {
258
273
  let rel = path.relative(outputTypesDir, path.join(actionsDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
259
274
  if (!rel.startsWith("."))
260
275
  rel = `./${rel}`;
261
- content += ` ${actionName}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
276
+ const camel = actionName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
277
+ content += ` "${actionName}": (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
278
+ if (camel !== actionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
279
+ content += ` ${camel}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
280
+ }
262
281
  declaredActions.add(actionName);
263
282
  }
264
283
  }
@@ -298,7 +317,11 @@ export class TypeGenerator {
298
317
  if (matchedFile) {
299
318
  if (!matchedFile.startsWith("."))
300
319
  matchedFile = `./${matchedFile}`;
301
- content += ` ${stepName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
320
+ const camel = stepName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
321
+ content += ` "${stepName}": (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
322
+ if (camel !== stepName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
323
+ content += ` ${camel}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
324
+ }
302
325
  declaredSteps.add(stepName);
303
326
  }
304
327
  }
@@ -312,7 +335,11 @@ export class TypeGenerator {
312
335
  let rel = path.relative(outputTypesDir, path.join(stepsDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
313
336
  if (!rel.startsWith("."))
314
337
  rel = `./${rel}`;
315
- content += ` ${stepName}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
338
+ const camel = stepName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
339
+ content += ` "${stepName}": (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
340
+ if (camel !== stepName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
341
+ content += ` ${camel}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
342
+ }
316
343
  declaredSteps.add(stepName);
317
344
  }
318
345
  }
@@ -324,7 +351,7 @@ export class TypeGenerator {
324
351
  }
325
352
  return results;
326
353
  }
327
- static generateFixturesDeclarationForScope(scopeDir, sharedDir) {
354
+ static generateFixturesDeclarationForScope(cwd, outputTypesDir, scopeDir, sharedDir) {
328
355
  const fixturesDirs = this.getEntityDirsForScope("fixtures", scopeDir, sharedDir);
329
356
  let content = `// Auto-generated ambient fixture declarations for TestSpectra\n`;
330
357
  content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
@@ -346,7 +373,15 @@ export class TypeGenerator {
346
373
  const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
347
374
  if (declaredFixtures.has(varName))
348
375
  continue;
349
- content += ` readonly ${varName}: string;\n`;
376
+ if (entry.name.endsWith(".json")) {
377
+ let rel = path.relative(outputTypesDir, path.join(dir, entry.name)).replace(/\\/g, "/");
378
+ if (!rel.startsWith("."))
379
+ rel = `./${rel}`;
380
+ content += ` readonly ${varName}: typeof import('${rel}');\n`;
381
+ }
382
+ else {
383
+ content += ` readonly ${varName}: string;\n`;
384
+ }
350
385
  declaredFixtures.add(varName);
351
386
  }
352
387
  }
@@ -368,7 +403,7 @@ export class TypeGenerator {
368
403
  const scopes = this.getEntityScopes(cwd);
369
404
  const sharedScope = scopes.find((s) => s.isShared);
370
405
  // 1. Generate Global / Standalone Root Types (or fallback)
371
- const rootFixtures = this.generateFixturesDeclarationForScope(cwd);
406
+ const rootFixtures = this.generateFixturesDeclarationForScope(cwd, rootTypesDir, cwd);
372
407
  fs.writeFileSync(path.join(rootTypesDir, "fixtures.d.ts"), rootFixtures, "utf-8");
373
408
  const rootDeclarations = this.generateDeclarationsForScope(cwd, rootTypesDir, cwd);
374
409
  for (const [platform, content] of Object.entries(rootDeclarations)) {
@@ -379,7 +414,7 @@ export class TypeGenerator {
379
414
  const sharedTypesDir = path.join(rootTypesDir, "shared");
380
415
  if (!fs.existsSync(sharedTypesDir))
381
416
  fs.mkdirSync(sharedTypesDir, { recursive: true });
382
- const sharedFixtures = this.generateFixturesDeclarationForScope(sharedScope.dir);
417
+ const sharedFixtures = this.generateFixturesDeclarationForScope(cwd, sharedTypesDir, sharedScope.dir);
383
418
  fs.writeFileSync(path.join(sharedTypesDir, "fixtures.d.ts"), sharedFixtures, "utf-8");
384
419
  // Shared scope has NO feature dependencies (only shared itself)
385
420
  const sharedDeclarations = this.generateDeclarationsForScope(cwd, sharedTypesDir, sharedScope.dir);
@@ -393,7 +428,7 @@ export class TypeGenerator {
393
428
  const featureTypesDir = path.join(rootTypesDir, "features", feature.name);
394
429
  if (!fs.existsSync(featureTypesDir))
395
430
  fs.mkdirSync(featureTypesDir, { recursive: true });
396
- const featureFixtures = this.generateFixturesDeclarationForScope(feature.dir, sharedScope?.dir);
431
+ const featureFixtures = this.generateFixturesDeclarationForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
397
432
  fs.writeFileSync(path.join(featureTypesDir, "fixtures.d.ts"), featureFixtures, "utf-8");
398
433
  // Feature scope gets (feature + shared) ONLY
399
434
  const featureDeclarations = this.generateDeclarationsForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);