@testspectra/cli 1.1.7 → 1.1.8-rc.2

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 (261) hide show
  1. package/bin/.testspectra-runner.hash +1 -1
  2. package/bin/spectra.js +0 -0
  3. package/bin/testspectra-runner.exe +0 -0
  4. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  5. package/demo-app/assets/index-DOtRypCa.js +154 -0
  6. package/demo-app/index.html +2 -2
  7. package/dist/.tsbuildinfo +1 -1
  8. package/dist/commands/__tests__/run-e2e.test.js +270 -29
  9. package/dist/commands/__tests__/run.test.js +92 -16
  10. package/dist/commands/demo.js +17 -7
  11. package/dist/commands/init.d.ts +1 -1
  12. package/dist/commands/init.js +154 -149
  13. package/dist/commands/lint.d.ts +5 -0
  14. package/dist/commands/lint.js +55 -0
  15. package/dist/commands/refactor.d.ts +2 -1
  16. package/dist/commands/refactor.js +129 -35
  17. package/dist/commands/run.d.ts +2 -3
  18. package/dist/commands/run.js +93 -28
  19. package/dist/commands/skills.d.ts +9 -0
  20. package/dist/commands/skills.js +28 -0
  21. package/dist/commands/sync-types.js +1 -0
  22. package/dist/commands/test.d.ts +17 -0
  23. package/dist/commands/test.js +104 -0
  24. package/dist/commands/watch.js +9 -3
  25. package/dist/config/loader.js +1 -1
  26. package/dist/config/schema.d.ts +5 -114
  27. package/dist/config/schema.js +1 -17
  28. package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
  29. package/dist/generator/__tests__/type-generator.test.js +1 -1
  30. package/dist/generator/tsconfig-generator.js +44 -16
  31. package/dist/generator/type-generator.d.ts +24 -1
  32. package/dist/generator/type-generator.js +104 -8
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +76 -40
  35. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  36. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  37. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  38. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  39. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  40. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  41. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  42. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  43. package/dist/linter/__tests__/step-parity.test.js +372 -0
  44. package/dist/linter/discovery.d.ts +30 -0
  45. package/dist/linter/discovery.js +249 -0
  46. package/dist/linter/extractor.d.ts +67 -0
  47. package/dist/linter/extractor.js +245 -0
  48. package/dist/linter/index.d.ts +7 -0
  49. package/dist/linter/index.js +7 -0
  50. package/dist/linter/parser.d.ts +24 -0
  51. package/dist/linter/parser.js +219 -0
  52. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  53. package/dist/linter/schemas/spec.schema.js +150 -0
  54. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  55. package/dist/linter/schemas/suite.schema.js +83 -0
  56. package/dist/linter/spec-linter.d.ts +23 -0
  57. package/dist/linter/spec-linter.js +630 -0
  58. package/dist/linter/types.d.ts +28 -0
  59. package/dist/linter/types.js +1 -0
  60. package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
  61. package/dist/refactor/refactor-engine.d.ts +25 -0
  62. package/dist/refactor/refactor-engine.js +374 -42
  63. package/dist/reporter/semantic-formatter.js +22 -2
  64. package/dist/reporter/types.d.ts +1 -1
  65. package/dist/runner/bridge.d.ts +1 -1
  66. package/dist/runner/bridge.js +34 -21
  67. package/dist/runner/reporter.d.ts +2 -0
  68. package/dist/runner/reporter.js +11 -3
  69. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  70. package/dist/utils/__tests__/api-server.test.js +62 -0
  71. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  72. package/dist/utils/__tests__/demo-server.test.js +35 -0
  73. package/dist/utils/api-server.d.ts +19 -0
  74. package/dist/utils/api-server.js +179 -0
  75. package/dist/utils/demo-server.d.ts +5 -0
  76. package/dist/utils/demo-server.js +8 -1
  77. package/package.json +29 -25
  78. package/templates/default/CLAUDE.md +16 -11
  79. package/templates/default/global-hooks/after/mobile.hook.ts +3 -3
  80. package/templates/default/global-hooks/after/web.hook.ts +3 -3
  81. package/templates/default/global-hooks/before/mobile.hook.ts +3 -3
  82. package/templates/default/global-hooks/before/web.hook.ts +3 -3
  83. package/templates/default/package.json +3 -3
  84. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  85. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  86. package/templates/default/page-objects/MatchersPage/mobile.ts +70 -46
  87. package/templates/default/page-objects/MatchersPage/web.ts +43 -37
  88. package/templates/default/page-objects/NavigationPage/mobile.ts +14 -14
  89. package/templates/default/page-objects/NavigationPage/web.ts +8 -8
  90. package/templates/default/page-objects/PlaygroundPage/mobile.ts +42 -36
  91. package/templates/default/page-objects/PlaygroundPage/web.ts +31 -31
  92. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -11
  93. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -12
  94. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -27
  95. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  96. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  97. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -16
  98. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -12
  99. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -24
  100. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  101. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  102. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  103. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  104. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  105. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  106. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  107. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  108. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  109. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  110. package/templates/default/specs/ApiInterception/suite.md +10 -9
  111. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  112. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  113. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  114. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  115. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  116. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  117. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  118. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  119. package/templates/default/specs/Authentication/suite.md +9 -0
  120. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +9 -9
  121. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +17 -12
  122. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
  123. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  124. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  125. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  126. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  127. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +19 -12
  128. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +11 -11
  129. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +17 -12
  130. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
  131. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  132. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  133. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  134. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  135. package/templates/default/specs/DeepLink/suite.md +9 -0
  136. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +11 -11
  137. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +17 -12
  138. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
  139. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +29 -29
  140. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +19 -12
  141. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
  142. package/templates/default/specs/ElementMatchers/suite.md +10 -9
  143. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  144. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  145. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  146. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  147. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  148. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  149. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  150. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  151. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +6 -6
  152. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +18 -12
  153. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
  154. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +6 -6
  155. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +18 -12
  156. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
  157. package/templates/default/spectra.config.ts +52 -68
  158. package/templates/default/support/actions/fillCredentials/mobile.action.ts +4 -4
  159. package/templates/default/support/actions/fillCredentials/web.action.ts +4 -4
  160. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  161. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  162. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +5 -5
  163. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +5 -5
  164. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +6 -6
  165. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +6 -6
  166. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db +0 -0
  167. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-shm +0 -0
  168. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-wal +0 -0
  169. package/templates/nx/.nx/workspace-data/d/daemon.log +1577 -168
  170. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  171. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  172. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  173. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  174. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  175. package/templates/nx/.nx/workspace-data/project-graph.json +14389 -0
  176. package/templates/nx/CLAUDE.md +10 -10
  177. package/templates/nx/package.json +3 -3
  178. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -3
  179. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -27
  180. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -3
  181. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -24
  182. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -3
  183. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
  184. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -3
  185. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +17 -17
  186. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -3
  187. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
  188. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -3
  189. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
  190. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -3
  191. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
  192. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  193. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  194. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  195. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -3
  196. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
  197. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -3
  198. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
  199. package/templates/nx/pnpm-lock.yaml +6663 -6663
  200. package/templates/nx/pnpm-workspace.yaml +9 -9
  201. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +33 -33
  202. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +91 -37
  203. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +5 -5
  204. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +8 -8
  205. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +26 -26
  206. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +31 -31
  207. package/templates/nx/shared/testing/project.json +19 -19
  208. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +4 -4
  209. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +4 -4
  210. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +3 -3
  211. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +5 -5
  212. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +4 -4
  213. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +6 -6
  214. package/templates/nx/spectra.config.ts +52 -68
  215. package/templates/react-component/.cursorrules +17 -0
  216. package/templates/react-component/.vscode/extensions.json +5 -0
  217. package/templates/react-component/CLAUDE.md +14 -0
  218. package/templates/react-component/README.md +69 -0
  219. package/templates/react-component/fixtures/component-mock.json +12 -0
  220. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  221. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  222. package/templates/react-component/package.json +27 -0
  223. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  224. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  225. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  226. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  227. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  228. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  229. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  230. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  231. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  232. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  233. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  234. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  235. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  236. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  237. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  238. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  239. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  240. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  241. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  242. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  243. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  244. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  245. package/templates/react-component/spectra.config.ts +27 -0
  246. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  247. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  248. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  249. package/templates/react-component/src/test-utils.tsx +23 -0
  250. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  251. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  252. package/templates/react-component/tsconfig.json +8 -0
  253. package/bin/testspectra-runner +0 -0
  254. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  255. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  256. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  257. package/dist/commands/cloud-run.d.ts +0 -13
  258. package/dist/commands/cloud-run.js +0 -120
  259. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  260. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  261. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -5,6 +5,7 @@ export interface RenameOptions {
5
5
  oldName: string;
6
6
  newName: string;
7
7
  dryRun?: boolean;
8
+ skipPathRename?: boolean;
8
9
  }
