@testspectra/cli 1.0.70 → 1.1.0-rc.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 (365) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +36 -270
  3. package/bin/spectra.js +1 -1
  4. package/bin/testspectra-runner +0 -0
  5. package/demo-app/assets/index-BGllUp7o.js +140 -0
  6. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  7. package/demo-app/index.html +2 -2
  8. package/dist/.tsbuildinfo +1 -0
  9. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  10. package/dist/commands/__tests__/doctor.test.js +39 -0
  11. package/dist/commands/__tests__/init.test.d.ts +1 -0
  12. package/dist/commands/__tests__/init.test.js +148 -0
  13. package/dist/commands/__tests__/license.test.d.ts +1 -0
  14. package/dist/commands/__tests__/license.test.js +13 -0
  15. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  16. package/dist/commands/__tests__/refactor.test.js +64 -0
  17. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  18. package/dist/commands/__tests__/run-e2e.test.js +422 -0
  19. package/dist/commands/__tests__/run.test.d.ts +1 -0
  20. package/dist/commands/__tests__/run.test.js +277 -0
  21. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  22. package/dist/commands/__tests__/watch.test.js +155 -0
  23. package/dist/commands/add.js +86 -85
  24. package/dist/commands/demo.js +21 -11
  25. package/dist/commands/devices.d.ts +14 -2
  26. package/dist/commands/devices.js +44 -34
  27. package/dist/commands/doctor.d.ts +3 -1
  28. package/dist/commands/doctor.js +43 -264
  29. package/dist/commands/init.d.ts +1 -1
  30. package/dist/commands/init.js +206 -196
  31. package/dist/commands/install.d.ts +4 -0
  32. package/dist/commands/install.js +59 -0
  33. package/dist/commands/license.d.ts +4 -0
  34. package/dist/commands/license.js +30 -0
  35. package/dist/commands/lint.d.ts +5 -0
  36. package/dist/commands/lint.js +55 -0
  37. package/dist/commands/refactor.d.ts +9 -0
  38. package/dist/commands/refactor.js +172 -0
  39. package/dist/commands/run.d.ts +23 -2
  40. package/dist/commands/run.js +321 -111
  41. package/dist/commands/skills.d.ts +9 -0
  42. package/dist/commands/skills.js +28 -0
  43. package/dist/commands/sync-types.d.ts +4 -0
  44. package/dist/commands/sync-types.js +14 -0
  45. package/dist/commands/update.js +39 -43
  46. package/dist/commands/watch.d.ts +6 -0
  47. package/dist/commands/watch.js +46 -43
  48. package/dist/config/loader.d.ts +1 -1
  49. package/dist/config/loader.js +40 -25
  50. package/dist/config/schema.d.ts +117 -54
  51. package/dist/config/schema.js +41 -35
  52. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  53. package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
  54. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  55. package/dist/generator/__tests__/type-generator.test.js +386 -0
  56. package/dist/generator/index.d.ts +2 -0
  57. package/dist/generator/index.js +2 -0
  58. package/dist/generator/tsconfig-generator.d.ts +27 -0
  59. package/dist/generator/tsconfig-generator.js +384 -0
  60. package/dist/generator/type-generator.d.ts +36 -0
  61. package/dist/generator/type-generator.js +644 -0
  62. package/dist/index.d.ts +9 -5
  63. package/dist/index.js +151 -51
  64. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  65. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  66. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  67. package/dist/lifecycle/hook-discovery.js +60 -0
  68. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  69. package/dist/lifecycle/hook-dispatcher.js +190 -0
  70. package/dist/lifecycle/index.d.ts +3 -0
  71. package/dist/lifecycle/index.js +3 -0
  72. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  73. package/dist/lifecycle/parallel-grouping.js +151 -0
  74. package/dist/linter/__tests__/anonymous-export.test.d.ts +1 -0
  75. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  76. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  77. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  78. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  79. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  80. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  81. package/dist/linter/__tests__/step-parity.test.js +372 -0
  82. package/dist/linter/discovery.d.ts +30 -0
  83. package/dist/linter/discovery.js +249 -0
  84. package/dist/linter/extractor.d.ts +66 -0
  85. package/dist/linter/extractor.js +235 -0
  86. package/dist/linter/index.d.ts +7 -0
  87. package/dist/linter/index.js +7 -0
  88. package/dist/linter/parser.d.ts +24 -0
  89. package/dist/linter/parser.js +219 -0
  90. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  91. package/dist/linter/schemas/spec.schema.js +150 -0
  92. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  93. package/dist/linter/schemas/suite.schema.js +83 -0
  94. package/dist/linter/spec-linter.d.ts +23 -0
  95. package/dist/linter/spec-linter.js +595 -0
  96. package/dist/linter/types.d.ts +28 -0
  97. package/dist/linter/types.js +1 -0
  98. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  99. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  100. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  101. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  102. package/dist/mobile/device-pool.d.ts +23 -0
  103. package/dist/mobile/device-pool.js +1 -0
  104. package/dist/mobile/index.d.ts +1 -0
  105. package/dist/mobile/index.js +1 -0
  106. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  107. package/dist/refactor/__tests__/refactor-engine.test.js +873 -0
  108. package/dist/refactor/index.d.ts +1 -0
  109. package/dist/refactor/index.js +1 -0
  110. package/dist/refactor/refactor-engine.d.ts +50 -0
  111. package/dist/refactor/refactor-engine.js +587 -0
  112. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  113. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  114. package/dist/reporter/semantic-formatter.d.ts +6 -0
  115. package/dist/reporter/semantic-formatter.js +191 -0
  116. package/dist/reporter/summary-view.d.ts +12 -0
  117. package/dist/reporter/summary-view.js +19 -0
  118. package/dist/reporter/terminal-stream.d.ts +32 -0
  119. package/dist/reporter/terminal-stream.js +82 -0
  120. package/dist/reporter/types.d.ts +2 -0
  121. package/dist/reporter/types.js +1 -0
  122. package/dist/runner/bridge.d.ts +10 -5
  123. package/dist/runner/bridge.js +101 -45
  124. package/dist/runner/reporter.d.ts +32 -2
  125. package/dist/runner/reporter.js +423 -13
  126. package/dist/server/demo.js +43 -44
  127. package/dist/types/generator.d.ts +1 -17
  128. package/dist/types/generator.js +1 -443
  129. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  130. package/dist/utils/__tests__/api-server.test.js +62 -0
  131. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  132. package/dist/utils/__tests__/demo-server.test.js +35 -0
  133. package/dist/utils/api-server.d.ts +19 -0
  134. package/dist/utils/api-server.js +134 -0
  135. package/dist/utils/demo-server.d.ts +5 -0
  136. package/dist/utils/demo-server.js +46 -36
  137. package/package.json +24 -13
  138. package/templates/default/CLAUDE.md +12 -5
  139. package/templates/default/fixtures/apiData.json +24 -0
  140. package/templates/default/package.json +12 -3
  141. package/templates/default/page-objects/MatchersPage/mobile.ts +53 -17
  142. package/templates/default/page-objects/MatchersPage/web.ts +17 -2
  143. package/templates/default/page-objects/NavigationPage/mobile.ts +11 -2
  144. package/templates/default/page-objects/NavigationPage/web.ts +1 -1
  145. package/templates/default/page-objects/PlaygroundPage/mobile.ts +28 -18
  146. package/templates/default/page-objects/PlaygroundPage/web.ts +4 -2
  147. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -0
  148. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -0
  149. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -0
  150. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  151. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  152. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -0
  153. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -0
  154. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -0
  155. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +33 -0
  156. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  157. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  158. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  159. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  160. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  161. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  162. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  163. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  164. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  165. package/templates/default/specs/ApiInterception/suite.md +10 -0
  166. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  167. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  168. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  169. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  170. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  171. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  172. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
  173. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  174. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  175. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
  176. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  177. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
  178. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +9 -4
  179. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  180. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  181. package/templates/default/specs/DeepLink/suite.md +9 -0
  182. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  183. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
  184. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  185. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  186. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
  187. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  188. package/templates/default/specs/ElementMatchers/suite.md +2 -1
  189. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  190. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  191. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  192. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  193. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  194. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  195. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
  196. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +4 -4
  197. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  198. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
  199. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +3 -3
  200. package/templates/default/spectra.config.ts +21 -15
  201. package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
  202. package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
  203. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +3 -1
  204. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +3 -3
  205. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +4 -2
  206. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +4 -4
  207. package/templates/default/tsconfig.json +3 -1
  208. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  209. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  210. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  211. package/templates/nx/.nx/workspace-data/d/daemon.log +4306 -0
  212. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  213. package/templates/nx/.nx/workspace-data/file-map.json +452 -0
  214. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  215. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  216. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  217. package/templates/nx/.nx/workspace-data/project-graph.json +14389 -0
  218. package/templates/nx/CLAUDE.md +2 -2
  219. package/templates/nx/package.json +6 -4
  220. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -0
  221. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  222. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  223. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -0
  224. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  225. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  226. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  227. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  228. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  229. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  230. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +15 -5
  231. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  232. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +9 -4
  233. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  234. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  235. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  236. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  237. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  238. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +4 -4
  239. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  240. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +3 -3
  241. package/templates/nx/pnpm-lock.yaml +6663 -0
  242. package/templates/nx/pnpm-workspace.yaml +2 -1
  243. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  244. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +13 -1
  245. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +85 -17
  246. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  247. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  248. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  249. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +4 -2
  250. package/templates/nx/shared/testing/project.json +19 -0
  251. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  252. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  253. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  254. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +3 -3
  255. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +2 -2
  256. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +4 -4
  257. package/templates/nx/spectra.config.ts +19 -13
  258. package/templates/nx/tsconfig.json +3 -1
  259. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  260. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  261. package/dist/index.cjs +0 -101
  262. package/dist/plugin.cjs +0 -575
  263. package/dist/plugin.d.ts +0 -16
  264. package/dist/plugin.js +0 -184
  265. package/dist/step/index.d.ts +0 -6
  266. package/dist/step/index.js +0 -6
  267. package/dist/step/matchers.d.ts +0 -14
  268. package/dist/step/matchers.js +0 -114
  269. package/dist/step/proto.d.ts +0 -46
  270. package/dist/step/proto.js +0 -114
  271. package/dist/step/runner/collection.d.ts +0 -13
  272. package/dist/step/runner/collection.js +0 -51
  273. package/dist/step/runner/single.d.ts +0 -21
  274. package/dist/step/runner/single.js +0 -138
  275. package/dist/step/spectra.d.ts +0 -34
  276. package/dist/step/spectra.js +0 -156
  277. package/dist/step/types.d.ts +0 -32
  278. package/dist/step/types.js +0 -5
  279. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  280. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  281. package/templates/default/tsconfig.spectra.android.json +0 -46
  282. package/templates/default/tsconfig.spectra.ios.json +0 -46
  283. package/templates/default/tsconfig.spectra.json +0 -22
  284. package/templates/default/tsconfig.spectra.web.json +0 -53
  285. package/templates/nx/fixtures/playgroundData.json +0 -7
  286. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  287. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  288. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  289. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  290. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  291. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  292. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  293. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  294. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  295. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  296. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  297. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  298. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  299. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  300. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  301. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  302. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  303. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  304. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  305. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  306. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  307. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  308. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  309. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  310. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  311. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  312. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  313. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  314. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  315. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  316. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  317. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  318. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  319. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  320. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  321. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  322. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  323. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  324. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  325. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  326. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  327. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  328. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  329. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  330. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  331. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  332. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  333. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  334. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  335. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  336. package/templates/nx/specs/Playground/suite.md +0 -9
  337. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  338. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  339. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  340. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  341. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  342. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  343. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  344. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  345. package/templates/nx/tsconfig.spectra.android.json +0 -39
  346. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  347. package/templates/nx/tsconfig.spectra.json +0 -25
  348. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  349. package/templates/nx/tsconfig.spectra.web.json +0 -45
  350. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  351. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  352. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  353. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  354. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  355. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  356. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  357. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  358. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  359. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  360. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  361. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  362. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  363. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  364. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  365. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -0,0 +1,595 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ZodError } from 'zod';
