UNPKG

self-healing-playwright

Version:

Self-healing locators for Playwright tests using AI-powered healing strategies

467 lines (466 loc) • 19.3 kB
"use strict"; 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;