197 lines
8.4 KiB
JavaScript
197 lines
8.4 KiB
JavaScript
import { chromium } from "playwright";
|
|
import fs from "node:fs";
|
|
|
|
const BASE = "http://localhost:5173";
|
|
const OUT = "screenshots";
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
|
|
const VIEWPORTS = [
|
|
{ name: "mobile", width: 375, height: 720 },
|
|
{ name: "tablet", width: 768, height: 1024 },
|
|
{ name: "desktop", width: 1280, height: 850 },
|
|
{ name: "wide", width: 1536, height: 900 },
|
|
];
|
|
|
|
const PAGES = [
|
|
{ name: "login", path: "/", login: false },
|
|
{ name: "dashboard", path: "/dashboard", login: true, role: "owner" },
|
|
{ name: "calendar-week", path: "/calendar", login: true, role: "owner", waitFor: ".fc" },
|
|
{ name: "employees", path: "/employees", login: true, role: "owner" },
|
|
{ name: "services", path: "/services", login: true, role: "owner" },
|
|
{ name: "clients", path: "/clients", login: true, role: "owner" },
|
|
{ name: "tickets", path: "/tickets", login: true, role: "owner" },
|
|
{ name: "cash", path: "/cash", login: true, role: "owner" },
|
|
{ name: "notifications", path: "/notifications", login: true, role: "owner" },
|
|
{ name: "settings", path: "/settings", login: true, role: "owner" },
|
|
{ name: "emp-calendar", path: "/calendar", login: true, role: "employee", waitFor: ".fc" },
|
|
{ name: "admin-overview", path: "/admin", login: true, role: "admin" },
|
|
{ name: "admin-businesses", path: "/admin/businesses", login: true, role: "admin" },
|
|
{ name: "public-booking", path: "/b/lumiere-estetica-spa", login: false },
|
|
];
|
|
|
|
const results = [];
|
|
const consoleErrors = [];
|
|
const pageErrors = [];
|
|
|
|
async function login(browser, viewport, role) {
|
|
const page = await browser.newPage({ viewport });
|
|
page.on("console", (m) => {
|
|
if (m.type() === "error") consoleErrors.push({ where: "login", text: m.text() });
|
|
});
|
|
page.on("pageerror", (e) => pageErrors.push({ where: "login", text: e.message }));
|
|
await page.goto(`${BASE}/`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(400);
|
|
const email =
|
|
role === "admin"
|
|
? "[email protected]"
|
|
: role === "owner"
|
|
? "[email protected]"
|
|
: "[email protected]";
|
|
// Fill the login form
|
|
await page.fill('input[type="email"]', email);
|
|
await page.fill('input[type="password"]', "demo1234");
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL(/\/(dashboard|calendar|admin)/, { timeout: 8000 });
|
|
await page.waitForTimeout(8000); // let queries resolve
|
|
// Save storage state
|
|
const state = await page.context().storageState();
|
|
await page.close();
|
|
return state;
|
|
}
|
|
|
|
async function measureOverflow(page) {
|
|
return page.evaluate(() => {
|
|
const docWidth = document.documentElement.clientWidth;
|
|
const bodyWidth = document.body.scrollWidth;
|
|
const htmlWidth = document.documentElement.scrollWidth;
|
|
const overflow = Math.max(bodyWidth, htmlWidth) - docWidth;
|
|
// find offending elements
|
|
const offenders = [];
|
|
document.querySelectorAll("*").forEach((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
if (r.right > docWidth + 2 && r.width > 50) {
|
|
offenders.push({
|
|
tag: el.tagName.toLowerCase(),
|
|
cls: (el.className || "").toString().slice(0, 60),
|
|
right: Math.round(r.right),
|
|
width: Math.round(r.width),
|
|
});
|
|
}
|
|
});
|
|
return { docWidth, scrollWidth: Math.max(bodyWidth, htmlWidth), overflow, offenders: offenders.slice(0, 5) };
|
|
});
|
|
}
|
|
|
|
async function run() {
|
|
const browser = await chromium.launch();
|
|
|
|
// Pre-login for owner, employee and admin to grab storage state
|
|
const ownerState = await login(browser, VIEWPORTS[2], "owner");
|
|
const empState = await login(browser, VIEWPORTS[2], "employee");
|
|
const adminState = await login(browser, VIEWPORTS[2], "admin");
|
|
|
|
for (const vp of VIEWPORTS) {
|
|
for (const p of PAGES) {
|
|
const tag = `${vp.name}/${p.name}`;
|
|
const state = p.role === "employee" ? empState : p.role === "admin" ? adminState : ownerState;
|
|
const context = await browser.newContext({ viewport: { width: vp.width, height: vp.height }, storageState: state });
|
|
const page = await context.newPage();
|
|
const localErrors = [];
|
|
page.on("console", (m) => {
|
|
if (m.type() === "error") {
|
|
localErrors.push(m.text());
|
|
consoleErrors.push({ where: tag, text: m.text() });
|
|
}
|
|
});
|
|
page.on("pageerror", (e) => {
|
|
localErrors.push("PAGEERROR: " + e.message);
|
|
pageErrors.push({ where: tag, text: e.message });
|
|
});
|
|
try {
|
|
await page.goto(`${BASE}${p.path}`, { waitUntil: "networkidle", timeout: 20000 });
|
|
if (p.waitFor) await page.waitForSelector(p.waitFor, { timeout: 15000 });
|
|
await page.waitForTimeout(2500);
|
|
const overflow = await measureOverflow(page);
|
|
const file = `${OUT}/${p.name}--${vp.width}.png`;
|
|
await page.screenshot({ path: file, fullPage: true });
|
|
// also a viewport-only shot for above-the-fold view
|
|
await page.screenshot({ path: `${OUT}/${p.name}--${vp.width}-fold.png` });
|
|
results.push({ tag, vp: vp.width, status: "ok", overflow: overflow.overflow, offenders: overflow.offenders, errors: localErrors });
|
|
console.log(`✓ ${tag} (overflow=${overflow.overflow}px${overflow.offenders.length ? ", " + overflow.offenders.length + " offenders" : ""})`);
|
|
} catch (e) {
|
|
results.push({ tag, vp: vp.width, status: "fail", error: e.message, errors: localErrors });
|
|
console.log(`✗ ${tag} — ${e.message.split("\n")[0]}`);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Interaction test: open appointment modal on mobile + desktop
|
|
for (const vp of [VIEWPORTS[0], VIEWPORTS[2]]) {
|
|
const context = await browser.newContext({ viewport: { width: vp.width, height: vp.height }, storageState: ownerState });
|
|
const page = await context.newPage();
|
|
try {
|
|
await page.goto(`${BASE}/calendar`, { waitUntil: "networkidle" });
|
|
await page.waitForSelector(".fc", { timeout: 15000 });
|
|
await page.waitForTimeout(2000);
|
|
// open mobile menu on small screens (screenshot only, then reset)
|
|
if (vp.width < 1024) {
|
|
const menuBtn = await page.locator('button:has(svg.lucide-menu)').first();
|
|
if (await menuBtn.isVisible()) {
|
|
await menuBtn.click();
|
|
await page.waitForTimeout(600);
|
|
await page.screenshot({ path: `${OUT}/calendar-mobile-menu--${vp.width}.png` });
|
|
// reset state by reloading
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await page.waitForSelector(".fc", { timeout: 15000 });
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
}
|
|
// Click "Nueva cita"
|
|
const newBtn = page.getByRole("button", { name: /Nueva cita/ }).first();
|
|
if (await newBtn.isVisible()) {
|
|
await newBtn.click();
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: `${OUT}/appointment-modal--${vp.width}.png` });
|
|
// check modal scroll
|
|
const modalOverflow = await page.evaluate(() => {
|
|
const modal = document.querySelector('[class*="fixed inset-0"] [class*="overflow-y-auto"]');
|
|
return modal ? { scrollH: modal.scrollHeight, clientH: modal.clientHeight } : null;
|
|
});
|
|
results.push({ tag: `interaction/modal@${vp.width}`, status: "ok", modalOverflow });
|
|
}
|
|
} catch (e) {
|
|
console.log(`✗ interaction@${vp.width} — ${e.message.split("\n")[0]}`);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
fs.writeFileSync("screenshots/report.json", JSON.stringify({ results, consoleErrors, pageErrors }, null, 2));
|
|
|
|
// Summary
|
|
const failed = results.filter((r) => r.status === "fail");
|
|
const overflowIssues = results.filter((r) => r.overflow > 4);
|
|
const totalErrors = consoleErrors.length + pageErrors.length;
|
|
console.log("\n========== SUMMARY ==========");
|
|
console.log(`Screens/visits: ${results.length}`);
|
|
console.log(`Failed loads: ${failed.length}`);
|
|
console.log(`Horizontal overflow issues (>4px): ${overflowIssues.length}`);
|
|
console.log(` ${overflowIssues.map((o) => `${o.tag}(+${o.overflow}px)`).join(", ") || " none"}`);
|
|
console.log(`Console/page errors: ${totalErrors}`);
|
|
if (totalErrors) {
|
|
const uniq = [...new Set([...consoleErrors.map((e) => e.text), ...pageErrors.map((e) => e.text)])];
|
|
uniq.slice(0, 10).forEach((e) => console.log(` • ${e.slice(0, 180)}`));
|
|
}
|
|
console.log(`\nReport → screenshots/report.json`);
|
|
console.log(`Screenshots → screenshots/*.png`);
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|