healenium-js
Version:
Healenium-style self-healing decorator for Playwright in TypeScript
104 lines (87 loc) • 3.67 kB
text/typescript
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;
}
}
};
};
}