← GPT-AI-LP
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));