4
+ import { areTitlesAligned, extractDeclaredStepsFromSpec, extractModuleExports, extractSymbolsFromTestCode, } from './extractor.js';
5
+ import { isInsideSharedLibrary } from './discovery.js';
6
+ import { parseMarkdownFrontmatter } from './parser.js';
7
+ import { SpecFrontmatterSchema, validateSpecSections } from './schemas/spec.schema.js';
8
+ import { SuiteFrontmatterSchema, validateSuiteSections } from './schemas/suite.schema.js';
9
+ export class SpecLinter {
10
+ /**
11
+ * Validates a single spec.md file.
12
+ */
13
+ static validateSpecFile(filePath, contentOverride, companionCodeOverrides) {
14
+ const diagnostics = [];
15
+ if (contentOverride === undefined && !fs.existsSync(filePath)) {
16
+ return {
17
+ diagnostics: [
18
+ {
19
+ code: 'testspectra/parse-error',
20
+ severity: 'error',
21
+ message: `File does not exist: ${filePath}`,
22
+ filePath,
23
+ line: 1,
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ const content = contentOverride !== undefined ? contentOverride : fs.readFileSync(filePath, 'utf-8');
29
+ const parsed = parseMarkdownFrontmatter(content, filePath);
30
+ diagnostics.push(...parsed.diagnostics);
31
+ if (!parsed.hasFrontmatter || !parsed.data) {
32
+ return { diagnostics };
33
+ }
34
+ const data = parsed.data;
35
+ const id = typeof data.id === 'string' ? data.id : undefined;
36
+ const idLine = parsed.keyLineMap.get('id') || 1;
37
+ try {
38
+ SpecFrontmatterSchema.parse(data);
39
+ }
40
+ catch (err) {
41
+ if (err instanceof ZodError) {
42
+ for (const issue of err.issues) {
43
+ const field = issue.path[0] ? String(issue.path[0]) : '';
44
+ const line = parsed.keyLineMap.get(field) || 1;
45
+ if (issue.code === 'invalid_enum_value') {
46
+ diagnostics.push({
47
+ code: 'testspectra/invalid-frontmatter-enum',
48
+ severity: 'error',
49
+ message: issue.message,
50
+ filePath,
51
+ line,
52
+ });
53
+ }
54
+ else if (issue.code === 'invalid_type' &&
55
+ (issue.received === 'undefined' || issue.message.toLowerCase().includes('missing required'))) {
56
+ diagnostics.push({
57
+ code: 'testspectra/missing-frontmatter',
58
+ severity: 'error',
59
+ message: issue.message,
60
+ filePath,
61
+ line: 1,
62
+ });
63
+ }
64
+ else if (['priority', 'caseType', 'status'].includes(field)) {
65
+ diagnostics.push({
66
+ code: 'testspectra/invalid-frontmatter-enum',
67
+ severity: 'error',
68
+ message: issue.message,
69
+ filePath,
70
+ line,
71
+ });
72
+ }
73
+ else {
74
+ diagnostics.push({
75
+ code: 'testspectra/missing-frontmatter',
76
+ severity: 'error',
77
+ message: issue.message,
78
+ filePath,
79
+ line,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ else {
85
+ diagnostics.push({
86
+ code: 'testspectra/parse-error',
87
+ severity: 'error',
88
+ message: err.message,
89
+ filePath,
90
+ line: 1,
91
+ });
92
+ }
93
+ }
94
+ // Validate markdown body sections
95
+ const sectionDiagnostics = validateSpecSections(parsed.body, filePath, parsed.bodyStartLine);
96
+ diagnostics.push(...sectionDiagnostics);
97
+ // ─────────────────────────────────────────────────────────────────────────
98
+ // Stage 3 & 4: AST Step Parity & Test Structure Enforcement
99
+ // ─────────────────────────────────────────────────────────────────────────
100
+ const declaredSteps = extractDeclaredStepsFromSpec(parsed.body, parsed.bodyStartLine);
101
+ const specDir = path.dirname(filePath);
102
+ let companionTestFiles = [];
103
+ try {
104
+ const entries = fs.readdirSync(specDir, { withFileTypes: true });
105
+ companionTestFiles = entries
106
+ .filter((e) => e.isFile() &&
107
+ (e.name.endsWith('.test.ts') ||
108
+ e.name.endsWith('.web.ts') ||
109
+ e.name.endsWith('.mobile.ts') ||
110
+ e.name.endsWith('.android.ts') ||
111
+ e.name.endsWith('.ios.ts') ||
112
+ e.name.endsWith('.common.ts')))
113
+ .map((e) => path.join(specDir, e.name));
114
+ }
115
+ catch { }
116
+ if (companionCodeOverrides) {
117
+ const overrideKeys = companionCodeOverrides instanceof Map
118
+ ? Array.from(companionCodeOverrides.keys())
119
+ : Object.keys(companionCodeOverrides);
120
+ for (const overridePath of overrideKeys) {
121
+ if (path.dirname(overridePath) === specDir && !companionTestFiles.includes(overridePath)) {
122
+ companionTestFiles.push(overridePath);
123
+ }
124
+ }
125
+ }
126
+ if (companionTestFiles.length === 0) {
127
+ if (declaredSteps.length > 0) {
128
+ for (const step of declaredSteps) {
129
+ diagnostics.push({
130
+ code: 'testspectra/missing-step-call',
131
+ severity: 'error',
132
+ message: `Parity Error: Step "@step:${step.name}" declared in spec.md line ${step.line} is missing in test code (no companion test files found)`,
133
+ filePath,
134
+ line: step.line,
135
+ });
136
+ }
137
+ }
138
+ }
139
+ else {
140
+ const coverage = data.coverage;
141
+ for (const testFilePath of companionTestFiles) {
142
+ const testFileName = path.basename(testFilePath);
143
+ let testCode = '';
144
+ if (companionCodeOverrides) {
145
+ if (companionCodeOverrides instanceof Map) {
146
+ testCode = companionCodeOverrides.get(testFilePath) || '';
147
+ }
148
+ else {
149
+ testCode = companionCodeOverrides[testFilePath] || '';
150
+ }
151
+ }
152
+ if (!testCode) {
153
+ try {
154
+ testCode = fs.readFileSync(testFilePath, 'utf-8');
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ }
160
+ const symbols = extractSymbolsFromTestCode(testCode, testFilePath);
161
+ // testspectra/no-manual-import: Manual import statement detected (must be zero-import)
162
+ for (const imp of symbols.importStatements) {
163
+ diagnostics.push({
164
+ code: 'testspectra/no-manual-import',
165
+ severity: 'error',
166
+ message: 'Manual imports forbidden; all TestSpectra APIs, Page Objects, Steps, and Fixtures are ambient globals',
167
+ filePath: testFilePath,
168
+ line: imp.line,
169
+ column: imp.column,
170
+ });
171
+ }
172
+ // testspectra/no-test-block: forbidden test() calls detected
173
+ for (const tb of symbols.testBlocks) {
174
+ diagnostics.push({
175
+ code: 'testspectra/no-test-block',
176
+ severity: 'error',
177
+ message: 'test() is forbidden; TestSpectra test scripts must strictly use it() instead of test()',
178
+ filePath: testFilePath,
179
+ line: tb.line,
180
+ column: tb.column,
181
+ });
182
+ }
183
+ // testspectra/single-it-required: Multiple it() blocks (or 0 it blocks) detected in a single test case script
184
+ if (symbols.itBlocks.length === 0) {
185
+ if (symbols.testBlocks.length === 0) {
186
+ diagnostics.push({
187
+ code: 'testspectra/single-it-required',
188
+ severity: 'error',
189
+ message: 'Test case script must contain exactly one it() block (found 0)',
190
+ filePath: testFilePath,
191
+ line: 1,
192
+ });
193
+ }
194
+ }
195
+ else if (symbols.itBlocks.length > 1) {
196
+ for (let i = 1; i < symbols.itBlocks.length; i++) {
197
+ const extra = symbols.itBlocks[i];
198
+ diagnostics.push({
199
+ code: 'testspectra/single-it-required',
200
+ severity: 'error',
201
+ message: `Multiple it() blocks detected (${symbols.itBlocks.length}); each test script must contain exactly one it() block`,
202
+ filePath: testFilePath,
203
+ line: extra.line,
204
+ column: extra.column,
205
+ });
206
+ }
207
+ }
208
+ // testspectra/no-describe-block: describe(...) block detected (suites are strictly structured via directory hierarchy)
209
+ for (const desc of symbols.describeBlocks) {
210
+ diagnostics.push({
211
+ code: 'testspectra/no-describe-block',
212
+ severity: 'error',
213
+ message: 'describe() blocks are forbidden; test suites are structured via directory hierarchy with suite.md',
214
+ filePath: testFilePath,
215
+ line: desc.line,
216
+ column: desc.column,
217
+ });
218
+ }
219
+ // testspectra/title-mismatch: it("...") title in test script differs from title declared in spec.md frontmatter
220
+ if (symbols.itBlocks.length > 0 && typeof data.title === 'string') {
221
+ const itTitle = symbols.itBlocks[0].title;
222
+ if (!areTitlesAligned(data.title, itTitle)) {
223
+ diagnostics.push({
224
+ code: 'testspectra/title-mismatch',
225
+ severity: 'warning',
226
+ message: `Test title "${itTitle}" in ${testFileName}:L${symbols.itBlocks[0].line} does not match spec.md title "${data.title}"`,
227
+ filePath: testFilePath,
228
+ line: symbols.itBlocks[0].line,
229
+ column: symbols.itBlocks[0].column,
230
+ });
231
+ }
232
+ }
233
+ // Determine if platform coverage is explicitly marked manual or unsupported
234
+ let isPlatformAutomated = true;
235
+ if (coverage) {
236
+ if (testFileName.startsWith('web.') && (coverage.web === 'manual' || coverage.web === 'unsupported')) {
237
+ isPlatformAutomated = false;
238
+ }
239
+ else if ((testFileName.startsWith('mobile.') ||
240
+ testFileName.startsWith('android.') ||
241
+ testFileName.startsWith('ios.')) &&
242
+ (coverage.mobile === 'manual' || coverage.mobile === 'unsupported')) {
243
+ isPlatformAutomated = false;
244
+ }
245
+ }
246
+ const codeStepNames = new Set(symbols.stepCalls.map((s) => s.name));
247
+ // testspectra/missing-step-call: Step declared as @step:<name> in spec.md is absent in companion test script
248
+ if (isPlatformAutomated) {
249
+ for (const declaredStep of declaredSteps) {
250
+ if (!codeStepNames.has(declaredStep.name)) {
251
+ diagnostics.push({
252
+ code: 'testspectra/missing-step-call',
253
+ severity: 'error',
254
+ message: `Parity Error: Step "@step:${declaredStep.name}" declared in spec.md line ${declaredStep.line} is missing in ${testFileName}`,
255
+ filePath,
256
+ line: declaredStep.line,
257
+ });
258
+ }
259
+ }
260
+ }
261
+ // testspectra/undocumented-step: Step.<name>() called in test script is not documented with @step:<name> in spec.md
262
+ const declaredStepNames = new Set(declaredSteps.map((s) => s.name));
263
+ for (const stepCall of symbols.stepCalls) {
264
+ if (!declaredStepNames.has(stepCall.name)) {
265
+ diagnostics.push({
266
+ code: 'testspectra/undocumented-step',
267
+ severity: 'warning',
268
+ message: `Step "${stepCall.name}" called in ${testFileName}:L${stepCall.line} is not documented with @step:${stepCall.name} in spec.md`,
269
+ filePath: testFilePath,
270
+ line: stepCall.line,
271
+ column: stepCall.column,
272
+ });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ return { diagnostics, id, idLine };
278
+ }
279
+ /**
280
+ * Validates a single suite.md file.
281
+ */
282
+ static validateSuiteFile(filePath, contentOverride) {
283
+ const diagnostics = [];
284
+ if (contentOverride === undefined && !fs.existsSync(filePath)) {
285
+ return {
286
+ diagnostics: [
287
+ {
288
+ code: 'testspectra/parse-error',
289
+ severity: 'error',
290
+ message: `File does not exist: ${filePath}`,
291
+ filePath,
292
+ line: 1,
293
+ },
294
+ ],
295
+ };
296
+ }
297
+ const content = contentOverride !== undefined ? contentOverride : fs.readFileSync(filePath, 'utf-8');
298
+ const parsed = parseMarkdownFrontmatter(content, filePath);
299
+ diagnostics.push(...parsed.diagnostics);
300
+ if (!parsed.hasFrontmatter || !parsed.data) {
301
+ return { diagnostics };
302
+ }
303
+ const data = parsed.data;
304
+ const id = typeof data.id === 'string' ? data.id : undefined;
305
+ const idLine = parsed.keyLineMap.get('id') || 1;
306
+ try {
307
+ SuiteFrontmatterSchema.parse(data);
308
+ }
309
+ catch (err) {
310
+ if (err instanceof ZodError) {
311
+ for (const issue of err.issues) {
312
+ const field = issue.path[0] ? String(issue.path[0]) : '';
313
+ const line = parsed.keyLineMap.get(field) || 1;
314
+ if (issue.code === 'invalid_enum_value') {
315
+ diagnostics.push({
316
+ code: 'testspectra/invalid-frontmatter-enum',
317
+ severity: 'error',
318
+ message: issue.message,
319
+ filePath,
320
+ line,
321
+ });
322
+ }
323
+ else if (issue.code === 'invalid_type' &&
324
+ (issue.received === 'undefined' || issue.message.toLowerCase().includes('missing required'))) {
325
+ diagnostics.push({
326
+ code: 'testspectra/missing-frontmatter',
327
+ severity: 'error',
328
+ message: issue.message,
329
+ filePath,
330
+ line: 1,
331
+ });
332
+ }
333
+ else if (field === 'status') {
334
+ diagnostics.push({
335
+ code: 'testspectra/invalid-frontmatter-enum',
336
+ severity: 'error',
337
+ message: issue.message,
338
+ filePath,
339
+ line,
340
+ });
341
+ }
342
+ else {
343
+ diagnostics.push({
344
+ code: 'testspectra/missing-frontmatter',
345
+ severity: 'error',
346
+ message: issue.message,
347
+ filePath,
348
+ line,
349
+ });
350
+ }
351
+ }
352
+ }
353
+ else {
354
+ diagnostics.push({
355
+ code: 'testspectra/parse-error',
356
+ severity: 'error',
357
+ message: err.message,
358
+ filePath,
359
+ line: 1,
360
+ });
361
+ }
362
+ }
363
+ // Validate markdown body sections
364
+ const sectionDiagnostics = validateSuiteSections(parsed.body, filePath, parsed.bodyStartLine);
365
+ diagnostics.push(...sectionDiagnostics);
366
+ return { diagnostics, id, idLine };
367
+ }
368
+ /**
369
+ * Validates all spec.md and suite.md files in a workspace or specific target path.
370
+ */
371
+ static validateWorkspace(workspaceDir, targetPath) {
372
+ const rootScan = targetPath ? path.resolve(workspaceDir, targetPath) : workspaceDir;
373
+ const specFiles = [];
374
+ const suiteFiles = [];
375
+ // Collect files
376
+ if (fs.existsSync(rootScan)) {
377
+ const stat = fs.statSync(rootScan);
378
+ if (stat.isFile()) {
379
+ const base = path.basename(rootScan);
380
+ if (base === 'spec.md')
381
+ specFiles.push(rootScan);
382
+ else if (base === 'suite.md')
383
+ suiteFiles.push(rootScan);
384
+ }
385
+ else {
386
+ // Directory traversal
387
+ const traverse = (dir) => {
388
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
389
+ for (const entry of entries) {
390
+ const fullPath = path.join(dir, entry.name);
391
+ if (entry.isDirectory()) {
392
+ if (entry.name === 'node_modules' ||
393
+ entry.name === 'dist' ||
394
+ entry.name === '.git' ||
395
+ entry.name === '.testspectra') {
396
+ continue;
397
+ }
398
+ traverse(fullPath);
399
+ }
400
+ else if (entry.isFile()) {
401
+ if (entry.name === 'spec.md') {
402
+ specFiles.push(fullPath);
403
+ }
404
+ else if (entry.name === 'suite.md') {
405
+ suiteFiles.push(fullPath);
406
+ }
407
+ }
408
+ }
409
+ };
410
+ // If target not specified and `specs/` exists, scan `specs/` by default
411
+ const defaultSpecsDir = path.join(rootScan, 'specs');
412
+ if (!targetPath && fs.existsSync(defaultSpecsDir)) {
413
+ traverse(defaultSpecsDir);
414
+ }
415
+ else {
416
+ traverse(rootScan);
417
+ }
418
+ }
419
+ }
420
+ const allDiagnostics = [];
421
+ const idTracker = new Map();
422
+ let passedSpecs = 0;
423
+ let passedSuites = 0;
424
+ // Validate suites first
425
+ for (const suitePath of suiteFiles) {
426
+ const { diagnostics, id, idLine } = this.validateSuiteFile(suitePath);
427
+ let fileValid = diagnostics.filter((d) => d.severity === 'error').length === 0;
428
+ if (id) {
429
+ if (idTracker.has(id)) {
430
+ const prev = idTracker.get(id);
431
+ diagnostics.push({
432
+ code: 'testspectra/duplicate-spec-id',
433
+ severity: 'error',
434
+ message: `Duplicate suite id "${id}" detected across the workspace (first seen in ${path.relative(workspaceDir, prev.filePath)})`,
435
+ filePath: suitePath,
436
+ line: idLine || 1,
437
+ });
438
+ fileValid = false;
439
+ }
440
+ else {
441
+ idTracker.set(id, { filePath: suitePath, line: idLine || 1 });
442
+ }
443
+ }
444
+ if (fileValid) {
445
+ passedSuites++;
446
+ }
447
+ allDiagnostics.push(...diagnostics);
448
+ }
449
+ // Validate specs
450
+ for (const specPath of specFiles) {
451
+ const { diagnostics, id, idLine } = this.validateSpecFile(specPath);
452
+ let fileValid = diagnostics.filter((d) => d.severity === 'error').length === 0;
453
+ if (id) {
454
+ if (idTracker.has(id)) {
455
+ const prev = idTracker.get(id);
456
+ diagnostics.push({
457
+ code: 'testspectra/duplicate-spec-id',
458
+ severity: 'error',
459
+ message: `Duplicate test case id "${id}" detected across the workspace (first seen in ${path.relative(workspaceDir, prev.filePath)})`,
460
+ filePath: specPath,
461
+ line: idLine || 1,
462
+ });
463
+ fileValid = false;
464
+ }
465
+ else {
466
+ idTracker.set(id, { filePath: specPath, line: idLine || 1 });
467
+ }
468
+ }
469
+ if (fileValid) {
470
+ passedSpecs++;
471
+ }
472
+ allDiagnostics.push(...diagnostics);
473
+ }
474
+ // ─────────────────────────────────────────────────────────────────────────
475
+ // testspectra/no-specs-in-shared-lib: Specs or test scripts detected inside shared testing library
476
+ // ─────────────────────────────────────────────────────────────────────────
477
+ for (const specPath of specFiles) {
478
+ if (isInsideSharedLibrary(specPath)) {
479
+ allDiagnostics.push({
480
+ code: 'testspectra/no-specs-in-shared-lib',
481
+ severity: 'error',
482
+ message: `Test specification detected inside shared testing library; specs must be placed in feature package directories`,
483
+ filePath: specPath,
484
+ line: 1,
485
+ });
486
+ }
487
+ }
488
+ for (const suitePath of suiteFiles) {
489
+ if (isInsideSharedLibrary(suitePath)) {
490
+ allDiagnostics.push({
491
+ code: 'testspectra/no-specs-in-shared-lib',
492
+ severity: 'error',
493
+ message: `Suite specification detected inside shared testing library; suites must be placed in feature package directories`,
494
+ filePath: suitePath,
495
+ line: 1,
496
+ });
497
+ }
498
+ }
499
+ // ─────────────────────────────────────────────────────────────────────────
500
+ // testspectra/no-manual-import (extended) & testspectra/require-anonymous-default-export: Scan shared scripts workspace-wide
501
+ // ─────────────────────────────────────────────────────────────────────────
502
+ const sharedScriptExtensions = ['.step.ts', '.action.ts', '.hook.ts'];
503
+ const collectSharedScripts = (dir) => {
504
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
505
+ for (const entry of entries) {
506
+ const fullPath = path.join(dir, entry.name);
507
+ if (entry.isDirectory()) {
508
+ if (entry.name === 'node_modules' ||
509
+ entry.name === 'dist' ||
510
+ entry.name === '.git' ||
511
+ entry.name === '.testspectra') {
512
+ continue;
513
+ }
514
+ collectSharedScripts(fullPath);
515
+ }
516
+ else if (entry.isFile()) {
517
+ const isSharedScript = sharedScriptExtensions.some((ext) => entry.name.endsWith(ext));
518
+ if (!isSharedScript)
519
+ continue;
520
+ let content = '';
521
+ try {
522
+ content = fs.readFileSync(fullPath, 'utf-8');
523
+ }
524
+ catch {
525
+ continue;
526
+ }
527
+ // testspectra/no-manual-import: Check for manual imports in non-test scripts
528
+ const symbols = extractSymbolsFromTestCode(content, fullPath);
529
+ for (const imp of symbols.importStatements) {
530
+ allDiagnostics.push({
531
+ code: 'testspectra/no-manual-import',
532
+ severity: 'error',
533
+ message: 'Manual imports forbidden; all TestSpectra APIs, Page Objects, Steps, and Fixtures are ambient globals',
534
+ filePath: fullPath,
535
+ line: imp.line,
536
+ column: imp.column,
537
+ });
538
+ }
539
+ // testspectra/require-anonymous-default-export: Check for anonymous async default export
540
+ const moduleExports = extractModuleExports(content, fullPath);
541
+ // testspectra/require-anonymous-default-export: Named exports are forbidden
542
+ for (const ne of moduleExports.namedExportLines) {
543
+ allDiagnostics.push({
544
+ code: 'testspectra/require-anonymous-default-export',
545
+ severity: 'error',
546
+ message: `Named export "${ne.name}" is forbidden; shared modules must use anonymous default export only`,
547
+ filePath: fullPath,
548
+ line: ne.line,
549
+ column: ne.column,
550
+ });
551
+ }
552
+ if (!moduleExports.hasAnonymousAsyncDefaultExport && moduleExports.namedExportLines.length === 0) {
553
+ allDiagnostics.push({
554
+ code: 'testspectra/require-anonymous-default-export',
555
+ severity: 'error',
556
+ message: 'Shared step/action/hook must export an anonymous async function as default export',
557
+ filePath: fullPath,
558
+ line: 1,
559
+ });
560
+ }
561
+ // testspectra/require-anonymous-default-export: Test blocks are forbidden in shared modules
562
+ for (const tb of moduleExports.testBlocks) {
563
+ allDiagnostics.push({
564
+ code: 'testspectra/require-anonymous-default-export',
565
+ severity: 'error',
566
+ message: `${tb.name}() block is forbidden inside shared step/action/hook modules`,
567
+ filePath: fullPath,
568
+ line: tb.line,
569
+ column: tb.column,
570
+ });
571
+ }
572
+ }
573
+ }
574
+ };
575
+ try {
576
+ collectSharedScripts(rootScan);
577
+ }
578
+ catch { }
579
+ const errorCount = allDiagnostics.filter((d) => d.severity === 'error').length;
580
+ const warningCount = allDiagnostics.filter((d) => d.severity === 'warning').length;
581
+ const stats = {
582
+ totalSpecs: specFiles.length,
583
+ totalSuites: suiteFiles.length,
584
+ passedSpecs,
585
+ passedSuites,
586
+ errorCount,
587
+ warningCount,
588
+ };
589
+ return {
590
+ valid: errorCount === 0,
591
+ diagnostics: allDiagnostics,
592
+ stats,
593
+ };
594
+ }
595
+ }
@@ -0,0 +1,28 @@
1
+ export type DiagnosticSeverity = 'error' | 'warning';
2
+ export type LintDiagnosticCode = 'testspectra/parse-error' | 'testspectra/missing-frontmatter' | 'testspectra/invalid-frontmatter-enum' | 'testspectra/missing-step-call' | 'testspectra/undocumented-step' | 'testspectra/title-mismatch' | 'testspectra/no-manual-import' | 'testspectra/single-it-required' | 'testspectra/no-test-block' | 'testspectra/no-describe-block' | 'testspectra/duplicate-spec-id' | 'testspectra/no-specs-in-shared-lib' | 'testspectra/require-anonymous-default-export';
3
+ export interface LintDiagnostic {
4
+ code: LintDiagnosticCode;
5
+ severity: DiagnosticSeverity;
6
+ message: string;
7
+ filePath: string;
8
+ line: number;
9
+ column?: number;
10
+ }
11
+ export interface SpecLintStats {
12
+ totalSpecs: number;
13
+ totalSuites: number;
14
+ passedSpecs: number;
15
+ passedSuites: number;
16
+ errorCount: number;
17
+ warningCount: number;
18
+ }
19
+ export interface SpecLintResult {
20
+ valid: boolean;
21
+ diagnostics: LintDiagnostic[];
22
+ stats: SpecLintStats;
23
+ }
24
+ export interface LintOptions {
25
+ strict?: boolean;
26
+ json?: boolean;
27
+ cwd?: string;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};