UNPKG

auto-heal-utility

Version:

A Playwright utility for auto-healing broken locators.

193 lines (192 loc) 9.21 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import fs from "fs/promises"; import path from "path"; export default class AutoHeal { constructor(page, timeout = 60 * 1000, logFile = "logs/locator_failures.log") { this.priorityMapCache = null; this.lastLogFileSize = 0; this.page = page; this.timeout = timeout; this.logFile = logFile; this.ensureLogDirectoryExists(); // Ensure the log directory exists before logging } /** * Ensures that the directory for the log file exists. * If it doesn't exist, it creates the directory. */ ensureLogDirectoryExists() { return __awaiter(this, void 0, void 0, function* () { const logDir = path.dirname(this.logFile); try { yield fs.mkdir(logDir, { recursive: true }); } catch (error) { console.error(`Error creating log directory: ${logDir}`); } }); } /** * Rotates the log file if it exceeds the maximum size (5 MB). * Moves the current log file to a backup file and starts a new log file. */ rotateLogFile() { return __awaiter(this, void 0, void 0, function* () { try { const maxSize = 5 * 1024 * 1024; // 5 MB const stats = yield fs.stat(this.logFile); if (stats.size > maxSize) { const tempFile = `${this.logFile}.tmp`; yield fs.rename(this.logFile, tempFile); const archiveFile = `${this.logFile}.${Date.now()}.bak`; yield fs.rename(tempFile, archiveFile); } } catch (error) { if (error.code !== "ENOENT") { console.error(`Error rotating log file: ${error.message}`); throw new Error(`Failed to rotate log file: ${error.message}`); } } }); } /** * Logs locator failures to a file for future analysis. * @param details - Details about the failure. * Includes details such as the locator, reason for failure, metadata, and a timestamp. */ logFailure(details) { return __awaiter(this, void 0, void 0, function* () { yield this.rotateLogFile(); // Ensure log rotation before logging const logEntry = Object.assign(Object.assign({}, details), { timestamp: new Date().toISOString() }); yield fs.appendFile(this.logFile, JSON.stringify(logEntry) + "\n"); }); } /** * Analyzes the failure log to generate a dynamic priority map. * @returns A dynamically generated priority map. * The priority map is a sorted list of locators based on their failure statistics. * Uses a cache to avoid reprocessing the log file if it hasn't changed. */ analyzeLogs() { return __awaiter(this, void 0, void 0, function* () { try { const stats = yield fs.stat(this.logFile); if (this.priorityMapCache && this.lastLogFileSize === stats.size) { return this.priorityMapCache; // Return cached priority map if log file hasn't changed } const logs = yield fs.readFile(this.logFile, "utf-8"); const failureData = logs .split("\n") .filter(Boolean) // Remove empty lines .map((log) => JSON.parse(log)); // Parse each log entry as JSON const locatorStats = this.priorityMapCache ? this.rebuildLocatorStats(failureData) : {}; // Update locator statistics with new failure data failureData.forEach((entry) => { const { locator, reason } = entry; if (!locatorStats[locator]) { locatorStats[locator] = { totalFailures: 0, reasons: {} }; } locatorStats[locator].totalFailures += 1; locatorStats[locator].reasons[reason] = (locatorStats[locator].reasons[reason] || 0) + 1; }); // Sort locators by the number of failures (fewer failures = higher priority) const sortedLocators = Object.keys(locatorStats).sort((a, b) => { const failuresA = locatorStats[a].totalFailures; const failuresB = locatorStats[b].totalFailures; return failuresA - failuresB; }); this.priorityMapCache = sortedLocators; // Cache the priority map this.lastLogFileSize = stats.size; // Update the cached log file size return sortedLocators; } catch (error) { if (error.code !== "ENOENT") { console.error(`Error analyzing log file: ${error.message}`); } return []; } }); } /** * Rebuilds the locator statistics from the cached priority map and new failure data. * @param failureData - Array of failure log entries. * @returns A rebuilt locator statistics object. * Ensures that the statistics are consistent with the cached priority map. */ rebuildLocatorStats(failureData) { const locatorStats = {}; // Initialize locatorStats from the cached priority map if (this.priorityMapCache) { this.priorityMapCache.forEach((locator) => { locatorStats[locator] = { totalFailures: 0, reasons: {} }; }); } // Update locatorStats with new failure data failureData.forEach((entry) => { const { locator, reason } = entry; if (!locatorStats[locator]) { locatorStats[locator] = { totalFailures: 0, reasons: {} }; } locatorStats[locator].totalFailures += 1; locatorStats[locator].reasons[reason] = (locatorStats[locator].reasons[reason] || 0) + 1; }); return locatorStats; } /** * Auto-heals locators by analyzing logs, sorting them based on priority, * and identifying the first visible and enabled element. * If no ideal locator is found, falls back to the first locator in the array. * @param locators - Array of locators to process. * @param metadata - Metadata about the current test (feature, scenario, step). * @returns The first locator that is visible and enabled, or the first locator in the array. */ autoHeal(locators, metadata) { return __awaiter(this, void 0, void 0, function* () { const priorityMap = yield this.analyzeLogs(); // Generate or retrieve the priority map // Map locators to their string representations for comparison const locatorMap = new Map(); locators.forEach((locator) => { locatorMap.set(locator, locator.toString()); }); let sortedLocators = locators; // Sort locators based on the priority map if (priorityMap.length > 0) { sortedLocators = locators.sort((a, b) => { const priorityA = priorityMap.indexOf(locatorMap.get(a) || ""); const priorityB = priorityMap.indexOf(locatorMap.get(b) || ""); return priorityA - priorityB; }); } // Iterate through sorted locators to find the first visible and enabled element for (const locator of sortedLocators) { try { yield locator.waitFor({ state: "attached", timeout: this.timeout }); if ((yield locator.isVisible()) && (yield locator.isEnabled())) { return locator; // Return the first valid locator } } catch (error) { // Log the failure and continue to the next locator yield this.logFailure({ locator: locatorMap.get(locator) || "unknown-locator", reason: `${error.message} (Timeout: ${this.timeout}ms)`, metadata, }); } } // Fallback to the first locator if no ideal locator is found if (sortedLocators.length > 0) { return sortedLocators[0]; } throw new Error("No locators provided to auto-heal."); }); } }