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