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.
- package/.env.example +48 -0
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +481 -0
- package/dist/cjs/ai/ai-provider.js +46 -0
- package/dist/cjs/ai/ai-provider.js.map +1 -0
- package/dist/cjs/ai/anthropic-provider.js +106 -0
- package/dist/cjs/ai/anthropic-provider.js.map +1 -0
- package/dist/cjs/ai/azure-openai-provider.js +130 -0
- package/dist/cjs/ai/azure-openai-provider.js.map +1 -0
- package/dist/cjs/ai/bedrock-provider.js +183 -0
- package/dist/cjs/ai/bedrock-provider.js.map +1 -0
- package/dist/cjs/ai/deepseek-provider.js +118 -0
- package/dist/cjs/ai/deepseek-provider.js.map +1 -0
- package/dist/cjs/ai/gemini-provider.js +129 -0
- package/dist/cjs/ai/gemini-provider.js.map +1 -0
- package/dist/cjs/ai/groq-provider.js +118 -0
- package/dist/cjs/ai/groq-provider.js.map +1 -0
- package/dist/cjs/ai/meta-provider.js +118 -0
- package/dist/cjs/ai/meta-provider.js.map +1 -0
- package/dist/cjs/ai/ollama-provider.js +127 -0
- package/dist/cjs/ai/ollama-provider.js.map +1 -0
- package/dist/cjs/ai/openai-provider.js +117 -0
- package/dist/cjs/ai/openai-provider.js.map +1 -0
- package/dist/cjs/ai/perplexity-provider.js +118 -0
- package/dist/cjs/ai/perplexity-provider.js.map +1 -0
- package/dist/cjs/ai/prompt-templates.js +174 -0
- package/dist/cjs/ai/prompt-templates.js.map +1 -0
- package/dist/cjs/ai/qwen-provider.js +118 -0
- package/dist/cjs/ai/qwen-provider.js.map +1 -0
- package/dist/cjs/analytics/healing-analytics.js +263 -0
- package/dist/cjs/analytics/healing-analytics.js.map +1 -0
- package/dist/cjs/cli/init.js +517 -0
- package/dist/cjs/cli/init.js.map +1 -0
- package/dist/cjs/config/config-loader.js +135 -0
- package/dist/cjs/config/config-loader.js.map +1 -0
- package/dist/cjs/config/defaults.js +109 -0
- package/dist/cjs/config/defaults.js.map +1 -0
- package/dist/cjs/core/dom-snapshot.js +280 -0
- package/dist/cjs/core/dom-snapshot.js.map +1 -0
- package/dist/cjs/core/enterprise-strategy.js +702 -0
- package/dist/cjs/core/enterprise-strategy.js.map +1 -0
- package/dist/cjs/core/healer.js +283 -0
- package/dist/cjs/core/healer.js.map +1 -0
- package/dist/cjs/core/interceptor.js +945 -0
- package/dist/cjs/core/interceptor.js.map +1 -0
- package/dist/cjs/core/locator-analyzer.js +172 -0
- package/dist/cjs/core/locator-analyzer.js.map +1 -0
- package/dist/cjs/core/locator-strategies.js +891 -0
- package/dist/cjs/core/locator-strategies.js.map +1 -0
- package/dist/cjs/core/self-heal-cache.js +178 -0
- package/dist/cjs/core/self-heal-cache.js.map +1 -0
- package/dist/cjs/core/smart-retry.js +248 -0
- package/dist/cjs/core/smart-retry.js.map +1 -0
- package/dist/cjs/core/visual-verification.js +262 -0
- package/dist/cjs/core/visual-verification.js.map +1 -0
- package/dist/cjs/git/code-modifier.js +184 -0
- package/dist/cjs/git/code-modifier.js.map +1 -0
- package/dist/cjs/git/git-operations.js +145 -0
- package/dist/cjs/git/git-operations.js.map +1 -0
- package/dist/cjs/git/pr-creator.js +190 -0
- package/dist/cjs/git/pr-creator.js.map +1 -0
- package/dist/cjs/index.js +97 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/rag/context-retriever.js +289 -0
- package/dist/cjs/rag/context-retriever.js.map +1 -0
- package/dist/cjs/rag/embeddings.js +82 -0
- package/dist/cjs/rag/embeddings.js.map +1 -0
- package/dist/cjs/rag/knowledge-store.js +159 -0
- package/dist/cjs/rag/knowledge-store.js.map +1 -0
- package/dist/cjs/reporters/heal-report.js +279 -0
- package/dist/cjs/reporters/heal-report.js.map +1 -0
- package/dist/cjs/reporters/heal-reporter.js +294 -0
- package/dist/cjs/reporters/heal-reporter.js.map +1 -0
- package/dist/cjs/server/review-server.js +166 -0
- package/dist/cjs/server/review-server.js.map +1 -0
- package/dist/cjs/server/routes.js +92 -0
- package/dist/cjs/server/routes.js.map +1 -0
- package/dist/cjs/utils/environment.js +57 -0
- package/dist/cjs/utils/environment.js.map +1 -0
- package/dist/cjs/utils/file-lock.js +136 -0
- package/dist/cjs/utils/file-lock.js.map +1 -0
- package/dist/cjs/utils/file-utils.js +49 -0
- package/dist/cjs/utils/file-utils.js.map +1 -0
- package/dist/cjs/utils/logger.js +78 -0
- package/dist/cjs/utils/logger.js.map +1 -0
- package/dist/esm/ai/ai-provider.js +44 -0
- package/dist/esm/ai/ai-provider.js.map +1 -0
- package/dist/esm/ai/anthropic-provider.js +104 -0
- package/dist/esm/ai/anthropic-provider.js.map +1 -0
- package/dist/esm/ai/azure-openai-provider.js +128 -0
- package/dist/esm/ai/azure-openai-provider.js.map +1 -0
- package/dist/esm/ai/bedrock-provider.js +181 -0
- package/dist/esm/ai/bedrock-provider.js.map +1 -0
- package/dist/esm/ai/deepseek-provider.js +116 -0
- package/dist/esm/ai/deepseek-provider.js.map +1 -0
- package/dist/esm/ai/gemini-provider.js +127 -0
- package/dist/esm/ai/gemini-provider.js.map +1 -0
- package/dist/esm/ai/groq-provider.js +116 -0
- package/dist/esm/ai/groq-provider.js.map +1 -0
- package/dist/esm/ai/meta-provider.js +116 -0
- package/dist/esm/ai/meta-provider.js.map +1 -0
- package/dist/esm/ai/ollama-provider.js +125 -0
- package/dist/esm/ai/ollama-provider.js.map +1 -0
- package/dist/esm/ai/openai-provider.js +115 -0
- package/dist/esm/ai/openai-provider.js.map +1 -0
- package/dist/esm/ai/perplexity-provider.js +116 -0
- package/dist/esm/ai/perplexity-provider.js.map +1 -0
- package/dist/esm/ai/prompt-templates.js +171 -0
- package/dist/esm/ai/prompt-templates.js.map +1 -0
- package/dist/esm/ai/qwen-provider.js +116 -0
- package/dist/esm/ai/qwen-provider.js.map +1 -0
- package/dist/esm/analytics/healing-analytics.js +261 -0
- package/dist/esm/analytics/healing-analytics.js.map +1 -0
- package/dist/esm/cli/init.js +495 -0
- package/dist/esm/cli/init.js.map +1 -0
- package/dist/esm/config/config-loader.js +132 -0
- package/dist/esm/config/config-loader.js.map +1 -0
- package/dist/esm/config/defaults.js +107 -0
- package/dist/esm/config/defaults.js.map +1 -0
- package/dist/esm/core/dom-snapshot.js +278 -0
- package/dist/esm/core/dom-snapshot.js.map +1 -0
- package/dist/esm/core/enterprise-strategy.js +695 -0
- package/dist/esm/core/enterprise-strategy.js.map +1 -0
- package/dist/esm/core/healer.js +281 -0
- package/dist/esm/core/healer.js.map +1 -0
- package/dist/esm/core/interceptor.js +940 -0
- package/dist/esm/core/interceptor.js.map +1 -0
- package/dist/esm/core/locator-analyzer.js +169 -0
- package/dist/esm/core/locator-analyzer.js.map +1 -0
- package/dist/esm/core/locator-strategies.js +882 -0
- package/dist/esm/core/locator-strategies.js.map +1 -0
- package/dist/esm/core/self-heal-cache.js +176 -0
- package/dist/esm/core/self-heal-cache.js.map +1 -0
- package/dist/esm/core/smart-retry.js +246 -0
- package/dist/esm/core/smart-retry.js.map +1 -0
- package/dist/esm/core/visual-verification.js +260 -0
- package/dist/esm/core/visual-verification.js.map +1 -0
- package/dist/esm/git/code-modifier.js +182 -0
- package/dist/esm/git/code-modifier.js.map +1 -0
- package/dist/esm/git/git-operations.js +143 -0
- package/dist/esm/git/git-operations.js.map +1 -0
- package/dist/esm/git/pr-creator.js +188 -0
- package/dist/esm/git/pr-creator.js.map +1 -0
- package/dist/esm/index.js +37 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/rag/context-retriever.js +287 -0
- package/dist/esm/rag/context-retriever.js.map +1 -0
- package/dist/esm/rag/embeddings.js +77 -0
- package/dist/esm/rag/embeddings.js.map +1 -0
- package/dist/esm/rag/knowledge-store.js +157 -0
- package/dist/esm/rag/knowledge-store.js.map +1 -0
- package/dist/esm/reporters/heal-report.js +277 -0
- package/dist/esm/reporters/heal-report.js.map +1 -0
- package/dist/esm/reporters/heal-reporter.js +290 -0
- package/dist/esm/reporters/heal-reporter.js.map +1 -0
- package/dist/esm/server/review-server.js +164 -0
- package/dist/esm/server/review-server.js.map +1 -0
- package/dist/esm/server/routes.js +90 -0
- package/dist/esm/server/routes.js.map +1 -0
- package/dist/esm/utils/environment.js +53 -0
- package/dist/esm/utils/environment.js.map +1 -0
- package/dist/esm/utils/file-lock.js +134 -0
- package/dist/esm/utils/file-lock.js.map +1 -0
- package/dist/esm/utils/file-utils.js +43 -0
- package/dist/esm/utils/file-utils.js.map +1 -0
- package/dist/esm/utils/logger.js +75 -0
- package/dist/esm/utils/logger.js.map +1 -0
- package/dist/types/ai/ai-provider.d.ts +4 -0
- package/dist/types/ai/ai-provider.d.ts.map +1 -0
- package/dist/types/ai/anthropic-provider.d.ts +11 -0
- package/dist/types/ai/anthropic-provider.d.ts.map +1 -0
- package/dist/types/ai/azure-openai-provider.d.ts +13 -0
- package/dist/types/ai/azure-openai-provider.d.ts.map +1 -0
- package/dist/types/ai/bedrock-provider.d.ts +14 -0
- package/dist/types/ai/bedrock-provider.d.ts.map +1 -0
- package/dist/types/ai/deepseek-provider.d.ts +12 -0
- package/dist/types/ai/deepseek-provider.d.ts.map +1 -0
- package/dist/types/ai/gemini-provider.d.ts +12 -0
- package/dist/types/ai/gemini-provider.d.ts.map +1 -0
- package/dist/types/ai/groq-provider.d.ts +12 -0
- package/dist/types/ai/groq-provider.d.ts.map +1 -0
- package/dist/types/ai/meta-provider.d.ts +12 -0
- package/dist/types/ai/meta-provider.d.ts.map +1 -0
- package/dist/types/ai/ollama-provider.d.ts +10 -0
- package/dist/types/ai/ollama-provider.d.ts.map +1 -0
- package/dist/types/ai/openai-provider.d.ts +11 -0
- package/dist/types/ai/openai-provider.d.ts.map +1 -0
- package/dist/types/ai/perplexity-provider.d.ts +12 -0
- package/dist/types/ai/perplexity-provider.d.ts.map +1 -0
- package/dist/types/ai/prompt-templates.d.ts +11 -0
- package/dist/types/ai/prompt-templates.d.ts.map +1 -0
- package/dist/types/ai/qwen-provider.d.ts +12 -0
- package/dist/types/ai/qwen-provider.d.ts.map +1 -0
- package/dist/types/analytics/healing-analytics.d.ts +36 -0
- package/dist/types/analytics/healing-analytics.d.ts.map +1 -0
- package/dist/types/cli/init.d.ts +15 -0
- package/dist/types/cli/init.d.ts.map +1 -0
- package/dist/types/config/config-loader.d.ts +4 -0
- package/dist/types/config/config-loader.d.ts.map +1 -0
- package/dist/types/config/defaults.d.ts +3 -0
- package/dist/types/config/defaults.d.ts.map +1 -0
- package/dist/types/core/dom-snapshot.d.ts +12 -0
- package/dist/types/core/dom-snapshot.d.ts.map +1 -0
- package/dist/types/core/enterprise-strategy.d.ts +56 -0
- package/dist/types/core/enterprise-strategy.d.ts.map +1 -0
- package/dist/types/core/healer.d.ts +52 -0
- package/dist/types/core/healer.d.ts.map +1 -0
- package/dist/types/core/interceptor.d.ts +64 -0
- package/dist/types/core/interceptor.d.ts.map +1 -0
- package/dist/types/core/locator-analyzer.d.ts +31 -0
- package/dist/types/core/locator-analyzer.d.ts.map +1 -0
- package/dist/types/core/locator-strategies.d.ts +45 -0
- package/dist/types/core/locator-strategies.d.ts.map +1 -0
- package/dist/types/core/self-heal-cache.d.ts +51 -0
- package/dist/types/core/self-heal-cache.d.ts.map +1 -0
- package/dist/types/core/smart-retry.d.ts +64 -0
- package/dist/types/core/smart-retry.d.ts.map +1 -0
- package/dist/types/core/visual-verification.d.ts +46 -0
- package/dist/types/core/visual-verification.d.ts.map +1 -0
- package/dist/types/git/code-modifier.d.ts +51 -0
- package/dist/types/git/code-modifier.d.ts.map +1 -0
- package/dist/types/git/git-operations.d.ts +40 -0
- package/dist/types/git/git-operations.d.ts.map +1 -0
- package/dist/types/git/pr-creator.d.ts +27 -0
- package/dist/types/git/pr-creator.d.ts.map +1 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/rag/context-retriever.d.ts +69 -0
- package/dist/types/rag/context-retriever.d.ts.map +1 -0
- package/dist/types/rag/embeddings.d.ts +32 -0
- package/dist/types/rag/embeddings.d.ts.map +1 -0
- package/dist/types/rag/index.d.ts +12 -0
- package/dist/types/rag/index.d.ts.map +1 -0
- package/dist/types/rag/knowledge-store.d.ts +38 -0
- package/dist/types/rag/knowledge-store.d.ts.map +1 -0
- package/dist/types/reporters/heal-report.d.ts +29 -0
- package/dist/types/reporters/heal-report.d.ts.map +1 -0
- package/dist/types/reporters/heal-reporter.d.ts +49 -0
- package/dist/types/reporters/heal-reporter.d.ts.map +1 -0
- package/dist/types/server/review-server.d.ts +20 -0
- package/dist/types/server/review-server.d.ts.map +1 -0
- package/dist/types/server/routes.d.ts +4 -0
- package/dist/types/server/routes.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +433 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/utils/environment.d.ts +10 -0
- package/dist/types/utils/environment.d.ts.map +1 -0
- package/dist/types/utils/file-lock.d.ts +37 -0
- package/dist/types/utils/file-lock.d.ts.map +1 -0
- package/dist/types/utils/file-utils.d.ts +7 -0
- package/dist/types/utils/file-utils.d.ts.map +1 -0
- package/dist/types/utils/logger.d.ts +9 -0
- package/dist/types/utils/logger.d.ts.map +1 -0
- 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
|