UNPKG

healenium-js

Version:

Healenium-style self-healing decorator for Playwright in TypeScript

104 lines (87 loc) 3.67 kB
import { Page } from 'playwright'; import fs from 'fs'; import path from 'path'; import levenshtein from 'js-levenshtein'; type Step = () => Promise<void>; const fallbackFile = path.join(__dirname, 'fallback-selectors.json'); const historyFile = path.join(__dirname, 'selector-history.json'); function loadFallbacks(): Record<string, string> { if (fs.existsSync(fallbackFile)) { return JSON.parse(fs.readFileSync(fallbackFile, 'utf-8')); } return {}; } function saveFallback(original: string, healed: string) { const data = loadFallbacks(); data[original] = healed; fs.writeFileSync(fallbackFile, JSON.stringify(data, null, 2)); } function updateSelectorHistory(original: string, healed: string) { let history: Record<string, string[]> = {}; if (fs.existsSync(historyFile)) { history = JSON.parse(fs.readFileSync(historyFile, 'utf-8')); } if (!history[original]) { history[original] = []; } if (!history[original].includes(healed)) { history[original].push(healed); } fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); } async function findSimilarSelector(page: Page, failedSelector: string): Promise<string | null> { const failedText = await page.locator(failedSelector).textContent().catch(() => ''); if (!failedText) return null; const elements = await page.$$('body *'); let bestMatch: { selector: string, score: number } = { selector: '', score: Infinity }; for (const el of elements) { const text = await el.textContent(); if (!text) continue; const distance = levenshtein(failedText.trim(), text.trim()); if (distance < bestMatch.score && distance < 10) { const tag = await el.evaluate(el => el.tagName.toLowerCase()); bestMatch = { selector: `${tag}:has-text("${text.trim()}")`, score: distance }; } } if (bestMatch.selector) { saveFallback(failedSelector, bestMatch.selector); updateSelectorHistory(failedSelector, bestMatch.selector); return bestMatch.selector; } return null; } export function Healenium(failedSelector: string) { return function ( _target: Object, _propertyKey: string, descriptor: TypedPropertyDescriptor<Step> ): void { const originalMethod = descriptor.value!; descriptor.value = async function (...args: any[]) { const page: Page = (this as any).page || args.find(arg => typeof arg === 'object' && 'click' in arg); if (!page) throw new Error('No Page object found for healing logic'); try { await (originalMethod as any).apply(this, args); } catch (e: any) { console.warn('[Healenium] Attempting to heal from error:', e.message); const fallbacks = loadFallbacks(); if (fallbacks[failedSelector]) { console.info(`[Healenium] Retrying with healed selector: ${fallbacks[failedSelector]}`); await page.click(fallbacks[failedSelector]); updateSelectorHistory(failedSelector, fallbacks[failedSelector]); return; } const healedSelector = await findSimilarSelector(page, failedSelector); if (healedSelector) { console.info(`[Healenium] Found and stored healed selector: ${healedSelector}`); await page.click(healedSelector); } else { throw e; } } }; }; }