self-healing-playwright
Version:
Self-healing locators for Playwright tests using AI-powered healing strategies
467 lines (466 loc) ⢠19.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealingEngine = void 0;
const openai_1 = __importDefault(require("openai"));
const cache_1 = require("../storage/cache");
const cheerio = __importStar(require("cheerio"));
class HealingEngine {
constructor(config) {
this.config = config;
this.client = new openai_1.default({
apiKey: config.groqApiKey,
baseURL: "https://api.groq.com/openai/v1"
});
this.cache = new cache_1.LocatorCache();
}
async initialize() {
// No initialization needed for now
}
async find(page, options) {
const { original, fallback = true } = options;
try {
console.log('Trying original locator:', original);
// First try the original locator
const locator = page.locator(original);
await locator.waitFor({ state: 'visible', timeout: 5000 });
return locator;
}
catch (error) {
console.log('Original locator failed:', error);
if (!fallback) {
throw error;
}
// If original locator fails and fallback is enabled, try to heal it
const context = {
originalLocator: original,
testFile: 'unknown',
lineNumber: 0,
domSnapshot: await page.content(),
page,
error: error instanceof Error ? error : new Error(String(error))
};
console.log('Attempting to heal locator...');
const result = await this.healLocator(context);
console.log('Healing result:', result);
if (result.success) {
console.log('Using healed locator:', result.newLocator);
return page.locator(result.newLocator);
}
// If healing failed, try to find similar elements
console.log('Healing failed, trying to find similar elements...');
const similarElements = await this.findSimilarElements(context);
if (similarElements.length > 0) {
console.log('Found similar elements:', similarElements);
return page.locator(similarElements[0]);
}
throw new Error(`Failed to find element with locator: ${original}`);
}
}
async findSimilarElements(context) {
const $ = cheerio.load(context.domSnapshot);
const locators = [];
// Try to find elements with similar structure
const similarElements = $('nav, header, div[role="navigation"]');
similarElements.each((_, element) => {
const $element = $(element);
const text = $element.text().trim();
const role = $element.attr('role');
const dataTestId = $element.attr('data-testid');
const className = $element.attr('class');
if (text) {
locators.push(`text="${text}"`);
}
if (role) {
locators.push(`[role="${role}"]`);
}
if (dataTestId) {
locators.push(`[data-testid="${dataTestId}"]`);
}
if (className) {
locators.push(`.${className.split(' ')[0]}`);
}
});
// Try to find elements with similar text content
const allElements = $('*');
allElements.each((_, element) => {
const $element = $(element);
const text = $element.text().trim();
if (text && text.length > 0) {
locators.push(`text="${text}"`);
}
});
// Remove duplicates
return [...new Set(locators)];
}
async healLocator(context) {
console.log('\nš Starting locator healing process...');
console.log('Original locator:', context.originalLocator);
console.log('----------------------------------------');
// Check cache first
const cachedLocator = await this.cache.getCachedLocator(context.originalLocator, context);
if (cachedLocator) {
console.log('⨠Found cached locator:', cachedLocator);
const isValid = await this.validateLocator(context.page, cachedLocator);
if (isValid) {
return {
success: true,
newLocator: cachedLocator,
originalLocator: context.originalLocator,
context,
confidence: 1.0,
strategy: 'cache'
};
}
}
// Try different healing strategies
const strategies = [
this.generateTextBasedLocators.bind(this),
this.generateRoleBasedLocators.bind(this),
this.generateXPathLocators.bind(this),
this.generateCSSLocators.bind(this),
this.generateAILocators.bind(this)
];
// Collect all generated locators
const allLocators = [];
const strategyNames = ['Text-based', 'Role-based', 'XPath', 'CSS', 'AI'];
console.log('\nš Trying different healing strategies:');
console.log('========================================');
for (let i = 0; i < strategies.length; i++) {
const strategy = strategies[i];
const strategyName = strategyNames[i];
console.log(`\nš Strategy ${i + 1}: ${strategyName}`);
console.log('----------------------------------------');
const locators = await strategy(context);
console.log(`ā
Generated ${locators.length} locators:`);
if (locators.length > 0) {
locators.forEach((locator, index) => {
console.log(` ${index + 1}. ${locator}`);
});
}
else {
console.log(' No locators generated');
}
allLocators.push(...locators);
}
// Remove duplicates and log all unique locators
const uniqueLocators = [...new Set(allLocators)];
console.log('\nš All unique locators generated:');
console.log('========================================');
if (uniqueLocators.length > 0) {
uniqueLocators.forEach((locator, index) => {
console.log(`${index + 1}. ${locator}`);
});
}
else {
console.log('No locators were generated by any strategy');
}
console.log('\nš Validating locators:');
console.log('========================================');
// Try each unique locator
for (const locator of uniqueLocators) {
const isValid = await this.validateLocator(context.page, locator);
console.log(`${isValid ? 'ā
' : 'ā'} ${locator}`);
if (isValid) {
console.log('\n⨠Found a valid locator!');
// Cache the successful locator
await this.cache.setCachedLocator(context.originalLocator, context, locator);
return {
success: true,
newLocator: locator,
originalLocator: context.originalLocator,
context,
confidence: 0.8,
strategy: 'pattern'
};
}
}
console.log('\nā No valid locators found');
return {
success: false,
newLocator: context.originalLocator,
originalLocator: context.originalLocator,
context,
confidence: 0,
strategy: 'ai'
};
}
async generateTextBasedLocators(context) {
const $ = cheerio.load(context.domSnapshot);
const locators = [];
// Look for elements with visible text
$('*').each((_, el) => {
const $el = $(el);
const text = $el.text().trim();
if (text && text.length > 0) {
locators.push(`text="${text}"`);
locators.push(`text=${text}`);
// Also try partial text matches
if (text.length > 10) {
locators.push(`text*="${text.substring(0, 10)}"`);
}
}
});
return [...new Set(locators)];
}
async generateRoleBasedLocators(context) {
const $ = cheerio.load(context.domSnapshot);
const locators = [];
// Look for elements with roles
$('[role]').each((_, el) => {
var _a;
const $el = $(el);
const role = $el.attr('role');
if (role) {
locators.push(`[role="${role}"]`);
// Also try with tag name
const tagName = (_a = $el.prop('tagName')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
if (tagName) {
locators.push(`${tagName}[role="${role}"]`);
}
}
});
// Look for elements with ARIA attributes
$('[aria-label], [aria-labelledby]').each((_, el) => {
var _a;
const $el = $(el);
const ariaLabel = $el.attr('aria-label');
const ariaLabelledby = $el.attr('aria-labelledby');
if (ariaLabel) {
locators.push(`[aria-label="${ariaLabel}"]`);
const tagName = (_a = $el.prop('tagName')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
if (tagName) {
locators.push(`${tagName}[aria-label="${ariaLabel}"]`);
}
}
if (ariaLabelledby) {
locators.push(`[aria-labelledby="${ariaLabelledby}"]`);
}
});
return [...new Set(locators)];
}
async generateXPathLocators(context) {
const $ = cheerio.load(context.domSnapshot);
const locators = [];
// Look for elements with unique attributes
$('*').each((_, el) => {
var _a;
const $el = $(el);
const tagName = (_a = $el.prop('tagName')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
const id = $el.attr('id');
const dataTestId = $el.attr('data-testid');
const className = $el.attr('class');
if (tagName) {
if (id) {
locators.push(`//${tagName}[@id="${id}"]`);
}
if (dataTestId) {
locators.push(`//${tagName}[@data-testid="${dataTestId}"]`);
}
if (className) {
const classes = className.split(' ');
classes.forEach(cls => {
if (cls) {
locators.push(`//${tagName}[contains(@class, "${cls}")]`);
}
});
}
}
});
return [...new Set(locators)];
}
async generateCSSLocators(context) {
const $ = cheerio.load(context.domSnapshot);
const locators = [];
// Look for elements with unique attributes
$('*').each((_, el) => {
var _a;
const $el = $(el);
const tagName = (_a = $el.prop('tagName')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
const id = $el.attr('id');
const dataTestId = $el.attr('data-testid');
const className = $el.attr('class');
const type = $el.attr('type');
const name = $el.attr('name');
if (tagName) {
// Tag-based locators
locators.push(tagName);
// ID-based locators
if (id) {
locators.push(`#${id}`);
locators.push(`${tagName}#${id}`);
}
// Data-testid based locators
if (dataTestId) {
locators.push(`[data-testid="${dataTestId}"]`);
locators.push(`${tagName}[data-testid="${dataTestId}"]`);
}
// Class-based locators
if (className) {
const classes = className.split(' ');
classes.forEach(cls => {
if (cls) {
locators.push(`.${cls}`);
locators.push(`${tagName}.${cls}`);
}
});
}
// Type-based locators
if (type) {
locators.push(`${tagName}[type="${type}"]`);
locators.push(`[type="${type}"]`);
}
// Name-based locators
if (name) {
locators.push(`${tagName}[name="${name}"]`);
locators.push(`[name="${name}"]`);
}
}
});
return [...new Set(locators)];
}
async generateAILocators(context) {
const $ = cheerio.load(context.domSnapshot);
const locators = [];
// First, try to find elements with similar structure
const tagMatch = context.originalLocator.match(/^([a-zA-Z]+)/);
const originalTag = tagMatch ? tagMatch[1].toLowerCase() : null;
if (originalTag) {
// Look for elements with the same tag or similar tags
const similarElements = $(`${originalTag}, [class*="${originalTag}"], [id*="${originalTag}"]`);
similarElements.each((_, el) => {
var _a;
const $el = $(el);
const tagName = (_a = $el.prop('tagName')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
const id = $el.attr('id');
const dataTestId = $el.attr('data-testid');
const className = $el.attr('class');
const role = $el.attr('role');
const ariaLabel = $el.attr('aria-label');
if (tagName) {
// Basic tag locator
locators.push(tagName);
// ID-based locators
if (id) {
locators.push(`#${id}`);
locators.push(`${tagName}#${id}`);
}
// Data-testid based locators
if (dataTestId) {
locators.push(`[data-testid="${dataTestId}"]`);
locators.push(`${tagName}[data-testid="${dataTestId}"]`);
}
// Class-based locators
if (className) {
const classes = className.split(' ');
classes.forEach(cls => {
if (cls) {
locators.push(`.${cls}`);
locators.push(`${tagName}.${cls}`);
}
});
}
// Role-based locators
if (role) {
locators.push(`[role="${role}"]`);
locators.push(`${tagName}[role="${role}"]`);
}
// ARIA-based locators
if (ariaLabel) {
locators.push(`[aria-label="${ariaLabel}"]`);
locators.push(`${tagName}[aria-label="${ariaLabel}"]`);
}
}
});
}
// If we still don't have any locators, try to find any interactive elements
if (locators.length === 0) {
const interactiveElements = $('button, a, input, select, textarea, [role="button"], [role="link"]');
interactiveElements.each((_, el) => {
var _a;
const $el = $(el);
const tagName = (_a = $el.prop('tagName')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
const id = $el.attr('id');
const dataTestId = $el.attr('data-testid');
const className = $el.attr('class');
if (tagName) {
locators.push(tagName);
if (id)
locators.push(`#${id}`);
if (dataTestId)
locators.push(`[data-testid="${dataTestId}"]`);
if (className) {
const classes = className.split(' ');
classes.forEach(cls => {
if (cls)
locators.push(`.${cls}`);
});
}
}
});
}
return [...new Set(locators)];
}
async validateLocator(page, locator) {
try {
console.log('Validating locator:', locator);
const element = page.locator(locator);
const count = await element.count();
console.log('Element count:', count);
if (count === 0) {
return false;
}
const isVisible = await element.isVisible();
console.log('Is visible:', isVisible);
if (!isVisible) {
return false;
}
const isEnabled = await element.isEnabled();
console.log('Is enabled:', isEnabled);
return isEnabled;
}
catch (error) {
console.error('Error validating locator:', error);
return false;
}
}
async cleanup() {
await this.cache.clearCache();
}
}
exports.HealingEngine = HealingEngine;