Files
hr-portal-v5-designs/visual-test-runner.js
T
2026-05-09 19:35:10 +00:00

316 lines
9.4 KiB
JavaScript

#!/usr/bin/env node
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const DEFAULT_VIEWPORTS = {
mobile: { width: 375, height: 812 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1440, height: 900 },
};
async function run() {
const manifestPath = path.resolve(process.cwd(), "test-manifest.json");
if (!fs.existsSync(manifestPath)) {
console.error("ERROR: test-manifest.json not found in", process.cwd());
process.exit(1);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
const baseUrl = manifest.baseUrl || "http://localhost:3000";
const viewports = manifest.viewports
? Object.fromEntries(
Object.entries(manifest.viewports).map(function (entry) {
return [entry[0], entry[1]];
})
)
: DEFAULT_VIEWPORTS;
const resultsDir = path.resolve(process.cwd(), "test-results");
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || "chromium";
var browser;
try {
browser = await puppeteer.launch({
headless: "new",
executablePath: executablePath,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
} catch (err) {
console.error("ERROR: Failed to launch browser:", err.message);
process.exit(1);
}
var results = { pages: [], passed: 0, failed: 0, errors: [] };
for (var pi = 0; pi < (manifest.pages || []).length; pi++) {
var pageDef = manifest.pages[pi];
var pageResult = {
id: pageDef.id,
url: baseUrl + (pageDef.path || "/"),
viewports: {},
passed: true,
};
var vpEntries = Object.entries(viewports);
for (var vi = 0; vi < vpEntries.length; vi++) {
var vpName = vpEntries[vi][0];
var vpSize = vpEntries[vi][1];
var vpResult = {
viewport: vpName,
screenshots: [],
actions: [],
consoleErrors: [],
passed: true,
};
var browserPage = null;
try {
browserPage = await browser.newPage();
await browserPage.setViewport(vpSize);
var consoleErrors = [];
browserPage.on("console", function (msg) {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
browserPage.on("pageerror", function (err) {
consoleErrors.push(err.message);
});
await browserPage.goto(pageResult.url, {
waitUntil: "networkidle2",
timeout: 30000,
});
if (pageDef.waitFor) {
await browserPage.waitForSelector(pageDef.waitFor, { timeout: 10000 });
}
var initialScreenshot = path.join(
resultsDir,
pageDef.id + "-" + vpName + "-initial.png"
);
await browserPage.screenshot({
path: initialScreenshot,
fullPage: true,
});
vpResult.screenshots.push(initialScreenshot);
var actions = pageDef.actions || [];
for (var ai = 0; ai < actions.length; ai++) {
var action = actions[ai];
var actionResult = { id: action.id, type: action.type, passed: true, error: null };
try {
await executeAction(browserPage, action);
var actionScreenshot = path.join(
resultsDir,
pageDef.id + "-" + vpName + "-" + action.id + ".png"
);
await browserPage.screenshot({
path: actionScreenshot,
fullPage: true,
});
vpResult.screenshots.push(actionScreenshot);
if (action.expectAfter) {
var valid = await validateExpectations(
browserPage,
action.expectAfter
);
if (!valid) {
actionResult.passed = false;
actionResult.error = "Expectation failed for " + action.id;
}
}
} catch (err) {
actionResult.passed = false;
actionResult.error = err.message;
}
if (!actionResult.passed) {
vpResult.passed = false;
}
vpResult.actions.push(actionResult);
}
vpResult.consoleErrors = consoleErrors;
if (consoleErrors.length > 0) {
vpResult.passed = false;
}
} catch (err) {
vpResult.passed = false;
vpResult.error = err.message;
results.errors.push({
page: pageDef.id,
viewport: vpName,
error: err.message,
});
} finally {
if (browserPage) {
await browserPage.close().catch(function () {});
}
}
if (!vpResult.passed) {
pageResult.passed = false;
}
pageResult.viewports[vpName] = vpResult;
}
if (pageResult.passed) {
results.passed++;
} else {
results.failed++;
}
results.pages.push(pageResult);
}
await browser.close();
var summaryPath = path.join(resultsDir, "summary.json");
fs.writeFileSync(summaryPath, JSON.stringify(results, null, 2));
console.log("");
console.log("=== Visual Test Results ===");
console.log("Pages tested: " + results.pages.length);
console.log("Passed: " + results.passed);
console.log("Failed: " + results.failed);
for (var ri = 0; ri < results.pages.length; ri++) {
var p = results.pages[ri];
var icon = p.passed ? "PASS" : "FAIL";
console.log(" [" + icon + "] " + p.id + " (" + p.url + ")");
var vpKeys = Object.keys(p.viewports);
for (var vk = 0; vk < vpKeys.length; vk++) {
var vr = p.viewports[vpKeys[vk]];
if (!vr.passed) {
console.log(" " + vpKeys[vk] + ": FAIL" + (vr.error ? " - " + vr.error : ""));
for (var ak = 0; ak < (vr.actions || []).length; ak++) {
var a = vr.actions[ak];
if (!a.passed) {
console.log(" action " + a.id + ": " + a.error);
}
}
if (vr.consoleErrors && vr.consoleErrors.length > 0) {
console.log(" console errors: " + vr.consoleErrors.length);
}
}
}
}
console.log("");
console.log("Summary written to: " + summaryPath);
process.exit(results.failed > 0 ? 1 : 0);
}
async function executeAction(page, action) {
switch (action.type) {
case "click":
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.click(action.selector);
break;
case "fill-form":
var fields = action.fields || [];
for (var fi = 0; fi < fields.length; fi++) {
var field = fields[fi];
await page.waitForSelector(field.selector, { timeout: 5000 });
await page.click(field.selector, { clickCount: 3 });
await page.type(field.selector, field.value);
}
break;
case "select":
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.select(action.selector, action.value);
break;
case "scroll":
if (action.selector) {
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.$eval(action.selector, function (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
});
} else {
await page.evaluate(
function (x, y) { window.scrollTo(x, y); },
action.x || 0,
action.y || 0
);
}
await new Promise(function (r) { setTimeout(r, 500); });
break;
case "hover":
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.hover(action.selector);
await new Promise(function (r) { setTimeout(r, 300); });
break;
case "wait":
if (action.selector) {
await page.waitForSelector(action.selector, {
timeout: action.timeout || 10000,
});
} else {
await new Promise(function (r) { setTimeout(r, action.duration || 1000); });
}
break;
default:
throw new Error("Unknown action type: " + action.type);
}
}
async function validateExpectations(page, expectations) {
var items = Array.isArray(expectations) ? expectations : [expectations];
for (var ei = 0; ei < items.length; ei++) {
var expect = items[ei];
if (expect.url) {
var currentUrl = page.url();
var pattern = new RegExp(expect.url);
if (!pattern.test(currentUrl)) {
return false;
}
}
if (expect.visible) {
var el = await page.$(expect.visible);
if (!el) return false;
var isVisible = await page.evaluate(function (s) {
var e = document.querySelector(s);
if (!e) return false;
var r = e.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}, expect.visible);
if (!isVisible) return false;
}
if (expect.hidden) {
var elH = await page.$(expect.hidden);
if (elH) {
var isVis = await page.evaluate(function (s) {
var e = document.querySelector(s);
if (!e) return false;
var r = e.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}, expect.hidden);
if (isVis) return false;
}
}
if (expect.text) {
var textContent = await page.$eval(
expect.text.selector,
function (el) { return el.textContent; }
);
if (!textContent || !textContent.includes(expect.text.contains)) {
return false;
}
}
}
return true;
}
run().catch(function (err) {
console.error("Fatal error:", err);
process.exit(1);
});