auto-heal-utility
Version:
A Playwright utility for auto-healing broken locators.
193 lines (192 loc) • 9.21 kB
JavaScript
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.");
});
}
}