9
10
  export interface RefactorResult {
10
11
  status: 'success' | 'failed';
@@ -19,7 +20,31 @@ export interface RefactorResult {
19
20
  }>;
20
21
  error?: string;
21
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[];
22
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>;
23
48
  private static findFiles;
24
49
  static renameSymbol(options: RenameOptions): Promise<RefactorResult>;
25
50
  }
@@ -1,7 +1,130 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
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'];
4
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
+ }
5
128
  static findFiles(dir, extensions) {
6
129
  const results = [];
7
130
  if (!fs.existsSync(dir))
@@ -30,7 +153,7 @@ export class RefactorEngine {
30
153
  return results;
31
154
  }
32
155
  static async renameSymbol(options) {
33
- const { workspaceRoot, type, oldName, newName, dryRun = false } = options;
156
+ const { workspaceRoot, type, oldName, newName, dryRun = false, skipPathRename = false } = options;
34
157
  if (!oldName || !newName || oldName === newName) {
35
158
  return {
36
159
  status: 'failed',
@@ -42,60 +165,133 @@ export class RefactorEngine {
42
165
  error: 'Invalid or identical old/new symbol names',
43
166
  };
44
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
+ }
45
192
  // Phase 1: Determine file operations and replacement patterns
46
193
  const plannedRenames = [];
47
194
  const plannedEdits = [];
48
- let oldSymbol = oldName;
49
- let newSymbol = newName;
50
- // 1. Target Directory / File Discovery
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
51
224
  if (type === 'page-object') {
52
- oldSymbol = oldName;
53
- newSymbol = newName;
54
- const poDir = path.join(workspaceRoot, 'page-objects', oldName);
225
+ oldSymbol = targetOldName;
226
+ newSymbol = targetNewName;
227
+ const targetPoDir = path.join(workspaceRoot, 'page-objects', targetNewName);
228
+ const poDir = path.join(workspaceRoot, 'page-objects', targetOldName);
55
229
  if (fs.existsSync(poDir)) {
56
- plannedRenames.push({ from: poDir, to: path.join(workspaceRoot, 'page-objects', newName) });
230
+ plannedRenames.push({ from: poDir, to: targetPoDir });
231
+ }
232
+ else if (skipPathRename && fs.existsSync(targetPoDir)) {
233
+ plannedRenames.push({ from: poDir, to: targetPoDir });
57
234
  }
58
235
  }
59
236
  else if (type === 'step') {
60
- oldSymbol = `Step.${oldName}`;
61
- newSymbol = `Step.${newName}`;
237
+ oldSymbol = `Step.${targetOldName}`;
238
+ newSymbol = `Step.${targetNewName}`;
62
239
  const stepCandidates = [
63
- path.join(workspaceRoot, 'support', 'steps', oldName),
64
- path.join(workspaceRoot, 'steps', oldName),
240
+ path.join(workspaceRoot, 'support', 'steps', targetOldName),
241
+ path.join(workspaceRoot, 'steps', targetOldName),
65
242
  ];
66
243
  for (const cand of stepCandidates) {
67
244
  if (fs.existsSync(cand)) {
68
245
  const parent = path.dirname(cand);
69
- plannedRenames.push({ from: cand, to: path.join(parent, newName) });
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
+ }
70
254
  }
71
255
  }
72
256
  }
73
257
  else if (type === 'action') {
74
- oldSymbol = `Spectra.${oldName}`;
75
- newSymbol = `Spectra.${newName}`;
258
+ oldSymbol = `Spectra.${targetOldName}`;
259
+ newSymbol = `Spectra.${targetNewName}`;
76
260
  const actionCandidates = [
77
- path.join(workspaceRoot, 'support', 'actions', oldName),
78
- path.join(workspaceRoot, 'actions', oldName),
261
+ path.join(workspaceRoot, 'support', 'actions', targetOldName),
262
+ path.join(workspaceRoot, 'actions', targetOldName),
79
263
  ];
80
264
  for (const cand of actionCandidates) {
81
265
  if (fs.existsSync(cand)) {
82
266
  const parent = path.dirname(cand);
83
- plannedRenames.push({ from: cand, to: path.join(parent, newName) });
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
+ }
84
275
  }
85
276
  }
86
277
  }
87
278
  else if (type === 'fixture') {
88
- oldSymbol = `Fixture.${oldName}`;
89
- newSymbol = `Fixture.${newName}`;
90
- const fixturesDir = path.join(workspaceRoot, 'fixtures');
91
- if (fs.existsSync(fixturesDir)) {
92
- const entries = fs.readdirSync(fixturesDir);
93
- for (const entry of entries) {
94
- const parsed = path.parse(entry);
95
- if (parsed.name === oldName) {
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) {
96
292
  plannedRenames.push({
97
- from: path.join(fixturesDir, entry),
98
- to: path.join(fixturesDir, `${newName}${parsed.ext}`),
293
+ from: path.join(parsed.dir, `${targetOldName}${parsed.ext}`),
294
+ to: file,
99
295
  });
100
296
  }
101
297
  }
@@ -110,6 +306,9 @@ export class RefactorEngine {
110
306
  path.join(workspaceRoot, 'steps'),
111
307
  path.join(workspaceRoot, 'actions'),
112
308
  path.join(workspaceRoot, 'global-hooks'),
309
+ path.join(workspaceRoot, 'packages'),
310
+ path.join(workspaceRoot, 'apps'),
311
+ path.join(workspaceRoot, 'shared'),
113
312
  ];
114
313
  const allCodeFiles = searchDirs.flatMap((dir) => this.findFiles(dir, ['.ts', '.js', '.md']));
115
314
  const uniqueFiles = Array.from(new Set(allCodeFiles));
@@ -126,6 +325,20 @@ export class RefactorEngine {
126
325
  modified = modified.replace(classReg, `class ${newName}`);
127
326
  count += classMatches;
128
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
+ }
129
342
  // Replace `OldName.` -> `NewName.`
130
343
  const refReg = new RegExp(`\\b${oldName}\\.`, 'g');
131
344
  const refMatches = (modified.match(refReg) || []).length;
@@ -161,12 +374,20 @@ export class RefactorEngine {
161
374
  }
162
375
  else if (type === 'fixture') {
163
376
  // Replace `Fixture.oldFixture` -> `Fixture.newFixture`
164
- const fixReg = new RegExp(`\\bFixture\\.${oldName}\\b`, 'g');
377
+ const fixReg = new RegExp(`\\bFixture\\.${targetOldName}\\b`, 'g');
165
378
  const fixMatches = (modified.match(fixReg) || []).length;
166
379
  if (fixMatches > 0) {
167
- modified = modified.replace(fixReg, `Fixture.${newName}`);
380
+ modified = modified.replace(fixReg, `Fixture.${targetNewName}`);
168
381
  count += fixMatches;
169
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
+ }
170
391
  }
171
392
  if (count > 0) {
172
393
  totalOccurrences += count;
@@ -178,6 +399,108 @@ export class RefactorEngine {
178
399
  });
179
400
  }
180
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
+ }
181
504
  if (dryRun) {
182
505
  return {
183
506
  status: 'success',
@@ -200,19 +523,26 @@ export class RefactorEngine {
200
523
  modifiedFilesWritten.push(edit.filePath);
201
524
  }
202
525
  // 2. Perform path renames (directories and files)
203
- for (const rn of plannedRenames) {
204
- if (fs.existsSync(rn.from)) {
205
- const parent = path.dirname(rn.to);
206
- if (!fs.existsSync(parent))
207
- fs.mkdirSync(parent, { recursive: true });
208
- fs.renameSync(rn.from, rn.to);
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
+ }
209
534
  }
210
535
  }
211
536
  // 3. Automatically regenerate ambient declarations post-refactor
212
537
  TypeGenerator.generateAll(workspaceRoot);
213
538
  const typesDir = path.join(workspaceRoot, '.testspectra', 'types');
214
539
  if (fs.existsSync(typesDir)) {
215
- modifiedFilesWritten.push(path.join(typesDir, 'web.d.ts'));
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
+ }
216
546
  }
217
547
  return {
218
548
  status: 'success',
@@ -233,12 +563,14 @@ export class RefactorEngine {
233
563
  catch { }
234
564
  }
235
565
  // Reverse path renames
236
- for (const rn of plannedRenames) {
237
- if (fs.existsSync(rn.to)) {
238
- try {
239
- fs.renameSync(rn.to, rn.from);
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 { }
240
573
  }
241
- catch { }
242
574
  }
243
575
  }
244
576
  return {
@@ -13,6 +13,17 @@ function extractTargetString(target) {
13
13
  }
14
14
  return String(target);
15
15
  }
16
+ /** Renders a `ScopedSelector`'s `.parent` chain (produced by `element.get()`/`.getAll()`
17
+ * chaining) as `"parent" within "grandparent"`, nearest ancestor first — or null when the
18
+ * target isn't scoped. */
19
+ function extractWithinChain(target) {
20
+ if (!target || typeof target !== 'object' || !target.parent)
21
+ return null;
22
+ const chain = [];
23
+ for (let p = target.parent; p; p = p.parent)
24
+ chain.push(p.selector);
25
+ return chain.join('" within "');
26
+ }
16
27
  function formatElementTarget(target) {
17
28
  const sel = extractTargetString(target);
18
29
  if (!sel)
@@ -20,7 +31,8 @@ function formatElementTarget(target) {
20
31
  if (sel.startsWith('element "') || sel.startsWith('collection "')) {
21
32
  return sel;
22
33
  }
23
- return `element "${sel}"`;
34
+ const within = extractWithinChain(target);
35
+ return within ? `element "${sel}" within "${within}"` : `element "${sel}"`;
24
36
  }
25
37
  function formatCollectionTarget(target) {
26
38
  const sel = extractTargetString(target);
@@ -29,7 +41,8 @@ function formatCollectionTarget(target) {
29
41
  if (sel.startsWith('collection "')) {
30
42
  return sel;
31
43
  }
32
- return `collection "${sel}"`;
44
+ const within = extractWithinChain(target);
45
+ return within ? `collection "${sel}" within "${within}"` : `collection "${sel}"`;
33
46
  }
34
47
  /**
35
48
  * Transforms raw matcher assertions and element commands into grammatically correct English sentences.
@@ -37,6 +50,11 @@ function formatCollectionTarget(target) {
37
50
  export function formatAction(actionKey, target, args = {}) {
38
51
  const el = formatElementTarget(target);
39
52
  switch (actionKey) {
53
+ // Component testing only (@testspectra/react's Spectra.mount()) — target is a component name,
54
+ // not a DOM selector, so it's rendered plainly rather than through formatElementTarget's
55
+ // `element "..."` wrapping.
56
+ case 'mount':
57
+ return `Mount component "${extractTargetString(target) || '{component}'}"`;
40
58
  case 'click':
41
59
  return `Click on ${el}`;
42
60
  case 'doubleClick':
@@ -83,6 +101,8 @@ export function formatAction(actionKey, target, args = {}) {
83
101
  return `Intercept ${args.method ?? '{method}'} "${args.url ?? '{url}'}" mock rule registered`;
84
102
  case 'clearCookies':
85
103
  return `Clear all browser cookies`;
104
+ case 'setCookies':
105
+ return `Set cookies for "${args.url ?? '{url}'}"`;
86
106
  case 'clearLocalStorage':
87
107
  return `Clear browser localStorage`;
88
108
  default:
@@ -1,2 +1,2 @@
1
- export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'intercept' | 'clearCookies' | 'clearLocalStorage';
1
+ export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'intercept' | 'clearCookies' | 'setCookies' | 'clearLocalStorage';
2
2
  export type AssertionKey = 'be.visible' | 'not.be.visible' | 'exist' | 'not.exist' | 'be.clickable' | 'not.be.clickable' | 'be.enabled' | 'be.disabled' | 'be.checked' | 'not.be.checked' | 'be.selected' | 'not.be.selected' | 'be.focused' | 'not.be.focused' | 'have.text' | 'not.have.text' | 'contain.text' | 'not.contain.text' | 'have.value' | 'not.have.value' | 'contain.value' | 'not.contain.value' | 'have.attr' | 'not.have.attr' | 'have.class' | 'not.have.class' | 'have.css' | 'not.have.css' | 'have.length' | 'not.have.length' | 'have.length.greaterThan' | 'have.length.lessThan' | 'be.empty' | 'not.be.empty' | 'shouldHaveUrl' | 'shouldContainUrl' | 'shouldHaveTitle' | 'shouldContainTitle' | 'shouldBeLoaded' | 'shouldBePageLoaded' | 'shouldHaveNoConsoleErrors' | 'have.url' | 'contain.url' | 'have.title' | 'contain.title' | 'be.loaded' | 'be.pageLoaded' | 'have.noConsoleErrors';
@@ -14,7 +14,7 @@ export interface RunOptions {
14
14
  targetDevice?: string;
15
15
  licenseKey?: string;
16
16
  outputJsonPath?: string;
17
- spec?: string;
17
+ specs?: string[];
18
18
  concurrency?: number;
19
19
  failedOnly?: boolean;
20
20
  failedTarget?: string;