playwright-self-healer
Version:
A powerful self-healing automation tool for Playwright that automatically finds alternative selectors when original ones fail, including CSS and XPath support, similar to Healenium but built for modern web applications. Now with comprehensive XPath healin
825 lines • 41.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealingPage = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const STORE_PATH = path_1.default.join(__dirname, '..', 'healed-selectors.json');
const CANDIDATE_TAGS = ['button', 'a', 'input', 'span', 'div', 'label', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'td', 'th'];
const SIMILARITY_THRESHOLD = 0.6;
const MAX_HEALING_ATTEMPTS = 5;
class HealingPage {
constructor(page) {
this.page = page;
this.selectorMap = this.loadHealedSelectors();
this.healingAttempts = new Map();
this.initializeHealingStrategies();
}
initializeHealingStrategies() {
this.healingStrategies = [
{
name: 'ID-based healing',
priority: 1,
heal: this.healById.bind(this)
},
{
name: 'Text-based healing',
priority: 2,
heal: this.healByText.bind(this)
},
{
name: 'Attribute-based healing',
priority: 3,
heal: this.healByAttributes.bind(this)
},
{
name: 'Position-based healing',
priority: 4,
heal: this.healByPosition.bind(this)
},
{
name: 'Similarity-based healing',
priority: 5,
heal: this.healBySimilarity.bind(this)
},
{
name: 'XPath-based healing',
priority: 6,
heal: this.healByXPath.bind(this)
}
].sort((a, b) => a.priority - b.priority);
}
async goto(url) {
await this.page.goto(url);
this.healingAttempts.clear(); // Clear attempts on page navigation
}
async click(selector, options) {
return this.performAction('click', selector, undefined, options);
}
async fill(selector, value, options) {
return this.performAction('fill', selector, value, options);
}
async check(selector, options) {
return this.performAction('check', selector, undefined, options);
}
async uncheck(selector, options) {
return this.performAction('uncheck', selector, undefined, options);
}
async hover(selector, options) {
return this.performAction('hover', selector, undefined, options);
}
async type(selector, text, options) {
return this.performAction('type', selector, text, options);
}
async selectOption(selector, value, options) {
return this.performAction('selectOption', selector, value, options);
}
async getText(selector, options) {
return this.performAction('getText', selector, undefined, options);
}
async getAttribute(selector, name, options) {
return this.performAction('getAttribute', selector, name, options);
}
async isVisible(selector, options) {
return this.performAction('isVisible', selector, undefined, options);
}
async waitForSelector(selector, options) {
return this.performAction('waitForSelector', selector, undefined, options);
}
async performAction(action, selector, value, options) {
const attempts = this.healingAttempts.get(selector) || 0;
try {
// First try the original selector
return await this.executeAction(action, selector, value, options);
}
catch (error) {
// If original selector fails, try to heal
if (this.isSelectorError(error) && attempts < MAX_HEALING_ATTEMPTS) {
console.log(`Selector failed: ${selector}, attempting to heal...`);
this.healingAttempts.set(selector, attempts + 1);
const newHealedSelector = await this.healSelector(selector, { action, value, options });
if (newHealedSelector) {
this.selectorMap[selector] = newHealedSelector;
this.healingAttempts.delete(selector); // Reset attempts on successful healing
this.saveHealedSelectors();
console.log(`Successfully healed selector: ${selector} -> ${newHealedSelector}`);
return await this.executeAction(action, newHealedSelector, value, options);
}
else {
throw new Error(`Failed to heal selector after ${attempts + 1} attempts: ${selector}`);
}
}
else {
throw error;
}
}
}
async executeAction(action, selector, value, options) {
switch (action) {
case 'click':
return await this.page.click(selector, options);
case 'fill':
return await this.page.fill(selector, value, options);
case 'check':
return await this.page.check(selector, options);
case 'uncheck':
return await this.page.uncheck(selector, options);
case 'hover':
return await this.page.hover(selector, options);
case 'type':
return await this.page.type(selector, value, options);
case 'selectOption':
return await this.page.selectOption(selector, value, options);
case 'getText':
return await this.page.textContent(selector, options);
case 'getAttribute':
return await this.page.getAttribute(selector, value, options);
case 'isVisible':
return await this.page.isVisible(selector, options);
case 'waitForSelector':
return await this.page.waitForSelector(selector, options);
default:
throw new Error(`Unknown action: ${action}`);
}
}
isSelectorError(error) {
const errorMessage = error.message || '';
return errorMessage.includes('No node found') ||
errorMessage.includes('Timeout') ||
errorMessage.includes('Element not found') ||
errorMessage.includes('Selector not found') ||
errorMessage.includes('Element is not attached to DOM');
}
async healSelector(originalSelector, context) {
// Reorder strategies based on context
let strategies = [...this.healingStrategies];
if (context?.action) {
// For form actions, prioritize input elements
if (['fill', 'type', 'selectOption'].includes(context.action)) {
strategies = strategies.sort((a, b) => {
if (a.name === 'ID-based healing')
return -1;
if (b.name === 'ID-based healing')
return 1;
if (a.name === 'Attribute-based healing')
return -1;
if (b.name === 'Attribute-based healing')
return 1;
return a.priority - b.priority;
});
}
// For click actions, prioritize button and link elements
else if (['click', 'hover'].includes(context.action)) {
strategies = strategies.sort((a, b) => {
if (a.name === 'ID-based healing')
return -1;
if (b.name === 'ID-based healing')
return 1;
if (a.name === 'Text-based healing')
return -1;
if (b.name === 'Text-based healing')
return 1;
return a.priority - b.priority;
});
}
// For text retrieval, prioritize text elements
else if (['getText'].includes(context.action)) {
strategies = strategies.sort((a, b) => {
if (a.name === 'ID-based healing')
return -1;
if (b.name === 'ID-based healing')
return 1;
if (a.name === 'Text-based healing')
return -1;
if (b.name === 'Text-based healing')
return 1;
return a.priority - b.priority;
});
}
}
for (const strategy of strategies) {
try {
const healedSelector = await strategy.heal(this.page, originalSelector, context);
if (healedSelector) {
console.log(`Healed using strategy: ${strategy.name}`);
return healedSelector;
}
}
catch (error) {
console.log(`Strategy ${strategy.name} failed:`, error.message);
}
}
return null;
}
async healById(page, originalSelector, context) {
return await page.evaluate(({ selector, context }) => {
// Extract ID from original selector
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
if (!idMatch)
return null;
const originalId = idMatch[1];
const element = document.getElementById(originalId);
if (element)
return `#${element.id}`;
// Try to find elements with similar IDs
const allElements = Array.from(document.querySelectorAll('[id]'));
const candidates = [];
for (const el of allElements) {
const currentId = el.id;
let score = 0;
// Exact match
if (currentId === originalId) {
score = 100;
}
// Contains match
else if (currentId.includes(originalId) || originalId.includes(currentId)) {
score = 50;
}
// Similar match (more flexible)
else if (currentId.replace(/[-_]/g, '') === originalId.replace(/[-_]/g, '')) {
score = 30;
}
// Partial match (for cases like message -> new-message, or similar ID patterns)
else if (originalId.length >= 2 && (currentId.includes(originalId.substring(0, Math.max(2, originalId.length - 1))) ||
currentId.includes(originalId) ||
originalId.includes(currentId.substring(0, Math.max(2, currentId.length - 1))) ||
(currentId.length > originalId.length && currentId.includes(originalId)) ||
(originalId.length > currentId.length && originalId.includes(currentId)))) {
score = 20;
}
// Reverse partial match
else if (currentId.length >= 2 && originalId.includes(currentId.substring(0, Math.max(2, currentId.length - 1)))) {
score = 20;
}
if (score > 0) {
// Consider context for better matching
if (context?.action) {
if (['fill', 'type', 'selectOption'].includes(context.action) &&
(el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT')) {
score += 20;
}
else if (['click', 'hover'].includes(context.action) &&
(el.tagName === 'BUTTON' || el.tagName === 'A')) {
score += 20;
}
else if (['getText'].includes(context.action) &&
(el.tagName === 'DIV' || el.tagName === 'SPAN' || el.tagName === 'P')) {
score += 20;
}
}
candidates.push({ element: el, score });
}
}
// Try to find elements with similar names or data attributes
for (const el of allElements) {
const name = el.getAttribute('name');
const dataTestId = el.getAttribute('data-testid');
const dataId = el.getAttribute('data-id');
if ((name && name.includes(originalId)) ||
(dataTestId && dataTestId.includes(originalId)) ||
(dataId && dataId.includes(originalId))) {
candidates.push({ element: el, score: 25 });
}
}
// Sort by score and return the best match
candidates.sort((a, b) => b.score - a.score);
if (candidates.length > 0) {
const best = candidates[0];
return best.element.tagName.toLowerCase() + `#${best.element.id}`;
}
return null;
}, { selector: originalSelector, context });
}
async healByText(page, originalSelector, context) {
return await page.evaluate(({ selector, tags, context }) => {
// Extract text from original selector or use context
let targetText = '';
const textMatch = selector.match(/text=["']([^"']+)["']/);
if (textMatch) {
targetText = textMatch[1];
}
// If no text selector, try to find elements by their text content for getText actions
if (!targetText && context?.action === 'getText') {
// For getText actions, look for elements with text content
const allElements = Array.from(document.querySelectorAll('*'));
const candidates = [];
for (const el of allElements) {
const text = el.textContent?.trim() || '';
if (text.length > 0 && text.length < 100) { // Reasonable text length
let score = 1;
// Prioritize common text containers
if (el.tagName === 'DIV' || el.tagName === 'SPAN' || el.tagName === 'P') {
score = 10;
}
else if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3') {
score = 8;
}
else if (el.tagName === 'LABEL') {
score = 6;
}
// Skip body, html, and head elements
if (el.tagName === 'BODY' || el.tagName === 'HTML' || el.tagName === 'HEAD') {
continue;
}
// Skip invisible elements
const style = el.style;
if (style.display === 'none' || style.visibility === 'hidden') {
continue;
}
// Skip elements that are too generic (like body or container divs)
if (el.tagName === 'BODY' || (el.tagName === 'DIV' && !el.id && !el.className)) {
continue;
}
candidates.push({ element: el, score, text });
}
}
// Sort by score and return the first candidate
candidates.sort((a, b) => b.score - a.score);
if (candidates.length > 0) {
const best = candidates[0];
// Only heal if the score is reasonably high and the element has some specificity
if (best.score >= 3 && (best.element.id || best.element.className)) {
return best.element.tagName.toLowerCase() + (best.element.id ? `#${best.element.id}` : '') +
(best.element.className ? `.${best.element.className.split(' ').join('.')}` : '');
}
}
}
// Original text selector logic
if (!targetText)
return null;
const candidates = Array.from(document.querySelectorAll(tags.join(',')));
for (const el of candidates) {
const text = el.textContent?.trim() || '';
if (text.toLowerCase().includes(targetText.toLowerCase()) ||
targetText.toLowerCase().includes(text.toLowerCase())) {
return el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') +
(el.className ? `.${el.className.split(' ').join('.')}` : '');
}
}
return null;
}, { selector: originalSelector, tags: CANDIDATE_TAGS, context });
}
async healByAttributes(page, originalSelector) {
return await page.evaluate((selector) => {
// Extract attributes from original selector
const attrMatches = selector.match(/\[([^\]]+)\]/g);
if (!attrMatches)
return null;
const attributes = attrMatches.map(attr => {
const match = attr.match(/\[([^=]+)(?:="([^"]+)")?\]/);
return { name: match?.[1], value: match?.[2] };
}).filter(attr => attr.name);
if (attributes.length === 0)
return null;
const allElements = Array.from(document.querySelectorAll('*'));
for (const el of allElements) {
let matches = 0;
for (const attr of attributes) {
if (attr.value && attr.name) {
if (el.getAttribute(attr.name) === attr.value)
matches++;
}
else if (attr.name) {
if (el.hasAttribute(attr.name))
matches++;
}
}
if (matches === attributes.length) {
return el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') +
(el.className ? `.${el.className.split(' ').join('.')}` : '');
}
}
return null;
}, originalSelector);
}
async healByPosition(page, originalSelector, context) {
return await page.evaluate(({ selector, context }) => {
// Try to find elements by their position in the DOM
const allElements = Array.from(document.querySelectorAll('*'));
const candidates = [];
for (const el of allElements) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
// Prioritize interactive elements
let priority = 1;
if (context?.action === 'getText') {
// For getText actions, prioritize text containers
if (el.tagName === 'DIV' || el.tagName === 'SPAN' || el.tagName === 'P') {
priority = 10;
}
else if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3') {
priority = 8;
}
else if (el.tagName === 'LABEL') {
priority = 6;
}
else if (el.textContent?.trim()) {
priority = 5;
}
}
else {
// For other actions, prioritize interactive elements
if (el.tagName === 'BUTTON' || el.tagName === 'A' || el.tagName === 'INPUT') {
priority = 10;
}
else if (el.tagName === 'DIV' || el.tagName === 'SPAN') {
priority = 1;
}
}
// Skip body, html, and head elements
if (el.tagName === 'BODY' || el.tagName === 'HTML' || el.tagName === 'HEAD') {
continue;
}
// Skip elements that are not visible or interactive
const style = el.style;
if (style.display === 'none' || style.visibility === 'hidden') {
continue;
}
// Skip elements that are not in the main content area
if (rect.top < 0 || rect.left < 0) {
continue;
}
// Consider context for better matching
if (context?.action) {
if (['fill', 'type', 'selectOption'].includes(context.action) &&
(el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT')) {
priority += 20;
// Additional context for input types - prioritize by input type relevance
if (el.tagName === 'INPUT') {
const inputType = el.type;
// Prioritize inputs that are not already used by other elements
const allInputs = Array.from(document.querySelectorAll('input'));
const visibleInputs = allInputs.filter(input => {
const inputRect = input.getBoundingClientRect();
return inputRect.width > 0 && inputRect.height > 0;
});
// Count how many inputs of this type we've seen
const inputsOfSameType = visibleInputs.filter(input => input.type === inputType);
// Give higher priority to less common input types
if (inputsOfSameType.length === 1) {
priority += 25; // Higher priority for unique input types
}
else if (inputsOfSameType.length === 2) {
priority += 10; // Medium priority for second occurrence
}
else {
priority -= 10; // Lower priority for common input types
}
// Additional priority based on input type diversity
// If we have multiple input types, prioritize the one that's less common
const allVisibleInputs = Array.from(document.querySelectorAll('input')).filter(input => {
const rect = input.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
const inputTypeCounts = {};
allVisibleInputs.forEach(input => {
const type = input.type;
inputTypeCounts[type] = (inputTypeCounts[type] || 0) + 1;
});
// If this input type is less common, give it higher priority
const currentTypeCount = inputTypeCounts[inputType] || 0;
if (currentTypeCount === 1) {
priority += 35; // Much higher priority for unique input types
}
else if (currentTypeCount === 2) {
priority += 15; // Medium priority for second occurrence
}
}
}
else if (['click', 'hover'].includes(context.action) &&
(el.tagName === 'BUTTON' || el.tagName === 'A')) {
priority += 20;
}
else if (['getText'].includes(context.action) &&
(el.tagName === 'DIV' || el.tagName === 'SPAN' || el.tagName === 'P')) {
priority += 20;
}
}
candidates.push({
element: el,
area: rect.width * rect.height,
tagName: el.tagName.toLowerCase(),
priority
});
}
}
// Sort by priority first, then by area
candidates.sort((a, b) => {
if (a.priority !== b.priority) {
return b.priority - a.priority;
}
return b.area - a.area;
});
if (candidates.length > 0) {
const best = candidates[0];
const classes = best.element.className ?
best.element.className.split(' ')
.filter(c => c.length > 0)
.filter(c => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(c)) // Only valid CSS class names
.slice(0, 3) // Limit to 3 classes
.join('.') : '';
return best.tagName + (best.element.id ? `#${best.element.id}` : '') +
(classes ? `.${classes}` : '');
}
return null;
}, { selector: originalSelector, context });
}
async healBySimilarity(page, originalSelector) {
return await page.evaluate(({ selector, tags, threshold }) => {
function getSelector(el) {
if (el.id)
return `#${el.id}`;
if (el.className) {
const classes = el.className.trim().split(/\s+/).filter(c => c.length > 0);
return classes.length > 0 ? `${el.tagName.toLowerCase()}.${classes.join('.')}` : el.tagName.toLowerCase();
}
return el.tagName.toLowerCase();
}
function getSimilarity(a, b) {
if (!a || !b)
return 0;
const la = a.toLowerCase();
const lb = b.toLowerCase();
if (la === lb)
return 1;
if (la.includes(lb) || lb.includes(la))
return 0.8;
// Simple similarity calculation
const longer = la.length > lb.length ? la : lb;
const shorter = la.length <= lb.length ? la : lb;
return (longer.includes(shorter)) ? (shorter.length / longer.length) : 0;
}
// Extract target text from selector
let targetText = '';
const textMatch = selector.match(/text=["']([^"']+)["']/);
if (textMatch) {
targetText = textMatch[1];
}
else {
// Try to extract from ID or class
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
if (idMatch) {
targetText = idMatch[1].replace(/[-_]/g, ' ');
}
}
// If no target text found, try to find elements with similar IDs
if (!targetText) {
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
if (idMatch) {
const originalId = idMatch[1];
const allElements = Array.from(document.querySelectorAll('[id]'));
for (const el of allElements) {
const currentId = el.id;
if (currentId.includes(originalId) || originalId.includes(currentId)) {
return el.tagName.toLowerCase() + `#${el.id}`;
}
}
}
}
const candidates = Array.from(document.querySelectorAll(tags.join(','))).map(el => {
const text = el.textContent?.trim() || '';
const title = el.getAttribute('title') || '';
const alt = el.getAttribute('alt') || '';
const ariaLabel = el.getAttribute('aria-label') || '';
const placeholder = el.getAttribute('placeholder') || '';
const name = el.getAttribute('name') || '';
const score = Math.max(getSimilarity(text, targetText), getSimilarity(title, targetText), getSimilarity(alt, targetText), getSimilarity(ariaLabel, targetText), getSimilarity(placeholder, targetText), getSimilarity(name, targetText));
return { el, score };
}).filter(c => c.score > threshold).sort((a, b) => b.score - a.score);
return candidates.length ? getSelector(candidates[0].el) : null;
}, { selector: originalSelector, tags: CANDIDATE_TAGS, threshold: SIMILARITY_THRESHOLD });
}
async healByXPath(page, originalSelector, context) {
return await page.evaluate(({ selector, context }) => {
// Check if the selector is already an XPath
const isXPath = selector.startsWith('//') || selector.startsWith('./') || selector.startsWith('/');
if (!isXPath) {
// If it's not an XPath, try to convert CSS selector to XPath
return null;
}
function evaluateXPath(xpath) {
try {
const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
const elements = [];
for (let i = 0; i < result.snapshotLength; i++) {
const node = result.snapshotItem(i);
if (node && node.nodeType === Node.ELEMENT_NODE) {
elements.push(node);
}
}
return elements;
}
catch (error) {
return [];
}
}
function generateXPathVariations(originalXPath) {
const variations = [];
// Original XPath
variations.push(originalXPath);
// Extract components from XPath
const tagMatch = originalXPath.match(/\/([a-zA-Z][a-zA-Z0-9]*)/);
const textMatch = originalXPath.match(/text\(\)\s*=\s*["']([^"']+)["']/);
const containsTextMatch = originalXPath.match(/contains\(text\(\),\s*["']([^"']+)["']\)/);
const attributeMatch = originalXPath.match(/@([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*["']([^"']+)["']/);
const containsAttributeMatch = originalXPath.match(/contains\(@([a-zA-Z][a-zA-Z0-9-]*),\s*["']([^"']+)["']\)/);
const tag = tagMatch ? tagMatch[1] : '*';
const text = textMatch ? textMatch[1] : (containsTextMatch ? containsTextMatch[1] : '');
const attribute = attributeMatch ? attributeMatch[1] : (containsAttributeMatch ? containsAttributeMatch[1] : '');
const attributeValue = attributeMatch ? attributeMatch[2] : (containsAttributeMatch ? containsAttributeMatch[2] : '');
// Generate variations based on components
if (text) {
// Text-based variations
variations.push(`//${tag}[contains(text(), "${text}")]`);
variations.push(`//${tag}[text() = "${text}"]`);
variations.push(`//*[contains(text(), "${text}")]`);
variations.push(`//${tag}[contains(., "${text}")]`);
// More flexible text matching
variations.push(`//${tag}[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "${text.toLowerCase()}")]`);
variations.push(`//*[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "${text.toLowerCase()}")]`);
}
if (attribute && attributeValue) {
// Attribute-based variations
variations.push(`//${tag}[@${attribute} = "${attributeValue}"]`);
variations.push(`//${tag}[contains(@${attribute}, "${attributeValue}")]`);
variations.push(`//*[@${attribute} = "${attributeValue}"]`);
}
if (attribute) {
// Attribute existence variations
variations.push(`//${tag}[@${attribute}]`);
variations.push(`//*[@${attribute}]`);
}
// Position-based variations
variations.push(`//${tag}[1]`);
variations.push(`//${tag}[position() = 1]`);
// Parent-child variations
variations.push(`//*//${tag}`);
variations.push(`//${tag}[1]//*`);
// Context-aware variations based on action
if (context?.action) {
if (['fill', 'type', 'selectOption'].includes(context.action)) {
variations.push(`//input[contains(text(), "${text}")]`);
variations.push(`//textarea[contains(text(), "${text}")]`);
variations.push(`//select[contains(text(), "${text}")]`);
}
else if (['click', 'hover'].includes(context.action)) {
variations.push(`//button[contains(text(), "${text}")]`);
variations.push(`//a[contains(text(), "${text}")]`);
variations.push(`//input[@type="button" and contains(@value, "${text}")]`);
// For click actions, also try to find by ID or class
if (tag === 'a') {
variations.push(`//a[@id]`);
variations.push(`//a[@class]`);
variations.push(`//a[@href]`);
}
else if (tag === 'button') {
variations.push(`//button[@id]`);
variations.push(`//button[@class]`);
variations.push(`//button[@type]`);
}
}
else if (['getText'].includes(context.action)) {
variations.push(`//div[contains(text(), "${text}")]`);
variations.push(`//span[contains(text(), "${text}")]`);
variations.push(`//p[contains(text(), "${text}")]`);
variations.push(`//h1[contains(text(), "${text}")]`);
variations.push(`//h2[contains(text(), "${text}")]`);
variations.push(`//h3[contains(text(), "${text}")]`);
}
}
return variations;
}
function xpathToCSS(xpath) {
try {
const elements = evaluateXPath(xpath);
if (elements.length > 0) {
const element = elements[0];
if (element.id) {
return `#${element.id}`;
}
else if (element.className) {
const classes = element.className.trim().split(/\s+/)
.filter(c => c.length > 0)
.filter(c => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(c)) // Only valid CSS class names
.slice(0, 3); // Limit to 3 classes to avoid invalid selectors
return classes.length > 0 ? `${element.tagName.toLowerCase()}.${classes.join('.')}` : element.tagName.toLowerCase();
}
else {
return element.tagName.toLowerCase();
}
}
}
catch (error) {
// Ignore conversion errors
}
return '';
}
// Try original XPath first
let elements = evaluateXPath(selector);
if (elements.length > 0) {
return xpathToCSS(selector);
}
// Generate and try variations
const variations = generateXPathVariations(selector);
for (const variation of variations) {
elements = evaluateXPath(variation);
if (elements.length > 0) {
const cssSelector = xpathToCSS(variation);
if (cssSelector) {
return cssSelector;
}
}
}
// If no XPath variations work, try to find similar elements by text content
if (context?.action === 'getText') {
const allElements = Array.from(document.querySelectorAll('*'));
for (const el of allElements) {
const text = el.textContent?.trim() || '';
if (text.length > 0 && text.length < 100) {
// Check if this element might be what we're looking for
if (el.tagName === 'DIV' || el.tagName === 'SPAN' || el.tagName === 'P' ||
el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3') {
if (el.id) {
return `#${el.id}`;
}
else if (el.className) {
const classes = el.className.trim().split(/\s+/)
.filter((c) => c.length > 0)
.filter((c) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(c)) // Only valid CSS class names
.slice(0, 3); // Limit to 3 classes
return classes.length > 0 ? `${el.tagName.toLowerCase()}.${classes.join('.')}` : el.tagName.toLowerCase();
}
}
}
}
}
// Fallback: Try to find elements by tag and attributes for click actions
if (context?.action === 'click') {
const tagMatch = selector.match(/\/([a-zA-Z][a-zA-Z0-9]*)/);
const tag = tagMatch ? tagMatch[1] : 'a';
const allElements = Array.from(document.querySelectorAll(tag));
for (const el of allElements) {
if (el.id) {
return `#${el.id}`;
}
else if (el.className) {
const classes = el.className.trim().split(/\s+/)
.filter((c) => c.length > 0)
.filter((c) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(c)) // Only valid CSS class names
.slice(0, 3); // Limit to 3 classes
return classes.length > 0 ? `${el.tagName.toLowerCase()}.${classes.join('.')}` : el.tagName.toLowerCase();
}
}
}
return null;
}, { selector: originalSelector, context });
}
// Utility methods
async getHealedSelectors() {
return { ...this.selectorMap };
}
async clearHealedSelectors() {
this.selectorMap = {};
this.healingAttempts.clear();
this.saveHealedSelectors();
}
async clearHealingAttempts() {
this.healingAttempts.clear();
}
async exportHealedSelectors(filePath) {
const path = filePath || STORE_PATH;
fs_1.default.writeFileSync(path, JSON.stringify(this.selectorMap, null, 2));
}
async importHealedSelectors(filePath) {
const data = fs_1.default.readFileSync(filePath, 'utf-8');
this.selectorMap = { ...this.selectorMap, ...JSON.parse(data) };
this.saveHealedSelectors();
}
loadHealedSelectors() {
try {
if (fs_1.default.existsSync(STORE_PATH)) {
return JSON.parse(fs_1.default.readFileSync(STORE_PATH, 'utf-8'));
}
}
catch (error) {
console.warn('Failed to load healed selectors:', error.message);
}
return {};
}
saveHealedSelectors() {
try {
const dir = path_1.default.dirname(STORE_PATH);
if (!fs_1.default.existsSync(dir)) {
fs_1.default.mkdirSync(dir, { recursive: true });
}
fs_1.default.writeFileSync(STORE_PATH, JSON.stringify(this.selectorMap, null, 2));
}
catch (error) {
console.warn('Failed to save healed selectors:', error.message);
}
}
}
exports.HealingPage = HealingPage;
//# sourceMappingURL=healingPage.js.map