mindheal 1.0.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 (255) hide show
  1. package/.env.example +48 -0
  2. package/CHANGELOG.md +27 -0
  3. package/LICENSE +21 -0
  4. package/README.md +481 -0
  5. package/dist/cjs/ai/ai-provider.js +46 -0
  6. package/dist/cjs/ai/ai-provider.js.map +1 -0
  7. package/dist/cjs/ai/anthropic-provider.js +106 -0
  8. package/dist/cjs/ai/anthropic-provider.js.map +1 -0
  9. package/dist/cjs/ai/azure-openai-provider.js +130 -0
  10. package/dist/cjs/ai/azure-openai-provider.js.map +1 -0
  11. package/dist/cjs/ai/bedrock-provider.js +183 -0
  12. package/dist/cjs/ai/bedrock-provider.js.map +1 -0
  13. package/dist/cjs/ai/deepseek-provider.js +118 -0
  14. package/dist/cjs/ai/deepseek-provider.js.map +1 -0
  15. package/dist/cjs/ai/gemini-provider.js +129 -0
  16. package/dist/cjs/ai/gemini-provider.js.map +1 -0
  17. package/dist/cjs/ai/groq-provider.js +118 -0
  18. package/dist/cjs/ai/groq-provider.js.map +1 -0
  19. package/dist/cjs/ai/meta-provider.js +118 -0
  20. package/dist/cjs/ai/meta-provider.js.map +1 -0
  21. package/dist/cjs/ai/ollama-provider.js +127 -0
  22. package/dist/cjs/ai/ollama-provider.js.map +1 -0
  23. package/dist/cjs/ai/openai-provider.js +117 -0
  24. package/dist/cjs/ai/openai-provider.js.map +1 -0
  25. package/dist/cjs/ai/perplexity-provider.js +118 -0
  26. package/dist/cjs/ai/perplexity-provider.js.map +1 -0
  27. package/dist/cjs/ai/prompt-templates.js +174 -0
  28. package/dist/cjs/ai/prompt-templates.js.map +1 -0
  29. package/dist/cjs/ai/qwen-provider.js +118 -0
  30. package/dist/cjs/ai/qwen-provider.js.map +1 -0
  31. package/dist/cjs/analytics/healing-analytics.js +263 -0
  32. package/dist/cjs/analytics/healing-analytics.js.map +1 -0
  33. package/dist/cjs/cli/init.js +517 -0
  34. package/dist/cjs/cli/init.js.map +1 -0
  35. package/dist/cjs/config/config-loader.js +135 -0
  36. package/dist/cjs/config/config-loader.js.map +1 -0
  37. package/dist/cjs/config/defaults.js +109 -0
  38. package/dist/cjs/config/defaults.js.map +1 -0
  39. package/dist/cjs/core/dom-snapshot.js +280 -0
  40. package/dist/cjs/core/dom-snapshot.js.map +1 -0
  41. package/dist/cjs/core/enterprise-strategy.js +702 -0
  42. package/dist/cjs/core/enterprise-strategy.js.map +1 -0
  43. package/dist/cjs/core/healer.js +283 -0
  44. package/dist/cjs/core/healer.js.map +1 -0
  45. package/dist/cjs/core/interceptor.js +945 -0
  46. package/dist/cjs/core/interceptor.js.map +1 -0
  47. package/dist/cjs/core/locator-analyzer.js +172 -0
  48. package/dist/cjs/core/locator-analyzer.js.map +1 -0
  49. package/dist/cjs/core/locator-strategies.js +891 -0
  50. package/dist/cjs/core/locator-strategies.js.map +1 -0
  51. package/dist/cjs/core/self-heal-cache.js +178 -0
  52. package/dist/cjs/core/self-heal-cache.js.map +1 -0
  53. package/dist/cjs/core/smart-retry.js +248 -0
  54. package/dist/cjs/core/smart-retry.js.map +1 -0
  55. package/dist/cjs/core/visual-verification.js +262 -0
  56. package/dist/cjs/core/visual-verification.js.map +1 -0
  57. package/dist/cjs/git/code-modifier.js +184 -0
  58. package/dist/cjs/git/code-modifier.js.map +1 -0
  59. package/dist/cjs/git/git-operations.js +145 -0
  60. package/dist/cjs/git/git-operations.js.map +1 -0
  61. package/dist/cjs/git/pr-creator.js +190 -0
  62. package/dist/cjs/git/pr-creator.js.map +1 -0
  63. package/dist/cjs/index.js +97 -0
  64. package/dist/cjs/index.js.map +1 -0
  65. package/dist/cjs/rag/context-retriever.js +289 -0
  66. package/dist/cjs/rag/context-retriever.js.map +1 -0
  67. package/dist/cjs/rag/embeddings.js +82 -0
  68. package/dist/cjs/rag/embeddings.js.map +1 -0
  69. package/dist/cjs/rag/knowledge-store.js +159 -0
  70. package/dist/cjs/rag/knowledge-store.js.map +1 -0
  71. package/dist/cjs/reporters/heal-report.js +279 -0
  72. package/dist/cjs/reporters/heal-report.js.map +1 -0
  73. package/dist/cjs/reporters/heal-reporter.js +294 -0
  74. package/dist/cjs/reporters/heal-reporter.js.map +1 -0
  75. package/dist/cjs/server/review-server.js +166 -0
  76. package/dist/cjs/server/review-server.js.map +1 -0
  77. package/dist/cjs/server/routes.js +92 -0
  78. package/dist/cjs/server/routes.js.map +1 -0
  79. package/dist/cjs/utils/environment.js +57 -0
  80. package/dist/cjs/utils/environment.js.map +1 -0
  81. package/dist/cjs/utils/file-lock.js +136 -0
  82. package/dist/cjs/utils/file-lock.js.map +1 -0
  83. package/dist/cjs/utils/file-utils.js +49 -0
  84. package/dist/cjs/utils/file-utils.js.map +1 -0
  85. package/dist/cjs/utils/logger.js +78 -0
  86. package/dist/cjs/utils/logger.js.map +1 -0
  87. package/dist/esm/ai/ai-provider.js +44 -0
  88. package/dist/esm/ai/ai-provider.js.map +1 -0
  89. package/dist/esm/ai/anthropic-provider.js +104 -0
  90. package/dist/esm/ai/anthropic-provider.js.map +1 -0
  91. package/dist/esm/ai/azure-openai-provider.js +128 -0
  92. package/dist/esm/ai/azure-openai-provider.js.map +1 -0
  93. package/dist/esm/ai/bedrock-provider.js +181 -0
  94. package/dist/esm/ai/bedrock-provider.js.map +1 -0
  95. package/dist/esm/ai/deepseek-provider.js +116 -0
  96. package/dist/esm/ai/deepseek-provider.js.map +1 -0
  97. package/dist/esm/ai/gemini-provider.js +127 -0
  98. package/dist/esm/ai/gemini-provider.js.map +1 -0
  99. package/dist/esm/ai/groq-provider.js +116 -0
  100. package/dist/esm/ai/groq-provider.js.map +1 -0
  101. package/dist/esm/ai/meta-provider.js +116 -0
  102. package/dist/esm/ai/meta-provider.js.map +1 -0
  103. package/dist/esm/ai/ollama-provider.js +125 -0
  104. package/dist/esm/ai/ollama-provider.js.map +1 -0
  105. package/dist/esm/ai/openai-provider.js +115 -0
  106. package/dist/esm/ai/openai-provider.js.map +1 -0
  107. package/dist/esm/ai/perplexity-provider.js +116 -0
  108. package/dist/esm/ai/perplexity-provider.js.map +1 -0
  109. package/dist/esm/ai/prompt-templates.js +171 -0
  110. package/dist/esm/ai/prompt-templates.js.map +1 -0
  111. package/dist/esm/ai/qwen-provider.js +116 -0
  112. package/dist/esm/ai/qwen-provider.js.map +1 -0
  113. package/dist/esm/analytics/healing-analytics.js +261 -0
  114. package/dist/esm/analytics/healing-analytics.js.map +1 -0
  115. package/dist/esm/cli/init.js +495 -0
  116. package/dist/esm/cli/init.js.map +1 -0
  117. package/dist/esm/config/config-loader.js +132 -0
  118. package/dist/esm/config/config-loader.js.map +1 -0
  119. package/dist/esm/config/defaults.js +107 -0
  120. package/dist/esm/config/defaults.js.map +1 -0
  121. package/dist/esm/core/dom-snapshot.js +278 -0
  122. package/dist/esm/core/dom-snapshot.js.map +1 -0
  123. package/dist/esm/core/enterprise-strategy.js +695 -0
  124. package/dist/esm/core/enterprise-strategy.js.map +1 -0
  125. package/dist/esm/core/healer.js +281 -0
  126. package/dist/esm/core/healer.js.map +1 -0
  127. package/dist/esm/core/interceptor.js +940 -0
  128. package/dist/esm/core/interceptor.js.map +1 -0
  129. package/dist/esm/core/locator-analyzer.js +169 -0
  130. package/dist/esm/core/locator-analyzer.js.map +1 -0
  131. package/dist/esm/core/locator-strategies.js +882 -0
  132. package/dist/esm/core/locator-strategies.js.map +1 -0
  133. package/dist/esm/core/self-heal-cache.js +176 -0
  134. package/dist/esm/core/self-heal-cache.js.map +1 -0
  135. package/dist/esm/core/smart-retry.js +246 -0
  136. package/dist/esm/core/smart-retry.js.map +1 -0
  137. package/dist/esm/core/visual-verification.js +260 -0
  138. package/dist/esm/core/visual-verification.js.map +1 -0
  139. package/dist/esm/git/code-modifier.js +182 -0
  140. package/dist/esm/git/code-modifier.js.map +1 -0
  141. package/dist/esm/git/git-operations.js +143 -0
  142. package/dist/esm/git/git-operations.js.map +1 -0
  143. package/dist/esm/git/pr-creator.js +188 -0
  144. package/dist/esm/git/pr-creator.js.map +1 -0
  145. package/dist/esm/index.js +37 -0
  146. package/dist/esm/index.js.map +1 -0
  147. package/dist/esm/rag/context-retriever.js +287 -0
  148. package/dist/esm/rag/context-retriever.js.map +1 -0
  149. package/dist/esm/rag/embeddings.js +77 -0
  150. package/dist/esm/rag/embeddings.js.map +1 -0
  151. package/dist/esm/rag/knowledge-store.js +157 -0
  152. package/dist/esm/rag/knowledge-store.js.map +1 -0
  153. package/dist/esm/reporters/heal-report.js +277 -0
  154. package/dist/esm/reporters/heal-report.js.map +1 -0
  155. package/dist/esm/reporters/heal-reporter.js +290 -0
  156. package/dist/esm/reporters/heal-reporter.js.map +1 -0
  157. package/dist/esm/server/review-server.js +164 -0
  158. package/dist/esm/server/review-server.js.map +1 -0
  159. package/dist/esm/server/routes.js +90 -0
  160. package/dist/esm/server/routes.js.map +1 -0
  161. package/dist/esm/utils/environment.js +53 -0
  162. package/dist/esm/utils/environment.js.map +1 -0
  163. package/dist/esm/utils/file-lock.js +134 -0
  164. package/dist/esm/utils/file-lock.js.map +1 -0
  165. package/dist/esm/utils/file-utils.js +43 -0
  166. package/dist/esm/utils/file-utils.js.map +1 -0
  167. package/dist/esm/utils/logger.js +75 -0
  168. package/dist/esm/utils/logger.js.map +1 -0
  169. package/dist/types/ai/ai-provider.d.ts +4 -0
  170. package/dist/types/ai/ai-provider.d.ts.map +1 -0
  171. package/dist/types/ai/anthropic-provider.d.ts +11 -0
  172. package/dist/types/ai/anthropic-provider.d.ts.map +1 -0
  173. package/dist/types/ai/azure-openai-provider.d.ts +13 -0
  174. package/dist/types/ai/azure-openai-provider.d.ts.map +1 -0
  175. package/dist/types/ai/bedrock-provider.d.ts +14 -0
  176. package/dist/types/ai/bedrock-provider.d.ts.map +1 -0
  177. package/dist/types/ai/deepseek-provider.d.ts +12 -0
  178. package/dist/types/ai/deepseek-provider.d.ts.map +1 -0
  179. package/dist/types/ai/gemini-provider.d.ts +12 -0
  180. package/dist/types/ai/gemini-provider.d.ts.map +1 -0
  181. package/dist/types/ai/groq-provider.d.ts +12 -0
  182. package/dist/types/ai/groq-provider.d.ts.map +1 -0
  183. package/dist/types/ai/meta-provider.d.ts +12 -0
  184. package/dist/types/ai/meta-provider.d.ts.map +1 -0
  185. package/dist/types/ai/ollama-provider.d.ts +10 -0
  186. package/dist/types/ai/ollama-provider.d.ts.map +1 -0
  187. package/dist/types/ai/openai-provider.d.ts +11 -0
  188. package/dist/types/ai/openai-provider.d.ts.map +1 -0
  189. package/dist/types/ai/perplexity-provider.d.ts +12 -0
  190. package/dist/types/ai/perplexity-provider.d.ts.map +1 -0
  191. package/dist/types/ai/prompt-templates.d.ts +11 -0
  192. package/dist/types/ai/prompt-templates.d.ts.map +1 -0
  193. package/dist/types/ai/qwen-provider.d.ts +12 -0
  194. package/dist/types/ai/qwen-provider.d.ts.map +1 -0
  195. package/dist/types/analytics/healing-analytics.d.ts +36 -0
  196. package/dist/types/analytics/healing-analytics.d.ts.map +1 -0
  197. package/dist/types/cli/init.d.ts +15 -0
  198. package/dist/types/cli/init.d.ts.map +1 -0
  199. package/dist/types/config/config-loader.d.ts +4 -0
  200. package/dist/types/config/config-loader.d.ts.map +1 -0
  201. package/dist/types/config/defaults.d.ts +3 -0
  202. package/dist/types/config/defaults.d.ts.map +1 -0
  203. package/dist/types/core/dom-snapshot.d.ts +12 -0
  204. package/dist/types/core/dom-snapshot.d.ts.map +1 -0
  205. package/dist/types/core/enterprise-strategy.d.ts +56 -0
  206. package/dist/types/core/enterprise-strategy.d.ts.map +1 -0
  207. package/dist/types/core/healer.d.ts +52 -0
  208. package/dist/types/core/healer.d.ts.map +1 -0
  209. package/dist/types/core/interceptor.d.ts +64 -0
  210. package/dist/types/core/interceptor.d.ts.map +1 -0
  211. package/dist/types/core/locator-analyzer.d.ts +31 -0
  212. package/dist/types/core/locator-analyzer.d.ts.map +1 -0
  213. package/dist/types/core/locator-strategies.d.ts +45 -0
  214. package/dist/types/core/locator-strategies.d.ts.map +1 -0
  215. package/dist/types/core/self-heal-cache.d.ts +51 -0
  216. package/dist/types/core/self-heal-cache.d.ts.map +1 -0
  217. package/dist/types/core/smart-retry.d.ts +64 -0
  218. package/dist/types/core/smart-retry.d.ts.map +1 -0
  219. package/dist/types/core/visual-verification.d.ts +46 -0
  220. package/dist/types/core/visual-verification.d.ts.map +1 -0
  221. package/dist/types/git/code-modifier.d.ts +51 -0
  222. package/dist/types/git/code-modifier.d.ts.map +1 -0
  223. package/dist/types/git/git-operations.d.ts +40 -0
  224. package/dist/types/git/git-operations.d.ts.map +1 -0
  225. package/dist/types/git/pr-creator.d.ts +27 -0
  226. package/dist/types/git/pr-creator.d.ts.map +1 -0
  227. package/dist/types/index.d.ts +40 -0
  228. package/dist/types/index.d.ts.map +1 -0
  229. package/dist/types/rag/context-retriever.d.ts +69 -0
  230. package/dist/types/rag/context-retriever.d.ts.map +1 -0
  231. package/dist/types/rag/embeddings.d.ts +32 -0
  232. package/dist/types/rag/embeddings.d.ts.map +1 -0
  233. package/dist/types/rag/index.d.ts +12 -0
  234. package/dist/types/rag/index.d.ts.map +1 -0
  235. package/dist/types/rag/knowledge-store.d.ts +38 -0
  236. package/dist/types/rag/knowledge-store.d.ts.map +1 -0
  237. package/dist/types/reporters/heal-report.d.ts +29 -0
  238. package/dist/types/reporters/heal-report.d.ts.map +1 -0
  239. package/dist/types/reporters/heal-reporter.d.ts +49 -0
  240. package/dist/types/reporters/heal-reporter.d.ts.map +1 -0
  241. package/dist/types/server/review-server.d.ts +20 -0
  242. package/dist/types/server/review-server.d.ts.map +1 -0
  243. package/dist/types/server/routes.d.ts +4 -0
  244. package/dist/types/server/routes.d.ts.map +1 -0
  245. package/dist/types/types/index.d.ts +433 -0
  246. package/dist/types/types/index.d.ts.map +1 -0
  247. package/dist/types/utils/environment.d.ts +10 -0
  248. package/dist/types/utils/environment.d.ts.map +1 -0
  249. package/dist/types/utils/file-lock.d.ts +37 -0
  250. package/dist/types/utils/file-lock.d.ts.map +1 -0
  251. package/dist/types/utils/file-utils.d.ts +7 -0
  252. package/dist/types/utils/file-utils.d.ts.map +1 -0
  253. package/dist/types/utils/logger.d.ts +9 -0
  254. package/dist/types/utils/logger.d.ts.map +1 -0
  255. package/package.json +106 -0
