CODE · 8.9 KB
scripts/browser-qa-real-e2e.mjs
Workspace snapshot · 08/31 23:51
import { mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const playwrightPath = process.env.CODEX_PLAYWRIGHT_PATH
|| "C:/Users/takum/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs";
const chromePath = process.env.CHROME_PATH
|| "C:/Program Files/Google/Chrome/Application/chrome.exe";
const { chromium } = await import(pathToFileURL(playwrightPath).href);
const require = createRequire(import.meta.url);
const sharp = require("C:/Users/takum/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/sharp");
const workspace = dirname(dirname(fileURLToPath(import.meta.url)));
const output = process.env.QA_OUTPUT
? join(workspace, process.env.QA_OUTPUT)
: join(workspace, "output", "real-e2e");
const evidence = join(output, "evidence");
const phase = process.argv[2] || "after";
const widths = [375, 390, 414, 1280];
const captureFullPageTiled = async (page, width) => {
const totalHeight = await page.evaluate(() => document.documentElement.scrollHeight);
const viewportHeight = page.viewportSize().height;
const tiles = [];
const seen = new Set();
for (let target = 0; target < totalHeight; target += viewportHeight) {
await page.evaluate((top) => window.scrollTo(0, top), target);
await page.waitForTimeout(60);
const top = await page.evaluate(() => Math.round(window.scrollY));
if (seen.has(top)) continue;
seen.add(top);
if (top > 0) await page.locator(".site-header").evaluate((header) => { header.style.visibility = "hidden"; });
tiles.push({ input: await page.screenshot(), left: 0, top });
}
const lastTop = Math.max(0, totalHeight - viewportHeight);
if (!seen.has(lastTop)) {
await page.evaluate((top) => window.scrollTo(0, top), lastTop);
await page.waitForTimeout(60);
await page.locator(".site-header").evaluate((header) => { header.style.visibility = "hidden"; });
tiles.push({ input: await page.screenshot(), left: 0, top: lastTop });
}
await page.locator(".site-header").evaluate((header) => { header.style.visibility = ""; });
await page.evaluate(() => window.scrollTo(0, 0));
return sharp({ create: { width, height: totalHeight, channels: 4, background: "#ffffff" } })
.composite(tiles)
.png()
.toBuffer();
};
await mkdir(join(evidence, phase, "sections"), { recursive: true });
const browser = await chromium.launch({ executablePath: chromePath, headless: true });
const results = [];
try {
for (const width of widths) {
const page = await browser.newPage({ viewport: { width, height: 900 }, deviceScaleFactor: 1 });
await page.goto(pathToFileURL(join(output, "index.html")).href, { waitUntil: "load" });
await page.evaluate(async () => {
await document.fonts.ready;
for (let y = 0; y < document.documentElement.scrollHeight; y += 700) {
window.scrollTo(0, y);
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}
await Promise.all([...document.images].map(async (image) => {
if (!image.complete) {
await Promise.race([
new Promise((resolve) => {
image.addEventListener("load", resolve, { once: true });
image.addEventListener("error", resolve, { once: true });
}),
new Promise((resolve) => setTimeout(resolve, 3000)),
]);
}
try { await Promise.race([image.decode(), new Promise((resolve) => setTimeout(resolve, 3000))]); } catch { /* naturalWidthで別途判定する */ }
}));
document.documentElement.style.scrollBehavior = "auto";
window.scrollTo(0, 0);
});
await page.waitForFunction(() => window.scrollY === 0);
await page.waitForTimeout(300);
const metrics = await page.evaluate(() => {
const root = document.documentElement;
const viewportWidth = window.innerWidth;
const sections = [...document.querySelectorAll("main section")].map((section, index) => {
const sectionRect = section.getBoundingClientRect();
const offenders = [...section.querySelectorAll("*")].flatMap((element) => {
if (element.closest(".integrated-copy-accessible,.section-background")) return [];
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
const scrollOwner = element.closest(".table-wrap,.story-rail,.lineup");
const intentionalScroll = scrollOwner && scrollOwner !== element
&& ["auto", "scroll"].includes(getComputedStyle(scrollOwner).overflowX);
const outside = !intentionalScroll && (rect.right > viewportWidth + 0.25 || rect.left < -0.25);
const clipped = ((element.scrollWidth > element.clientWidth + 1) || (element.scrollHeight > element.clientHeight + 1))
&& ["hidden", "clip"].includes(style.overflow);
return outside || clipped ? [{
tag: element.tagName.toLowerCase(),
className: String(element.className || ""),
outside,
clipped,
left: Number(rect.left.toFixed(1)),
right: Number(rect.right.toFixed(1)),
}] : [];
}).slice(0, 20);
const images = [...section.querySelectorAll("img")].map((image) => {
const rect = image.getBoundingClientRect();
return {
complete: image.complete,
naturalWidth: image.naturalWidth,
naturalHeight: image.naturalHeight,
width: Math.round(rect.width),
height: Math.round(rect.height),
objectFit: getComputedStyle(image).objectFit,
};
});
return {
index: index + 1,
id: section.id,
variant: [...section.classList].find((name) => name.startsWith("variant-"))?.slice(8) || "",
top: Math.round(sectionRect.top + window.scrollY),
height: Math.round(sectionRect.height),
offenders,
images,
};
});
const copy = document.querySelector(".variant-hero-form .hero-copy")?.getBoundingClientRect();
const form = document.querySelector(".variant-hero-form .hero-form-wrap")?.getBoundingClientRect();
const tableWrap = document.querySelector(".variant-comparison-table .table-wrap");
const table = tableWrap?.querySelector("table");
const mobileNav = document.querySelector(".mobile-nav");
return {
innerWidth: viewportWidth,
scrollWidth: root.scrollWidth,
scrollHeight: root.scrollHeight,
sections,
hero: {
copyTop: copy ? Math.round(copy.top + window.scrollY) : null,
formTop: form ? Math.round(form.top + window.scrollY) : null,
},
navigation: {
mobileDisplay: getComputedStyle(mobileNav).display,
desktopDisplay: getComputedStyle(document.querySelector(".site-nav")).display,
mobileLinks: mobileNav.querySelectorAll("a").length,
},
comparison: tableWrap ? {
containerWidth: tableWrap.clientWidth,
tableWidth: table.scrollWidth,
overflowX: getComputedStyle(tableWrap).overflowX,
} : null,
};
});
const fullPage = await captureFullPageTiled(page, width);
await writeFile(join(evidence, `full-${width}.png`), fullPage);
await writeFile(join(evidence, phase, `full-${width}.png`), fullPage);
const sectionDirectory = join(evidence, phase, "sections", String(width));
await mkdir(sectionDirectory, { recursive: true });
await page.locator(".site-header").evaluate((header) => { header.style.visibility = "hidden"; });
await page.locator(".skip").evaluate((skip) => { skip.style.visibility = "hidden"; });
const sectionLocators = page.locator("main section");
for (let index = 0; index < await sectionLocators.count(); index += 1) {
const section = sectionLocators.nth(index);
const id = await section.getAttribute("id");
await section.screenshot({ path: join(sectionDirectory, `${String(index + 1).padStart(2, "0")}-${id}.png`) });
}
await page.locator(".site-header").evaluate((header) => { header.style.visibility = ""; });
await page.locator(".skip").evaluate((skip) => { skip.style.visibility = ""; });
results.push({ width, ...metrics });
await page.close();
}
} finally {
await browser.close();
}
await writeFile(join(evidence, phase, "metrics-chromium.json"), JSON.stringify(results, null, 2), "utf8");
console.log(JSON.stringify(results.map((result) => ({
width: result.width,
innerWidth: result.innerWidth,
scrollWidth: result.scrollWidth,
hero: result.hero,
navigation: result.navigation,
comparison: result.comparison,
brokenImages: result.sections.flatMap((section) => section.images.filter((image) => !image.naturalWidth).map(() => section.id)),
offenderSections: result.sections.filter((section) => section.offenders.length).map((section) => section.id),
})), null, 2));