@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 @@
1
+ export * from './refactor-engine.js';
@@ -0,0 +1 @@
1
+ export * from './refactor-engine.js';
@@ -0,0 +1,50 @@
1
+ export type RefactorType = 'page-object' | 'step' | 'action' | 'fixture';
2
+ export interface RenameOptions {
3
+ workspaceRoot: string;
4
+ type: RefactorType;
5
+ oldName: string;
6
+ newName: string;
7
+ dryRun?: boolean;
8
+ skipPathRename?: boolean;
9
+ }
10
+ export interface RefactorResult {
11
+ status: 'success' | 'failed';
12
+ entityType: RefactorType;
13
+ oldSymbol: string;
14
+ newSymbol: string;
15
+ modifiedFiles: string[];
16
+ occurrencesReplaced: number;
17
+ renamedPaths?: Array<{
18
+ from: string;
19
+ to: string;
20
+ }>;
21
+ error?: string;
22
+ }
23
+ export declare const ENTITY_NAMING_RULES: Record<RefactorType, {
24
+ regex: RegExp;
25
+ convention: string;
26
+ }>;
27
+ export declare const SUPPORTED_FIXTURE_EXTENSIONS: string[];
28
+ export declare class RefactorEngine {
29
+ /**
30
+ * Deterministically parses testing entity type and symbol name from a filesystem path.
31
+ * Supports directory paths, file paths, and paths with trailing slashes.
32
+ */
33
+ static parseEntityFromPath(filePath: string): {
34
+ type: RefactorType;
35
+ symbol: string;
36
+ } | null;
37
+ /**
38
+ * Deterministically parses source and target paths, validates entity types,
39
+ * and executes symbol refactoring across the workspace.
40
+ */
41
+ static renameFromPaths(options: {
42
+ workspaceRoot: string;
43
+ fromPath: string;
44
+ toPath: string;
45
+ dryRun?: boolean;
46
+ skipPathRename?: boolean;
47
+ }): Promise<RefactorResult>;
48
+ private static findFiles;
49
+ static renameSymbol(options: RenameOptions): Promise<RefactorResult>;
50
+ }
@@ -0,0 +1,587 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { TypeGenerator } from '../generator/type-generator.js';
4
+ export const ENTITY_NAMING_RULES = {
5
+ 'page-object': {
6
+ regex: /^[A-Z][a-zA-Z0-9]*$/,
7
+ convention: 'PascalCase (e.g. "LoginPage", "AuthPage")',
8
+ },
9
+ step: {
10
+ regex: /^[a-z][a-zA-Z0-9]*$/,
11
+ convention: 'camelCase (e.g. "loginUser", "loginAsAdmin")',
12
+ },
13
+ action: {
14
+ regex: /^[a-z][a-zA-Z0-9]*$/,
15
+ convention: 'camelCase (e.g. "customSwipe", "swipeTop")',
16
+ },
17
+ fixture: {
18
+ regex: /^[a-z][a-zA-Z0-9_]*$/,
19
+ convention: 'camelCase or snake_case starting with lowercase (e.g. "users", "apiPayloads", "user_accounts")',
20
+ },
21
+ };
22
+ export const SUPPORTED_FIXTURE_EXTENSIONS = ['.json', '.csv', '.yaml', '.yml', '.ts', '.js'];
23
+ export class RefactorEngine {
24
+ /**
25
+ * Deterministically parses testing entity type and symbol name from a filesystem path.
26
+ * Supports directory paths, file paths, and paths with trailing slashes.
27
+ */
28
+ static parseEntityFromPath(filePath) {
29
+ if (!filePath || typeof filePath !== 'string')
30
+ return null;
31
+ const normalized = filePath.replace(/\\/g, '/').replace(/\/+$/, '');
32
+ const parts = normalized.split('/').filter(Boolean);
33
+ // 1. Page Objects: page-objects/<name> or page-objects/<name>/web.ts or page-objects/<name>.ts
34
+ const poIdx = parts.findIndex((p) => p === 'page-objects');
35
+ if (poIdx !== -1 && poIdx + 1 < parts.length) {
36
+ const rawSymbol = parts[poIdx + 1];
37
+ const symbol = path.parse(rawSymbol).name;
38
+ if (symbol)
39
+ return { type: 'page-object', symbol };
40
+ }
41
+ // 2. Steps: support/steps/<name> or steps/<name> or steps/<name>/step.md or steps/<name>.ts
42
+ const stepIdx = parts.findIndex((p) => p === 'steps');
43
+ if (stepIdx !== -1 && stepIdx + 1 < parts.length) {
44
+ const rawSymbol = parts[stepIdx + 1];
45
+ const symbol = path.parse(rawSymbol).name;
46
+ if (symbol)
47
+ return { type: 'step', symbol };
48
+ }
49
+ // 3. Actions: support/actions/<name> or actions/<name> or actions/<name>/action.ts or actions/<name>.ts
50
+ const actionIdx = parts.findIndex((p) => p === 'actions');
51
+ if (actionIdx !== -1 && actionIdx + 1 < parts.length) {
52
+ const rawSymbol = parts[actionIdx + 1];
53
+ const symbol = path.parse(rawSymbol).name;
54
+ if (symbol)
55
+ return { type: 'action', symbol };
56
+ }
57
+ // 4. Fixtures: fixtures/<name>.<ext> or fixtures/nested/<name>.<ext> or fixtures/<name>
58
+ const fixIdx = parts.findIndex((p) => p === 'fixtures');
59
+ if (fixIdx !== -1 && fixIdx + 1 < parts.length) {
60
+ const lastPart = parts[parts.length - 1];
61
+ const symbol = path.parse(lastPart).name;
62
+ if (symbol)
63
+ return { type: 'fixture', symbol };
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Deterministically parses source and target paths, validates entity types,
69
+ * and executes symbol refactoring across the workspace.
70
+ */
71
+ static async renameFromPaths(options) {
72
+ const { workspaceRoot, fromPath, toPath, dryRun, skipPathRename } = options;
73
+ if (!fromPath || !toPath) {
74
+ return {
75
+ status: 'failed',
76
+ entityType: 'page-object',
77
+ oldSymbol: fromPath || '',
78
+ newSymbol: toPath || '',
79
+ modifiedFiles: [],
80
+ occurrencesReplaced: 0,
81
+ error: 'Both source and target paths must be specified for path-based refactoring.',
82
+ };
83
+ }
84
+ const fromParsed = RefactorEngine.parseEntityFromPath(fromPath);
85
+ if (!fromParsed) {
86
+ return {
87
+ status: 'failed',
88
+ entityType: 'page-object',
89
+ oldSymbol: fromPath,
90
+ newSymbol: toPath,
91
+ modifiedFiles: [],
92
+ occurrencesReplaced: 0,
93
+ error: `Unable to determine entity type from source path: "${fromPath}". Path must be within page-objects/, support/steps/, support/actions/, or fixtures/.`,
94
+ };
95
+ }
96
+ const toParsed = RefactorEngine.parseEntityFromPath(toPath);
97
+ if (!toParsed) {
98
+ return {
99
+ status: 'failed',
100
+ entityType: fromParsed.type,
101
+ oldSymbol: fromParsed.symbol,
102
+ newSymbol: toPath,
103
+ modifiedFiles: [],
104
+ occurrencesReplaced: 0,
105
+ error: `Unable to determine entity type from target path: "${toPath}". Path must be within page-objects/, support/steps/, support/actions/, or fixtures/.`,
106
+ };
107
+ }
108
+ if (fromParsed.type !== toParsed.type) {
109
+ return {
110
+ status: 'failed',
111
+ entityType: fromParsed.type,
112
+ oldSymbol: fromParsed.symbol,
113
+ newSymbol: toParsed.symbol,
114
+ modifiedFiles: [],
115
+ occurrencesReplaced: 0,
116
+ error: `Entity type mismatch: source is "${fromParsed.type}" but target is "${toParsed.type}". Cross-entity refactoring is not supported.`,
117
+ };
118
+ }
119
+ return RefactorEngine.renameSymbol({
120
+ workspaceRoot,
121
+ type: fromParsed.type,
122
+ oldName: fromParsed.symbol,
123
+ newName: toParsed.symbol,
124
+ dryRun,
125
+ skipPathRename,
126
+ });
127
+ }
128
+ static findFiles(dir, extensions) {
129
+ const results = [];
130
+ if (!fs.existsSync(dir))
131
+ return results;
132
+ function walk(currentDir) {
133
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ if (entry.name === 'node_modules' ||
136
+ entry.name === 'dist' ||
137
+ entry.name === '.git' ||
138
+ entry.name === '.testspectra') {
139
+ continue;
140
+ }
141
+ const fullPath = path.join(currentDir, entry.name);
142
+ if (entry.isDirectory()) {
143
+ walk(fullPath);
144
+ }
145
+ else if (entry.isFile()) {
146
+ if (extensions.some((ext) => entry.name.endsWith(ext))) {
147
+ results.push(fullPath);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ walk(dir);
153
+ return results;
154
+ }
155
+ static async renameSymbol(options) {
156
+ const { workspaceRoot, type, oldName, newName, dryRun = false, skipPathRename = false } = options;
157
+ if (!oldName || !newName || oldName === newName) {
158
+ return {
159
+ status: 'failed',
160
+ entityType: type,
161
+ oldSymbol: oldName,
162
+ newSymbol: newName,
163
+ modifiedFiles: [],
164
+ occurrencesReplaced: 0,
165
+ error: 'Invalid or identical old/new symbol names',
166
+ };
167
+ }
168
+ let targetOldName = oldName;
169
+ let targetNewName = newName;
170
+ if (type === 'fixture') {
171
+ const parsedOld = path.parse(oldName);
172
+ if (parsedOld.ext && SUPPORTED_FIXTURE_EXTENSIONS.includes(parsedOld.ext.toLowerCase())) {
173
+ targetOldName = parsedOld.name;
174
+ }
175
+ const parsedNew = path.parse(newName);
176
+ if (parsedNew.ext && SUPPORTED_FIXTURE_EXTENSIONS.includes(parsedNew.ext.toLowerCase())) {
177
+ targetNewName = parsedNew.name;
178
+ }
179
+ }
180
+ const rule = ENTITY_NAMING_RULES[type];
181
+ if (rule && !rule.regex.test(targetNewName)) {
182
+ return {
183
+ status: 'failed',
184
+ entityType: type,
185
+ oldSymbol: oldName,
186
+ newSymbol: newName,
187
+ modifiedFiles: [],
188
+ occurrencesReplaced: 0,
189
+ error: `Invalid new symbol name "${targetNewName}". Symbol name for ${type} must follow ${rule.convention}.`,
190
+ };
191
+ }
192
+ // Phase 1: Determine file operations and replacement patterns
193
+ const plannedRenames = [];
194
+ const plannedEdits = [];
195
+ let oldSymbol = targetOldName;
196
+ let newSymbol = targetNewName;
197
+ const fixtureDirs = [
198
+ path.join(workspaceRoot, 'fixtures'),
199
+ path.join(workspaceRoot, 'support', 'fixtures'),
200
+ path.join(workspaceRoot, 'shared', 'fixtures'),
201
+ ];
202
+ function findFixtureFiles(dir) {
203
+ const files = [];
204
+ if (!fs.existsSync(dir))
205
+ return files;
206
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
207
+ for (const entry of entries) {
208
+ const fullPath = path.join(dir, entry.name);
209
+ if (entry.isDirectory()) {
210
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
211
+ files.push(...findFixtureFiles(fullPath));
212
+ }
213
+ }
214
+ else if (entry.isFile()) {
215
+ const ext = path.extname(entry.name).toLowerCase();
216
+ if (SUPPORTED_FIXTURE_EXTENSIONS.includes(ext)) {
217
+ files.push(fullPath);
218
+ }
219
+ }
220
+ }
221
+ return files;
222
+ }
223
+ // 1. Target Directory / File Discovery for Source Entity
224
+ if (type === 'page-object') {
225
+ oldSymbol = targetOldName;
226
+ newSymbol = targetNewName;
227
+ const targetPoDir = path.join(workspaceRoot, 'page-objects', targetNewName);
228
+ const poDir = path.join(workspaceRoot, 'page-objects', targetOldName);
229
+ if (fs.existsSync(poDir)) {
230
+ plannedRenames.push({ from: poDir, to: targetPoDir });
231
+ }
232
+ else if (skipPathRename && fs.existsSync(targetPoDir)) {
233
+ plannedRenames.push({ from: poDir, to: targetPoDir });
234
+ }
235
+ }
236
+ else if (type === 'step') {
237
+ oldSymbol = `Step.${targetOldName}`;
238
+ newSymbol = `Step.${targetNewName}`;
239
+ const stepCandidates = [
240
+ path.join(workspaceRoot, 'support', 'steps', targetOldName),
241
+ path.join(workspaceRoot, 'steps', targetOldName),
242
+ ];
243
+ for (const cand of stepCandidates) {
244
+ if (fs.existsSync(cand)) {
245
+ const parent = path.dirname(cand);
246
+ plannedRenames.push({ from: cand, to: path.join(parent, targetNewName) });
247
+ }
248
+ else if (skipPathRename) {
249
+ const parent = path.dirname(cand);
250
+ const targetCand = path.join(parent, targetNewName);
251
+ if (fs.existsSync(targetCand)) {
252
+ plannedRenames.push({ from: cand, to: targetCand });
253
+ }
254
+ }
255
+ }
256
+ }
257
+ else if (type === 'action') {
258
+ oldSymbol = `Spectra.${targetOldName}`;
259
+ newSymbol = `Spectra.${targetNewName}`;
260
+ const actionCandidates = [
261
+ path.join(workspaceRoot, 'support', 'actions', targetOldName),
262
+ path.join(workspaceRoot, 'actions', targetOldName),
263
+ ];
264
+ for (const cand of actionCandidates) {
265
+ if (fs.existsSync(cand)) {
266
+ const parent = path.dirname(cand);
267
+ plannedRenames.push({ from: cand, to: path.join(parent, targetNewName) });
268
+ }
269
+ else if (skipPathRename) {
270
+ const parent = path.dirname(cand);
271
+ const targetCand = path.join(parent, targetNewName);
272
+ if (fs.existsSync(targetCand)) {
273
+ plannedRenames.push({ from: cand, to: targetCand });
274
+ }
275
+ }
276
+ }
277
+ }
278
+ else if (type === 'fixture') {
279
+ oldSymbol = `Fixture.${targetOldName}`;
280
+ newSymbol = `Fixture.${targetNewName}`;
281
+ for (const dir of fixtureDirs) {
282
+ const fixtureFiles = findFixtureFiles(dir);
283
+ for (const file of fixtureFiles) {
284
+ const parsed = path.parse(file);
285
+ if (parsed.name === targetOldName) {
286
+ plannedRenames.push({
287
+ from: file,
288
+ to: path.join(parsed.dir, `${targetNewName}${parsed.ext}`),
289
+ });
290
+ }
291
+ else if (skipPathRename && parsed.name === targetNewName) {
292
+ plannedRenames.push({
293
+ from: path.join(parsed.dir, `${targetOldName}${parsed.ext}`),
294
+ to: file,
295
+ });
296
+ }
297
+ }
298
+ }
299
+ }
300
+ // 2. Scan Codebases and AST replacements in memory
301
+ const searchDirs = [
302
+ path.join(workspaceRoot, 'specs'),
303
+ path.join(workspaceRoot, 'suites'),
304
+ path.join(workspaceRoot, 'page-objects'),
305
+ path.join(workspaceRoot, 'support'),
306
+ path.join(workspaceRoot, 'steps'),
307
+ path.join(workspaceRoot, 'actions'),
308
+ path.join(workspaceRoot, 'global-hooks'),
309
+ path.join(workspaceRoot, 'packages'),
310
+ path.join(workspaceRoot, 'apps'),
311
+ path.join(workspaceRoot, 'shared'),
312
+ ];
313
+ const allCodeFiles = searchDirs.flatMap((dir) => this.findFiles(dir, ['.ts', '.js', '.md']));
314
+ const uniqueFiles = Array.from(new Set(allCodeFiles));
315
+ let totalOccurrences = 0;
316
+ for (const filePath of uniqueFiles) {
317
+ const originalContent = fs.readFileSync(filePath, 'utf-8');
318
+ let modified = originalContent;
319
+ let count = 0;
320
+ if (type === 'page-object') {
321
+ // Replace `class OldName` -> `class NewName`
322
+ const classReg = new RegExp(`\\bclass\\s+${oldName}\\b`, 'g');
323
+ const classMatches = (modified.match(classReg) || []).length;
324
+ if (classMatches > 0) {
325
+ modified = modified.replace(classReg, `class ${newName}`);
326
+ count += classMatches;
327
+ }
328
+ // Replace `new OldName` -> `new NewName` (e.g. `export default new OldName()`, `new OldName(...)`)
329
+ const newReg = new RegExp(`\\bnew\\s+${oldName}\\b`, 'g');
330
+ const newMatches = (modified.match(newReg) || []).length;
331
+ if (newMatches > 0) {
332
+ modified = modified.replace(newReg, `new ${newName}`);
333
+ count += newMatches;
334
+ }
335
+ // Replace `export default OldName` -> `export default NewName`
336
+ const exportDefaultReg = new RegExp(`\\bexport\\s+default\\s+${oldName}\\b`, 'g');
337
+ const exportDefaultMatches = (modified.match(exportDefaultReg) || []).length;
338
+ if (exportDefaultMatches > 0) {
339
+ modified = modified.replace(exportDefaultReg, `export default ${newName}`);
340
+ count += exportDefaultMatches;
341
+ }
342
+ // Replace `OldName.` -> `NewName.`
343
+ const refReg = new RegExp(`\\b${oldName}\\.`, 'g');
344
+ const refMatches = (modified.match(refReg) || []).length;
345
+ if (refMatches > 0) {
346
+ modified = modified.replace(refReg, `${newName}.`);
347
+ count += refMatches;
348
+ }
349
+ }
350
+ else if (type === 'step') {
351
+ // Replace `Step.oldStep(` -> `Step.newStep(`
352
+ const stepCallReg = new RegExp(`\\bStep\\.${oldName}\\b`, 'g');
353
+ const stepCallMatches = (modified.match(stepCallReg) || []).length;
354
+ if (stepCallMatches > 0) {
355
+ modified = modified.replace(stepCallReg, `Step.${newName}`);
356
+ count += stepCallMatches;
357
+ }
358
+ // Replace `@step:oldStep` -> `@step:newStep` in markdown specs
359
+ const directiveReg = new RegExp(`@step:${oldName}\\b`, 'g');
360
+ const dirMatches = (modified.match(directiveReg) || []).length;
361
+ if (dirMatches > 0) {
362
+ modified = modified.replace(directiveReg, `@step:${newName}`);
363
+ count += dirMatches;
364
+ }
365
+ }
366
+ else if (type === 'action') {
367
+ // Replace `Spectra.oldAction(` -> `Spectra.newAction(`
368
+ const actionReg = new RegExp(`\\bSpectra\\.${oldName}\\b`, 'g');
369
+ const actionMatches = (modified.match(actionReg) || []).length;
370
+ if (actionMatches > 0) {
371
+ modified = modified.replace(actionReg, `Spectra.${newName}`);
372
+ count += actionMatches;
373
+ }
374
+ }
375
+ else if (type === 'fixture') {
376
+ // Replace `Fixture.oldFixture` -> `Fixture.newFixture`
377
+ const fixReg = new RegExp(`\\bFixture\\.${targetOldName}\\b`, 'g');
378
+ const fixMatches = (modified.match(fixReg) || []).length;
379
+ if (fixMatches > 0) {
380
+ modified = modified.replace(fixReg, `Fixture.${targetNewName}`);
381
+ count += fixMatches;
382
+ }
383
+ // Also replace file references with supported extensions (e.g. 'users.csv', 'users.yaml', etc.)
384
+ const extPattern = SUPPORTED_FIXTURE_EXTENSIONS.map((e) => e.replace('.', '')).join('|');
385
+ const fixFileReg = new RegExp(`(?<!Fixture\\.)\\b${targetOldName}\\.(${extPattern})\\b`, 'g');
386
+ const fixFileMatches = (modified.match(fixFileReg) || []).length;
387
+ if (fixFileMatches > 0) {
388
+ modified = modified.replace(fixFileReg, `${targetNewName}.$1`);
389
+ count += fixFileMatches;
390
+ }
391
+ }
392
+ if (count > 0) {
393
+ totalOccurrences += count;
394
+ plannedEdits.push({
395
+ filePath,
396
+ originalContent,
397
+ newContent: modified,
398
+ count,
399
+ });
400
+ }
401
+ }
402
+ // Source Existence Guard:
403
+ // If no filesystem files/directories match the source symbol AND no code occurrences were found,
404
+ // the source symbol does not exist anywhere in the workspace.
405
+ if (plannedRenames.length === 0 && totalOccurrences === 0) {
406
+ return {
407
+ status: 'failed',
408
+ entityType: type,
409
+ oldSymbol,
410
+ newSymbol,
411
+ modifiedFiles: [],
412
+ occurrencesReplaced: 0,
413
+ error: `Source ${type} "${oldName}" was not found in the workspace (no matching files or symbol occurrences).`,
414
+ };
415
+ }
416
+ // Target Collision Guards (Priority 2 - checked only if source entity exists and skipPathRename is false)
417
+ if (!skipPathRename) {
418
+ if (type === 'page-object') {
419
+ const targetPoDir = path.join(workspaceRoot, 'page-objects', targetNewName);
420
+ if (fs.existsSync(targetPoDir)) {
421
+ return {
422
+ status: 'failed',
423
+ entityType: type,
424
+ oldSymbol,
425
+ newSymbol,
426
+ modifiedFiles: [],
427
+ occurrencesReplaced: 0,
428
+ error: `Target page-object "${targetNewName}" already exists at "${path.relative(workspaceRoot, targetPoDir)}"`,
429
+ };
430
+ }
431
+ }
432
+ else if (type === 'step') {
433
+ const targetStepCandidates = [
434
+ path.join(workspaceRoot, 'support', 'steps', targetNewName),
435
+ path.join(workspaceRoot, 'steps', targetNewName),
436
+ ];
437
+ for (const cand of targetStepCandidates) {
438
+ if (fs.existsSync(cand)) {
439
+ return {
440
+ status: 'failed',
441
+ entityType: type,
442
+ oldSymbol,
443
+ newSymbol,
444
+ modifiedFiles: [],
445
+ occurrencesReplaced: 0,
446
+ error: `Target step "${targetNewName}" already exists at "${path.relative(workspaceRoot, cand)}"`,
447
+ };
448
+ }
449
+ }
450
+ }
451
+ else if (type === 'action') {
452
+ const targetActionCandidates = [
453
+ path.join(workspaceRoot, 'support', 'actions', targetNewName),
454
+ path.join(workspaceRoot, 'actions', targetNewName),
455
+ ];
456
+ for (const cand of targetActionCandidates) {
457
+ if (fs.existsSync(cand)) {
458
+ return {
459
+ status: 'failed',
460
+ entityType: type,
461
+ oldSymbol,
462
+ newSymbol,
463
+ modifiedFiles: [],
464
+ occurrencesReplaced: 0,
465
+ error: `Target action "${targetNewName}" already exists at "${path.relative(workspaceRoot, cand)}"`,
466
+ };
467
+ }
468
+ }
469
+ }
470
+ else if (type === 'fixture') {
471
+ for (const dir of fixtureDirs) {
472
+ const fixtureFiles = findFixtureFiles(dir);
473
+ for (const file of fixtureFiles) {
474
+ const parsed = path.parse(file);
475
+ if (parsed.name === targetNewName) {
476
+ return {
477
+ status: 'failed',
478
+ entityType: type,
479
+ oldSymbol,
480
+ newSymbol,
481
+ modifiedFiles: [],
482
+ occurrencesReplaced: 0,
483
+ error: `Target fixture "${targetNewName}" already exists at "${path.relative(workspaceRoot, file)}"`,
484
+ };
485
+ }
486
+ }
487
+ }
488
+ }
489
+ // Universal pre-commit collision guard
490
+ for (const rn of plannedRenames) {
491
+ if (fs.existsSync(rn.to)) {
492
+ return {
493
+ status: 'failed',
494
+ entityType: type,
495
+ oldSymbol,
496
+ newSymbol,
497
+ modifiedFiles: [],
498
+ occurrencesReplaced: 0,
499
+ error: `Target destination already exists on disk: "${path.relative(workspaceRoot, rn.to)}"`,
500
+ };
501
+ }
502
+ }
503
+ }
504
+ if (dryRun) {
505
+ return {
506
+ status: 'success',
507
+ entityType: type,
508
+ oldSymbol,
509
+ newSymbol,
510
+ modifiedFiles: plannedEdits.map((e) => path.relative(workspaceRoot, e.filePath)),
511
+ occurrencesReplaced: totalOccurrences,
512
+ renamedPaths: plannedRenames,
513
+ };
514
+ }
515
+ // Phase 2: Atomic Flush & Rollback Guard
516
+ const modifiedFilesWritten = [];
517
+ const originalSnapshots = new Map();
518
+ try {
519
+ // 1. Write file edits
520
+ for (const edit of plannedEdits) {
521
+ originalSnapshots.set(edit.filePath, edit.originalContent);
522
+ fs.writeFileSync(edit.filePath, edit.newContent, 'utf-8');
523
+ modifiedFilesWritten.push(edit.filePath);
524
+ }
525
+ // 2. Perform path renames (directories and files)
526
+ if (!skipPathRename) {
527
+ for (const rn of plannedRenames) {
528
+ if (fs.existsSync(rn.from)) {
529
+ const parent = path.dirname(rn.to);
530
+ if (!fs.existsSync(parent))
531
+ fs.mkdirSync(parent, { recursive: true });
532
+ fs.renameSync(rn.from, rn.to);
533
+ }
534
+ }
535
+ }
536
+ // 3. Automatically regenerate ambient declarations post-refactor
537
+ TypeGenerator.generateAll(workspaceRoot);
538
+ const typesDir = path.join(workspaceRoot, '.testspectra', 'types');
539
+ if (fs.existsSync(typesDir)) {
540
+ if (fs.existsSync(path.join(typesDir, 'web.d.ts'))) {
541
+ modifiedFilesWritten.push(path.join(typesDir, 'web.d.ts'));
542
+ }
543
+ if (fs.existsSync(path.join(typesDir, 'fixtures.d.ts'))) {
544
+ modifiedFilesWritten.push(path.join(typesDir, 'fixtures.d.ts'));
545
+ }
546
+ }
547
+ return {
548
+ status: 'success',
549
+ entityType: type,
550
+ oldSymbol,
551
+ newSymbol,
552
+ modifiedFiles: Array.from(new Set(modifiedFilesWritten.map((p) => path.relative(workspaceRoot, p)))),
553
+ occurrencesReplaced: totalOccurrences,
554
+ renamedPaths: plannedRenames,
555
+ };
556
+ }
557
+ catch (ioErr) {
558
+ // ROLLBACK GUARD: Restore all modified files from original snapshots
559
+ for (const [filePath, orig] of originalSnapshots.entries()) {
560
+ try {
561
+ fs.writeFileSync(filePath, orig, 'utf-8');
562
+ }
563
+ catch { }
564
+ }
565
+ // Reverse path renames
566
+ if (!skipPathRename) {
567
+ for (const rn of plannedRenames) {
568
+ if (fs.existsSync(rn.to)) {
569
+ try {
570
+ fs.renameSync(rn.to, rn.from);
571
+ }
572
+ catch { }
573
+ }
574
+ }
575
+ }
576
+ return {
577
+ status: 'failed',
578
+ entityType: type,
579
+ oldSymbol,
580
+ newSymbol,
581
+ modifiedFiles: [],
582
+ occurrencesReplaced: 0,
583
+ error: `Atomic rollback executed due to I/O error: ${ioErr.message}`,
584
+ };
585
+ }
586
+ }
587
+ }