@@ -0,0 +1,695 @@
1
+ import { logger } from '../utils/logger.js';
2
+
3
+ /**
4
+ * Enterprise Application Healing Strategy
5
+ *
6
+ * Specialized healing for complex enterprise web applications:
7
+ * - SAP Fiori / SAP GUI for HTML / UI5
8
+ * - Salesforce Lightning / Classic / LWC
9
+ * - Oracle ERP / NetSuite
10
+ * - Workday
11
+ * - ServiceNow
12
+ * - Microsoft Dynamics 365
13
+ *
14
+ * These platforms share common challenges:
15
+ * 1. Dynamically generated IDs that change on every render
16
+ * 2. Deep Shadow DOM nesting (Lightning Web Components)
17
+ * 3. Deeply nested iframes (SAP GUI, Classic Salesforce)
18
+ * 4. Custom web components with proprietary tag names
19
+ * 5. Virtual scrolling / lazy-loaded grids with thousands of rows
20
+ * 6. Complex multi-level menus and navigation trees
21
+ * 7. Hashed/obfuscated CSS class names
22
+ * 8. Heavy async data loading with skeleton/shimmer screens
23
+ */
24
+ // ─── Dynamic ID Patterns ────────────────────────────────────────────────────
25
+ /**
26
+ * Patterns that match dynamically generated IDs across enterprise platforms.
27
+ * When a selector contains one of these patterns, the ID portion is volatile
28
+ * and should be stripped/replaced during healing.
29
+ */
30
+ const DYNAMIC_ID_PATTERNS = [
31
+ // SAP UI5 / Fiori
32
+ { platform: 'sap', pattern: /^__xmlview\d+--/, description: 'SAP XML View prefix' },
33
+ { platform: 'sap', pattern: /^__component\d+---/, description: 'SAP Component prefix' },
34
+ { platform: 'sap', pattern: /^__control\d+-/, description: 'SAP Control ID' },
35
+ { platform: 'sap', pattern: /^__field\d+-/, description: 'SAP Field ID' },
36
+ { platform: 'sap', pattern: /^__item\d+-/, description: 'SAP Item ID' },
37
+ { platform: 'sap', pattern: /^__table\d+-/, description: 'SAP Table ID' },
38
+ { platform: 'sap', pattern: /^__dialog\d+-/, description: 'SAP Dialog ID' },
39
+ { platform: 'sap', pattern: /-__clone\d+$/, description: 'SAP Clone suffix' },
40
+ { platform: 'sap', pattern: /^sap-ui-blocklayer-/, description: 'SAP Block layer' },
41
+ // Salesforce Lightning / Aura / LWC
42
+ { platform: 'salesforce', pattern: /^globalId_\d+/, description: 'Salesforce Global ID' },
43
+ { platform: 'salesforce', pattern: /^auraId_\d+/, description: 'Salesforce Aura ID' },
44
+ { platform: 'salesforce', pattern: /;\d+;[a-z]$/, description: 'Salesforce Aura locator suffix' },
45
+ { platform: 'salesforce', pattern: /^cmp[A-Za-z0-9]{10,}/, description: 'Salesforce component hash' },
46
+ { platform: 'salesforce', pattern: /^[0-9]+:[0-9]+;a$/, description: 'Salesforce numeric locator' },
47
+ // Oracle / NetSuite
48
+ { platform: 'oracle', pattern: /^pt\d+_\d+_/, description: 'Oracle PeopleSoft prefix' },
49
+ { platform: 'oracle', pattern: /^N\d{5,}/, description: 'Oracle Forms numeric ID' },
50
+ { platform: 'oracle', pattern: /^_fox[A-Z0-9]+/, description: 'Oracle ADF Faces prefix' },
51
+ // Workday
52
+ { platform: 'workday', pattern: /^wd-[A-Za-z0-9]{8,}-/, description: 'Workday widget prefix' },
53
+ { platform: 'workday', pattern: /^TABSTRIP_\d+_/, description: 'Workday tab strip ID' },
54
+ // ServiceNow
55
+ { platform: 'servicenow', pattern: /^sys_[a-f0-9]{32}/, description: 'ServiceNow SysID' },
56
+ { platform: 'servicenow', pattern: /^x_[a-z]+_[a-z]+_/, description: 'ServiceNow scoped app prefix' },
57
+ // Microsoft Dynamics 365
58
+ { platform: 'dynamics', pattern: /^MscrmControls\./, description: 'Dynamics CRM control' },
59
+ { platform: 'dynamics', pattern: /^id-[a-f0-9]{8}-[a-f0-9]{4}/, description: 'Dynamics GUID prefix' },
60
+ // Generic patterns (shared across platforms)
61
+ { platform: 'generic', pattern: /^ember\d+/, description: 'Ember auto-generated ID' },
62
+ { platform: 'generic', pattern: /^react-select-\d+-/, description: 'React Select ID' },
63
+ { platform: 'generic', pattern: /^ext-gen\d+/, description: 'ExtJS auto-generated ID' },
64
+ { platform: 'generic', pattern: /^gwt-uid-\d+/, description: 'GWT auto-generated ID' },
65
+ { platform: 'generic', pattern: /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/, description: 'UUID v4' },
66
+ { platform: 'generic', pattern: /^[a-f0-9]{24,}$/, description: 'Long hex hash' },
67
+ { platform: 'generic', pattern: /_\d{10,}$/, description: 'Timestamp suffix' },
68
+ ];
69
+ /**
70
+ * Custom element tag prefixes for enterprise platforms.
71
+ * Maps proprietary tag prefixes to their platform.
72
+ */
73
+ const ENTERPRISE_TAG_PREFIXES = [
74
+ // Salesforce Lightning
75
+ { prefix: 'lightning-', platform: 'salesforce' },
76
+ { prefix: 'c-', platform: 'salesforce-lwc' },
77
+ { prefix: 'force-', platform: 'salesforce' },
78
+ { prefix: 'one-', platform: 'salesforce' },
79
+ { prefix: 'ui-', platform: 'salesforce' },
80
+ { prefix: 'aura-', platform: 'salesforce' },
81
+ { prefix: 'slot-', platform: 'salesforce' },
82
+ { prefix: 'flowruntime-', platform: 'salesforce' },
83
+ // SAP UI5
84
+ { prefix: 'ui5-', platform: 'sap' },
85
+ { prefix: 'sap-', platform: 'sap' },
86
+ // ServiceNow
87
+ { prefix: 'sn-', platform: 'servicenow' },
88
+ { prefix: 'now-', platform: 'servicenow' },
89
+ // Microsoft Dynamics / Fluent UI
90
+ { prefix: 'fluent-', platform: 'dynamics' },
91
+ // Generic web component patterns
92
+ { prefix: 'vaadin-', platform: 'vaadin' },
93
+ { prefix: 'ion-', platform: 'ionic' },
94
+ { prefix: 'mat-', platform: 'angular-material' },
95
+ { prefix: 'mwc-', platform: 'material-web' },
96
+ ];
97
+ /**
98
+ * Stable attribute names that enterprise platforms use as data identifiers.
99
+ * These are more reliable than generated IDs.
100
+ */
101
+ const ENTERPRISE_STABLE_ATTRIBUTES = [
102
+ // SAP
103
+ 'data-sap-ui', 'data-sap-ui-id', 'data-sap-ui-related', 'data-sap-ui-column',
104
+ 'data-sap-ui-rowindex', 'data-sap-ui-colindex',
105
+ // Salesforce
106
+ 'data-aura-rendered-by', 'data-aura-class', 'data-component-id',
107
+ 'data-target-selection-name', 'data-field', 'data-field-id',
108
+ 'data-record-id', 'data-tab-name', 'data-refid',
109
+ // Oracle
110
+ 'data-afr-rkey', 'data-afr-fgid',
111
+ // ServiceNow
112
+ 'data-type', 'data-field-name', 'data-table-name',
113
+ 'data-sys-id', 'data-element',
114
+ // Workday
115
+ 'data-automation-id', 'data-uxi-element-id', 'data-uxi-widget-type',
116
+ // Dynamics 365
117
+ 'data-id', 'data-lp-id', 'data-control-name',
118
+ // Generic stable attributes
119
+ 'data-testid', 'data-test-id', 'data-test', 'data-cy',
120
+ 'data-qa', 'data-automation', 'data-hook',
121
+ 'name', 'aria-label', 'aria-labelledby', 'aria-describedby',
122
+ 'title', 'placeholder', 'alt',
123
+ ];
124
+ // ─── Helper Functions ───────────────────────────────────────────────────────
125
+ /**
126
+ * Detects if an ID appears to be dynamically generated.
127
+ */
128
+ function isDynamicId(id) {
129
+ return DYNAMIC_ID_PATTERNS.some((p) => p.pattern.test(id));
130
+ }
131
+ /**
132
+ * Detects the enterprise platform from page URL, DOM content, or element tags.
133
+ */
134
+ function detectPlatform(url, html) {
135
+ const urlLower = url.toLowerCase();
136
+ const htmlLower = html.toLowerCase();
137
+ // URL-based detection
138
+ if (urlLower.includes('.force.com') || urlLower.includes('.lightning.force') || urlLower.includes('salesforce.com')) {
139
+ return 'salesforce';
140
+ }
141
+ if (urlLower.includes('.sapcloud.') || urlLower.includes('/sap/') || urlLower.includes('fiorilaunchpad')) {
142
+ return 'sap';
143
+ }
144
+ if (urlLower.includes('.oraclecloud.') || urlLower.includes('netsuite.com') || urlLower.includes('oracle.com')) {
145
+ return 'oracle';
146
+ }
147
+ if (urlLower.includes('workday.com') || urlLower.includes('.myworkday.')) {
148
+ return 'workday';
149
+ }
150
+ if (urlLower.includes('service-now.com') || urlLower.includes('servicenow.com')) {
151
+ return 'servicenow';
152
+ }
153
+ if (urlLower.includes('.dynamics.com') || urlLower.includes('crm.dynamics')) {
154
+ return 'dynamics';
155
+ }
156
+ // DOM-based detection
157
+ if (htmlLower.includes('lightning-') || htmlLower.includes('data-aura-rendered-by')) {
158
+ return 'salesforce';
159
+ }
160
+ if (htmlLower.includes('sap-ui-') || htmlLower.includes('ui5-') || htmlLower.includes('data-sap-ui')) {
161
+ return 'sap';
162
+ }
163
+ if (htmlLower.includes('now-') || htmlLower.includes('sn-') || htmlLower.includes('data-table-name')) {
164
+ return 'servicenow';
165
+ }
166
+ if (htmlLower.includes('data-automation-id') || htmlLower.includes('data-uxi-widget-type')) {
167
+ return 'workday';
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * Strips dynamic ID prefixes/suffixes to extract the stable portion.
173
+ * Example: "__xmlview0--loginButton" → "loginButton"
174
+ */
175
+ function extractStableIdPart(id) {
176
+ for (const { pattern } of DYNAMIC_ID_PATTERNS) {
177
+ if (pattern.test(id)) {
178
+ // Try to extract the meaningful suffix after the dynamic prefix
179
+ const stripped = id.replace(pattern, '');
180
+ if (stripped.length > 2) {
181
+ return stripped;
182
+ }
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ /**
188
+ * Levenshtein-based string similarity (0-1).
189
+ */
190
+ function stringSimilarity(a, b) {
191
+ if (a === b)
192
+ return 1;
193
+ if (a.length === 0 || b.length === 0)
194
+ return 0;
195
+ const maxLen = Math.max(a.length, b.length);
196
+ const la = a.length;
197
+ const lb = b.length;
198
+ const prev = new Array(lb + 1);
199
+ for (let j = 0; j <= lb; j++)
200
+ prev[j] = j;
201
+ for (let i = 1; i <= la; i++) {
202
+ let prevDiag = prev[0];
203
+ prev[0] = i;
204
+ for (let j = 1; j <= lb; j++) {
205
+ const temp = prev[j];
206
+ if (a.toLowerCase().charCodeAt(i - 1) === b.toLowerCase().charCodeAt(j - 1)) {
207
+ prev[j] = prevDiag;
208
+ }
209
+ else {
210
+ prev[j] = 1 + Math.min(prevDiag, prev[j], prev[j - 1]);
211
+ }
212
+ prevDiag = temp;
213
+ }
214
+ }
215
+ return 1 - prev[lb] / maxLen;
216
+ }
217
+ /**
218
+ * Extracts candidate elements from the page with enterprise-specific
219
+ * attribute collection. Pierces Shadow DOM and traverses iframes.
220
+ */
221
+ async function extractEnterpriseCandidates(page) {
222
+ const stableAttrNames = [...ENTERPRISE_STABLE_ATTRIBUTES];
223
+ const customPrefixes = ENTERPRISE_TAG_PREFIXES.map((t) => t.prefix);
224
+ try {
225
+ return await page.evaluate(({ stableAttrs, tagPrefixes }) => {
226
+ const candidates = [];
227
+ const MAX_CANDIDATES = 800;
228
+ function isCustomElement(tag) {
229
+ const lower = tag.toLowerCase();
230
+ for (const prefix of tagPrefixes) {
231
+ if (lower.startsWith(prefix))
232
+ return lower;
233
+ }
234
+ // Custom elements always contain a hyphen
235
+ if (lower.includes('-') && !lower.startsWith('data-'))
236
+ return lower;
237
+ return null;
238
+ }
239
+ function collectFromRoot(root, inShadow, iframeDepth) {
240
+ if (candidates.length >= MAX_CANDIDATES)
241
+ return;
242
+ // Broad selector for interactive + semantic elements
243
+ const selectors = [
244
+ 'button', 'input', 'select', 'textarea', 'a[href]',
245
+ '[role]', '[aria-label]', '[data-testid]', '[data-test-id]',
246
+ '[data-test]', '[data-cy]', '[data-qa]', '[data-automation]',
247
+ '[data-automation-id]', '[data-field]', '[data-field-name]',
248
+ '[data-component-id]', '[data-control-name]', '[data-hook]',
249
+ '[data-sap-ui]', '[data-sap-ui-id]', '[data-aura-rendered-by]',
250
+ '[data-uxi-element-id]', '[name]',
251
+ 'h1', 'h2', 'h3', 'h4', 'label', 'th', 'td',
252
+ 'li', 'option', 'summary',
253
+ ];
254
+ let elements;
255
+ try {
256
+ elements = Array.from(root.querySelectorAll(selectors.join(',')));
257
+ }
258
+ catch {
259
+ elements = Array.from(root.querySelectorAll('*'));
260
+ }
261
+ // Also collect custom elements
262
+ try {
263
+ const allEls = Array.from(root.querySelectorAll('*'));
264
+ for (const el of allEls) {
265
+ if (candidates.length >= MAX_CANDIDATES)
266
+ break;
267
+ if (isCustomElement(el.tagName) && !elements.includes(el)) {
268
+ elements.push(el);
269
+ }
270
+ }
271
+ }
272
+ catch {
273
+ // Ignore
274
+ }
275
+ for (const el of elements) {
276
+ if (candidates.length >= MAX_CANDIDATES)
277
+ break;
278
+ const tag = el.tagName.toLowerCase();
279
+ const id = el.id || '';
280
+ const text = (el.textContent || '').trim().substring(0, 200);
281
+ const role = el.getAttribute('role') || '';
282
+ const ariaLabel = el.getAttribute('aria-label') || '';
283
+ const placeholder = el.getAttribute('placeholder') || '';
284
+ const name = el.getAttribute('name') || '';
285
+ const title = el.getAttribute('title') || '';
286
+ const href = el.getAttribute('href') || '';
287
+ // Collect stable enterprise attributes
288
+ const stableAttrMap = {};
289
+ for (const attrName of stableAttrs) {
290
+ const val = el.getAttribute(attrName);
291
+ if (val)
292
+ stableAttrMap[attrName] = val;
293
+ }
294
+ const classes = Array.from(el.classList).slice(0, 10);
295
+ candidates.push({
296
+ tag,
297
+ id,
298
+ stableId: null, // Computed post-extraction
299
+ classes,
300
+ text,
301
+ role,
302
+ ariaLabel,
303
+ stableAttrs: stableAttrMap,
304
+ placeholder,
305
+ name,
306
+ title,
307
+ href,
308
+ isInsideShadowDOM: inShadow,
309
+ iframeDepth,
310
+ customElementTag: isCustomElement(tag),
311
+ platform: null, // Computed post-extraction
312
+ });
313
+ }
314
+ // Recurse into shadow roots
315
+ const shadowHosts = Array.from(root.querySelectorAll('*'));
316
+ for (const el of shadowHosts) {
317
+ if (candidates.length >= MAX_CANDIDATES)
318
+ break;
319
+ if (el.shadowRoot) {
320
+ collectFromRoot(el.shadowRoot, true, iframeDepth);
321
+ }
322
+ }
323
+ }
324
+ collectFromRoot(document, false, 0);
325
+ // Also try to collect from accessible iframes
326
+ try {
327
+ const iframes = Array.from(document.querySelectorAll('iframe'));
328
+ for (const iframe of iframes) {
329
+ if (candidates.length >= MAX_CANDIDATES)
330
+ break;
331
+ try {
332
+ const iframeDoc = iframe.contentDocument;
333
+ if (iframeDoc) {
334
+ collectFromRoot(iframeDoc, false, 1);
335
+ }
336
+ }
337
+ catch {
338
+ // Cross-origin iframe — skip
339
+ }
340
+ }
341
+ }
342
+ catch {
343
+ // Ignore
344
+ }
345
+ return candidates;
346
+ }, { stableAttrs: stableAttrNames, tagPrefixes: customPrefixes });
347
+ }
348
+ catch (err) {
349
+ logger.warn(`[Enterprise] Failed to extract candidates: ${err instanceof Error ? err.message : err}`);
350
+ return [];
351
+ }
352
+ }
353
+ // ─── Scoring Engine ─────────────────────────────────────────────────────────
354
+ /**
355
+ * Scores a candidate against the original broken locator.
356
+ * Uses a multi-signal approach weighting enterprise-specific attributes higher.
357
+ */
358
+ function scoreCandidate(candidate, originalLocator, detectedPlatform) {
359
+ const origSelector = originalLocator.selector;
360
+ const origExpr = originalLocator.playwrightExpression;
361
+ const origType = originalLocator.type;
362
+ let bestScore = 0;
363
+ let matchType = '';
364
+ let newSelector = '';
365
+ let newType = 'css';
366
+ let expression = '';
367
+ // ── Signal 1: Stable ID match (dynamic ID stripped) ───────────────────
368
+ if (candidate.id) {
369
+ const stableId = extractStableIdPart(candidate.id);
370
+ if (stableId) {
371
+ candidate.stableId = stableId;
372
+ // Check if the original selector contains the stable part
373
+ const origStable = extractStableIdPart(origSelector.replace(/^#/, '')) ?? origSelector.replace(/^#/, '');
374
+ const similarity = stringSimilarity(stableId, origStable);
375
+ if (similarity > bestScore) {
376
+ bestScore = similarity;
377
+ matchType = 'stable-id';
378
+ newSelector = `[id$="${stableId}"]`;
379
+ newType = 'css';
380
+ expression = `page.locator('[id$="${stableId}"]')`;
381
+ }
382
+ }
383
+ }
384
+ // ── Signal 2: Enterprise stable attributes ────────────────────────────
385
+ for (const [attrName, attrValue] of Object.entries(candidate.stableAttrs)) {
386
+ if (!attrValue)
387
+ continue;
388
+ // data-testid, data-automation-id etc. are very high confidence
389
+ const isTestAttribute = attrName.includes('testid') || attrName.includes('test-id')
390
+ || attrName.includes('automation') || attrName.includes('data-cy')
391
+ || attrName.includes('data-qa') || attrName.includes('data-hook');
392
+ // Check if original selector referenced this attribute
393
+ const origReferencesAttr = origSelector.includes(attrName) || origSelector.includes(attrValue);
394
+ let attrScore = 0;
395
+ if (origReferencesAttr) {
396
+ attrScore = 0.95;
397
+ }
398
+ else if (isTestAttribute) {
399
+ // Test attributes on elements with similar text/role
400
+ const textSim = stringSimilarity(candidate.text.toLowerCase(), origSelector.toLowerCase());
401
+ attrScore = 0.7 + (textSim * 0.2);
402
+ }
403
+ else {
404
+ // Other stable attribute — moderate match
405
+ const valueSim = stringSimilarity(attrValue.toLowerCase(), origSelector.replace(/^[#.]/, '').toLowerCase());
406
+ attrScore = valueSim * 0.6;
407
+ }
408
+ if (attrScore > bestScore) {
409
+ bestScore = attrScore;
410
+ if (attrName === 'data-testid' || attrName === 'data-test-id') {
411
+ matchType = 'enterprise-testid';
412
+ newSelector = attrValue;
413
+ newType = 'testid';
414
+ expression = `page.getByTestId('${attrValue}')`;
415
+ }
416
+ else if (attrName === 'aria-label') {
417
+ matchType = 'enterprise-aria-label';
418
+ newSelector = attrValue;
419
+ newType = 'label';
420
+ expression = `page.getByLabel('${attrValue}')`;
421
+ }
422
+ else if (attrName === 'placeholder') {
423
+ matchType = 'enterprise-placeholder';
424
+ newSelector = attrValue;
425
+ newType = 'placeholder';
426
+ expression = `page.getByPlaceholder('${attrValue}')`;
427
+ }
428
+ else if (attrName === 'title') {
429
+ matchType = 'enterprise-title';
430
+ newSelector = attrValue;
431
+ newType = 'title';
432
+ expression = `page.getByTitle('${attrValue}')`;
433
+ }
434
+ else {
435
+ matchType = `enterprise-attr:${attrName}`;
436
+ newSelector = `[${attrName}="${attrValue}"]`;
437
+ newType = 'css';
438
+ expression = `page.locator('[${attrName}="${attrValue}"]')`;
439
+ }
440
+ }
441
+ }
442
+ // ── Signal 3: ARIA role + name matching ───────────────────────────────
443
+ if (candidate.role) {
444
+ let roleScore = 0;
445
+ const origRefRole = origExpr.includes('getByRole') || origExpr.includes(`role="${candidate.role}"`);
446
+ if (origRefRole) {
447
+ roleScore = 0.85;
448
+ }
449
+ else {
450
+ roleScore = 0.4;
451
+ }
452
+ // Boost if aria-label matches original selector text
453
+ if (candidate.ariaLabel) {
454
+ const labelSim = stringSimilarity(candidate.ariaLabel.toLowerCase(), origSelector.replace(/^[#.]/, '').replace(/[-_]/g, ' ').toLowerCase());
455
+ roleScore = Math.max(roleScore, 0.5 + labelSim * 0.4);
456
+ }
457
+ if (roleScore > bestScore) {
458
+ bestScore = roleScore;
459
+ matchType = 'enterprise-role';
460
+ newSelector = candidate.role;
461
+ newType = 'role';
462
+ const nameOpt = candidate.ariaLabel ? `, { name: '${candidate.ariaLabel}' }` : '';
463
+ expression = `page.getByRole('${candidate.role}'${nameOpt})`;
464
+ }
465
+ }
466
+ // ── Signal 4: Text content matching ───────────────────────────────────
467
+ if (candidate.text && candidate.text.length > 0 && candidate.text.length < 100) {
468
+ const cleanText = candidate.text.replace(/\s+/g, ' ').trim();
469
+ const origClean = origSelector.replace(/^[#.]/, '').replace(/[-_]/g, ' ').toLowerCase();
470
+ const textSim = stringSimilarity(cleanText.toLowerCase(), origClean);
471
+ // For text-based original locators, text matching is very relevant
472
+ const origIsTextBased = origType === 'text' || origExpr.includes('getByText');
473
+ const textScore = origIsTextBased
474
+ ? textSim * 0.9
475
+ : textSim * 0.5;
476
+ if (textScore > bestScore && cleanText.length > 1) {
477
+ bestScore = textScore;
478
+ matchType = 'enterprise-text';
479
+ newSelector = cleanText;
480
+ newType = 'text';
481
+ expression = `page.getByText('${cleanText.replace(/'/g, "\\'")}')`;
482
+ }
483
+ }
484
+ // ── Signal 5: Custom element tag matching ─────────────────────────────
485
+ if (candidate.customElementTag) {
486
+ const tagSim = stringSimilarity(candidate.customElementTag, origSelector.replace(/^[#.]/, '').toLowerCase());
487
+ // Custom elements combined with stable attributes are very reliable
488
+ const hasStableAttr = Object.keys(candidate.stableAttrs).length > 0;
489
+ const customScore = hasStableAttr ? 0.5 + tagSim * 0.3 : tagSim * 0.4;
490
+ if (customScore > bestScore) {
491
+ bestScore = customScore;
492
+ matchType = 'enterprise-custom-element';
493
+ // Build a selector using the custom element + best stable attribute
494
+ const bestAttr = Object.entries(candidate.stableAttrs)[0];
495
+ if (bestAttr) {
496
+ newSelector = `${candidate.customElementTag}[${bestAttr[0]}="${bestAttr[1]}"]`;
497
+ }
498
+ else if (candidate.ariaLabel) {
499
+ newSelector = `${candidate.customElementTag}[aria-label="${candidate.ariaLabel}"]`;
500
+ }
501
+ else {
502
+ newSelector = candidate.customElementTag;
503
+ }
504
+ newType = 'css';
505
+ expression = `page.locator('${newSelector}')`;
506
+ }
507
+ }
508
+ // ── Signal 6: Name attribute matching ─────────────────────────────────
509
+ if (candidate.name) {
510
+ const nameSim = stringSimilarity(candidate.name.toLowerCase(), origSelector.replace(/^[#.]/, '').toLowerCase());
511
+ const nameScore = nameSim * 0.7;
512
+ if (nameScore > bestScore) {
513
+ bestScore = nameScore;
514
+ matchType = 'enterprise-name';
515
+ newSelector = `[name="${candidate.name}"]`;
516
+ newType = 'css';
517
+ expression = `page.locator('[name="${candidate.name}"]')`;
518
+ }
519
+ }
520
+ // ── Confidence adjustments ────────────────────────────────────────────
521
+ // Boost for same-platform matches when platform is detected
522
+ if (detectedPlatform && candidate.platform === detectedPlatform) {
523
+ bestScore = Math.min(1, bestScore * 1.1);
524
+ }
525
+ // Slight penalty for shadow DOM (harder to verify)
526
+ if (candidate.isInsideShadowDOM) {
527
+ bestScore *= 0.95;
528
+ }
529
+ // Penalty for deep iframe nesting
530
+ if (candidate.iframeDepth > 0) {
531
+ bestScore *= 0.9;
532
+ }
533
+ if (bestScore < 0.35)
534
+ return null;
535
+ return {
536
+ score: bestScore,
537
+ matchType,
538
+ newSelector,
539
+ newType,
540
+ expression,
541
+ };
542
+ }
543
+ // ─── Main Enterprise Strategy ───────────────────────────────────────────────
544
+ /**
545
+ * Enterprise healing strategy.
546
+ *
547
+ * Handles dynamic IDs, custom web components, enterprise-specific stable
548
+ * attributes, and Shadow DOM / iframe piercing for SAP, Salesforce,
549
+ * Oracle, Workday, ServiceNow, Dynamics 365, and similar platforms.
550
+ */
551
+ async function enterpriseStrategy(page, originalLocator, domSnapshot) {
552
+ const start = Date.now();
553
+ const strategy = 'enterprise';
554
+ // Detect which platform we're dealing with
555
+ const detectedPlatform = detectPlatform(domSnapshot.url, domSnapshot.html);
556
+ if (detectedPlatform) {
557
+ logger.info(`[Enterprise] Detected platform: ${detectedPlatform}`);
558
+ }
559
+ // Check if original selector contains a dynamic ID
560
+ const origId = originalLocator.selector.replace(/^#/, '');
561
+ const hasDynamicId = isDynamicId(origId);
562
+ if (hasDynamicId) {
563
+ const stablePart = extractStableIdPart(origId);
564
+ logger.debug(`[Enterprise] Original selector has dynamic ID. Stable part: "${stablePart ?? 'none'}"`);
565
+ }
566
+ // Extract candidates with enterprise-aware attribute collection
567
+ const candidates = await extractEnterpriseCandidates(page);
568
+ if (candidates.length === 0) {
569
+ return {
570
+ strategy,
571
+ locator: null,
572
+ confidence: 0,
573
+ duration: Date.now() - start,
574
+ error: 'No enterprise candidates found',
575
+ };
576
+ }
577
+ logger.debug(`[Enterprise] Extracted ${candidates.length} candidates`);
578
+ // Tag candidates with platform info
579
+ for (const c of candidates) {
580
+ if (c.customElementTag) {
581
+ const match = ENTERPRISE_TAG_PREFIXES.find((p) => c.customElementTag.startsWith(p.prefix));
582
+ if (match)
583
+ c.platform = match.platform;
584
+ }
585
+ }
586
+ // Score all candidates
587
+ let bestResult = null;
588
+ for (const candidate of candidates) {
589
+ const result = scoreCandidate(candidate, originalLocator, detectedPlatform);
590
+ if (result && (!bestResult || result.score > bestResult.score)) {
591
+ bestResult = result;
592
+ }
593
+ }
594
+ if (!bestResult) {
595
+ return {
596
+ strategy,
597
+ locator: null,
598
+ confidence: 0,
599
+ duration: Date.now() - start,
600
+ };
601
+ }
602
+ logger.info(`[Enterprise] Best match: ${bestResult.matchType} → "${bestResult.expression}" ` +
603
+ `(confidence: ${bestResult.score.toFixed(3)})`);
604
+ return {
605
+ strategy,
606
+ locator: {
607
+ type: bestResult.newType,
608
+ selector: bestResult.newSelector,
609
+ playwrightExpression: bestResult.expression,
610
+ },
611
+ confidence: bestResult.score,
612
+ duration: Date.now() - start,
613
+ };
614
+ }
615
+ // ─── Wait Strategy for Enterprise Loading Patterns ──────────────────────────
616
+ /**
617
+ * Enterprise applications often have extended loading times with skeleton
618
+ * screens, spinners, and async data fetches. This helper waits for
619
+ * enterprise-specific loading indicators to disappear.
620
+ */
621
+ async function waitForEnterpriseLoad(page, timeout = 15000) {
622
+ const loadingSelectors = [
623
+ // SAP
624
+ '.sapUiLocalBusyIndicator',
625
+ '.sapMBusyDialog',
626
+ '#sap-ui-blocklayer-popup',
627
+ 'ui5-busy-indicator[active]',
628
+ // Salesforce
629
+ '.slds-spinner_container',
630
+ 'lightning-spinner',
631
+ '.forceListViewManagerLoading',
632
+ '[role="progressbar"]',
633
+ // ServiceNow
634
+ '.loading-placeholder',
635
+ '.sn-loading',
636
+ // Workday
637
+ '.wd-LoadingPanel',
638
+ '[data-automation-id="loadingSpinner"]',
639
+ // Generic
640
+ '.skeleton',
641
+ '.shimmer',
642
+ '[aria-busy="true"]',
643
+ ];
644
+ for (const selector of loadingSelectors) {
645
+ try {
646
+ const locator = page.locator(selector);
647
+ const count = await locator.count();
648
+ if (count > 0) {
649
+ logger.debug(`[Enterprise] Waiting for loading indicator: ${selector}`);
650
+ await locator.first().waitFor({ state: 'hidden', timeout });
651
+ }
652
+ }
653
+ catch {
654
+ // Timeout or element not found — continue
655
+ }
656
+ }
657
+ }
658
+ /**
659
+ * Scrolls a virtual scroll container to try to bring more elements into view.
660
+ * Enterprise grids (SAP ALV, Salesforce report tables) often virtualize rows.
661
+ */
662
+ async function scrollVirtualContainer(page, containerSelector) {
663
+ const defaultContainers = [
664
+ '.sapUiTableCCnt', // SAP UI5 Table
665
+ '.sapMListItems', // SAP Mobile List
666
+ '.slds-scrollable_y', // Salesforce SLDS
667
+ '.virtualScrollInner', // Generic virtual scroll
668
+ '[role="grid"]', // ARIA grid
669
+ '[role="listbox"]', // ARIA listbox
670
+ '.ag-body-viewport', // AG Grid
671
+ '.dx-scrollable-container', // DevExtreme
672
+ ];
673
+ const selectors = containerSelector ? [containerSelector] : defaultContainers;
674
+ for (const selector of selectors) {
675
+ try {
676
+ const container = page.locator(selector).first();
677
+ const count = await container.count();
678
+ if (count > 0) {
679
+ logger.debug(`[Enterprise] Scrolling virtual container: ${selector}`);
680
+ await container.evaluate((el) => {
681
+ el.scrollTop += 500;
682
+ });
683
+ // Give the virtual scroll time to render
684
+ await page.waitForTimeout(300);
685
+ break;
686
+ }
687
+ }
688
+ catch {
689
+ // Continue to next container
690
+ }
691
+ }
692
+ }
693
+
694
+ export { detectPlatform, enterpriseStrategy, extractStableIdPart, isDynamicId, scrollVirtualContainer, waitForEnterpriseLoad };
695
+ //# sourceMappingURL=enterprise-strategy.js.map