// TWENTYOZ L6 — actual Chromium scenarios for the standalone showcase. import puppeteer from "puppeteer-core"; import { createRequire } from "node:module"; import fs from "node:fs"; import http from "node:http"; import path from "node:path"; import url from "node:url"; import zlib from "node:zlib"; const require = createRequire(import.meta.url); const here = path.dirname(url.fileURLToPath(import.meta.url)); const root = path.resolve(here, ".."); const publicRoot = path.join(root, "public"); const targetRoot = path.join(publicRoot, "work", "twentyoz-corp"); const chrome = [ process.env.CHROME_PATH, "C:/Program Files/Google/Chrome/Application/chrome.exe", "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", ].find((candidate) => candidate && fs.existsSync(candidate)); if (!chrome) throw new Error("Chrome executable not found"); const MIME = new Map([ [".html", "text/html; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".webp", "image/webp"], [".png", "image/png"], [".woff2", "font/woff2"], [".glb", "model/gltf-binary"], [".txt", "text/plain; charset=utf-8"], ]); const server = http.createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url, "http://localhost").pathname); const candidate = path.resolve(publicRoot, `.${pathname}`); if (!candidate.startsWith(publicRoot + path.sep)) { response.writeHead(403).end("forbidden"); return; } const file = fs.existsSync(candidate) && fs.statSync(candidate).isDirectory() ? path.join(candidate, "index.html") : candidate; if (!fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404).end("not found"); return; } response.setHeader("Content-Type", MIME.get(path.extname(file).toLowerCase()) || "application/octet-stream"); response.setHeader("Cache-Control", "no-store"); fs.createReadStream(file).pipe(response); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); const baseUrl = `http://127.0.0.1:${address.port}/work/twentyoz-corp/index.html`; const catalogUrl = `http://127.0.0.1:${address.port}/work/twentyoz-corp/products.html`; let pass = 0; let fail = 0; const failures = []; const observations = []; const suites = new Map(); const assert = (condition, suite, name, detail = "") => { const current = suites.get(suite) || { pass: 0, fail: 0 }; if (condition) { pass += 1; current.pass += 1; } else { fail += 1; current.fail += 1; failures.push(`${suite} · ${name}${detail ? ` — ${detail}` : ""}`); } suites.set(suite, current); }; const assertEvery = (items, predicate, suite, name, describe = (item) => JSON.stringify(item)) => { for (const item of items) assert(predicate(item), suite, name, describe(item)); }; const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const browser = await puppeteer.launch({ executablePath: chrome, headless: "new", args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--force-device-scale-factor=1"], }); const errors = []; async function newPage() { const page = await browser.newPage(); page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`)); page.on("console", (message) => { if (message.type() === "error") { const location = message.location(); errors.push(`console: ${message.text()}${location.url ? ` @ ${location.url}` : ""}`); } }); page.on("response", (response) => { if (response.status() >= 400) errors.push(`response: ${response.status()} ${response.url()}`); }); await page.evaluateOnNewDocument(() => { window.__twentyozPerf = { cls: 0, lcp: 0 }; new PerformanceObserver((list) => { for (const entry of list.getEntries()) if (!entry.hadRecentInput) window.__twentyozPerf.cls += entry.value; }).observe({ type: "layout-shift", buffered: true }); new PerformanceObserver((list) => { const entries = list.getEntries(); if (entries.length) window.__twentyozPerf.lcp = entries.at(-1).startTime; }).observe({ type: "largest-contentful-paint", buffered: true }); }); return page; } async function goto(page, suffix = "") { await page.goto(baseUrl + suffix, { waitUntil: "networkidle0", timeout: 30000 }); await page.evaluate(() => document.fonts.ready); } try { // 1. Semantic structure, progressive fallback and responsive geometry. const page = await newPage(); for (const width of [320, 390, 768, 1440]) { await page.setViewport({ width, height: width < 500 ? 844 : 900, isMobile: width < 500, hasTouch: width < 500 }); await goto(page); const layout = await page.evaluate(() => { const visible = (element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0; }; const h1 = document.querySelector("h1"); const lineHeight = Number.parseFloat(getComputedStyle(h1).lineHeight); const navTops = [...document.querySelectorAll(".nav a")].map((link) => Math.round(link.getBoundingClientRect().top)); const navLinks = [...document.querySelectorAll(".nav a")].map((link) => { const rect = link.getBoundingClientRect(); const style = getComputedStyle(link); return { text: link.textContent.trim(), visible: style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0, height: Math.round(rect.height), insideDocument: rect.left >= 0 && rect.right <= document.documentElement.clientWidth && rect.top >= 0 && rect.bottom <= document.documentElement.scrollHeight, }; }); const headSiblings = [...document.querySelectorAll(".head-inner > *")].map((element) => { const rect = element.getBoundingClientRect(); return { name: element.className || element.tagName.toLowerCase(), visible: visible(element), left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, }; }); const headOverlaps = []; for (let index = 0; index < headSiblings.length; index += 1) { for (let other = index + 1; other < headSiblings.length; other += 1) { const first = headSiblings[index]; const second = headSiblings[other]; if (first.visible && second.visible && first.left < second.right && first.right > second.left && first.top < second.bottom && first.bottom > second.top) { headOverlaps.push(`${first.name}/${second.name}`); } } } return { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, h1Count: document.querySelectorAll("h1").length, h1Lines: Math.round(h1.getBoundingClientRect().height / lineHeight), navTopCount: new Set(navTops).size, navLinks, headOverlaps, checked: document.querySelector('input[name="field"]:checked')?.value, visiblePanels: [...document.querySelectorAll(".field-panel")].filter(visible).map((panel) => panel.dataset.panel), fieldTargets: [...document.querySelectorAll(".field-tabs label")].map((label) => { const rect = label.getBoundingClientRect(); return { width: Math.round(rect.width), height: Math.round(rect.height) }; }), brokenImages: [...document.images].filter((image) => image.complete && image.naturalWidth === 0).length, webgl: document.querySelector(".hero").dataset.webgl, fallbackOpacity: Number.parseFloat(getComputedStyle(document.querySelector(".core-poster")).opacity), panelWidth: Math.round(document.querySelector(".field-panels").getBoundingClientRect().width), tabsWidth: Math.round(document.querySelector(".field-tabs").getBoundingClientRect().width), }; }); const suite = `responsive ${width}`; assert(layout.scrollWidth <= layout.width, suite, "no horizontal document overflow", `${layout.scrollWidth}/${layout.width}`); assert(layout.h1Count === 1, suite, "one h1", String(layout.h1Count)); assert(layout.h1Lines <= 2, suite, "hero title at most two lines", String(layout.h1Lines)); observations.push(`${suite} · navigation rows observed: ${layout.navTopCount}`); assertEvery(layout.navLinks, (link) => link.visible, suite, "navigation link visible", (link) => link.text); assertEvery(layout.navLinks, (link) => link.height >= 24, suite, "navigation link height >=24", (link) => `${link.text} ${link.height}px`); assertEvery(layout.navLinks, (link) => link.insideDocument, suite, "navigation link stays inside document", (link) => link.text); assert(layout.headOverlaps.length === 0, suite, "header siblings do not overlap", layout.headOverlaps.join(",")); assert(layout.checked === "edu", suite, "default field is education", String(layout.checked)); assert(layout.visiblePanels.length === 1 && layout.visiblePanels[0] === "edu", suite, "one matching panel visible", layout.visiblePanels.join(",")); assertEvery(layout.fieldTargets, (target) => target.height >= 44, suite, "field target height >=44", (target) => `${target.width}x${target.height}`); assert(layout.brokenImages === 0, suite, "all images decode", String(layout.brokenImages)); assert(layout.webgl === "fallback" && layout.fallbackOpacity === 1, suite, "automation fallback is complete", `${layout.webgl}/${layout.fallbackOpacity}`); assert(Math.abs(layout.panelWidth - layout.tabsWidth) <= 1, suite, "field panel fills the full tab width", `${layout.panelWidth}/${layout.tabsWidth}`); } // 2. All field controls: mouse and native keyboard radio behavior. await page.setViewport({ width: 1440, height: 900 }); await goto(page); const fieldCases = [ ["field-edu", "edu", "VIVEN"], ["field-dc", "dc", "XD Hub"], ["field-gov", "gov", "EduBill"], ["field-plant", "plant", "ACS"], ]; for (const [id, value, proof] of fieldCases) { await page.click(`label[for="${id}"]`); const state = await page.evaluate((value) => { const panel = document.querySelector(`[data-panel="${value}"]`); return { checked: document.querySelector('input[name="field"]:checked')?.value, display: getComputedStyle(panel).display, text: panel.textContent, coreState: document.querySelector(".hero").dataset.coreState, }; }, value); assert(state.checked === value, "field interactions", `${value} radio checked`, state.checked); assert(state.display !== "none", "field interactions", `${value} panel visible`, state.display); assert(state.text.includes(proof), "field interactions", `${value} proof content`, state.text.slice(0, 80)); assert(state.coreState === value, "field interactions", `${value} core state synced`, state.coreState); } await page.focus("#field-edu"); await page.keyboard.press("ArrowRight"); assert(await page.$eval("#field-dc", (input) => input.checked), "field interactions", "arrow key changes native radio"); // 3. Archive disclosure and anchor navigation. assert(!(await page.$eval("#career", (details) => details.open)), "content controls", "career starts closed"); await page.click("#career summary"); assert(await page.$eval("#career", (details) => details.open), "content controls", "career opens by summary"); assert((await page.$$eval(".jobs li", (items) => items.length)) === 5, "content controls", "five closed job records remain available"); assert((await page.$$eval(".record-list > li", (items) => items.length)) === 7, "official content", "seven current official records remain available"); assert((await page.$$eval('.record-list a[href="https://twentyoz.kr/news/111"]', (items) => items.length)) === 1, "official content", "latest official award record links to its source"); await page.click('.nav a[href="#ledger"]'); await wait(120); assert((await page.evaluate(() => location.hash)) === "#ledger", "content controls", "work anchor updates URL"); assert((await page.evaluate(() => scrollY)) > 0, "content controls", "work anchor scrolls page"); // 4. Axe accessibility in light and dark fallback modes. const axePath = require.resolve("axe-core/axe.min.js"); for (const theme of ["light", "dark"]) { await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: theme }]); await goto(page); await page.addScriptTag({ path: axePath }); const axe = await page.evaluate(async () => axe.run(document, { resultTypes: ["violations"] })); const axeDetail = axe.violations .map((item) => `${item.id}: ${item.nodes.map((node) => node.target.join(" ")).join(" | ")}`) .join("; "); assert(axe.violations.length === 0, `axe ${theme}`, "zero WCAG violations", axeDetail); const themeProof = await page.evaluate(() => ({ background: getComputedStyle(document.body).backgroundColor, poster: document.querySelector(".core-poster img").currentSrc, })); assert(theme === "light" ? themeProof.background === "rgb(233, 231, 226)" : themeProof.background === "rgb(13, 15, 15)", `axe ${theme}`, "theme background role applied", themeProof.background); assert(theme === "light" ? themeProof.poster.includes("core-light.webp") : themeProof.poster.includes("core-dark.webp"), `axe ${theme}`, "matching poster selected", themeProof.poster); } await page.emulateMediaFeatures([]); // 5. Reduced motion and explicit WebGL failure stay complete. const reducedPage = await newPage(); await reducedPage.emulateMediaFeatures([{ name: "prefers-reduced-motion", value: "reduce" }]); await reducedPage.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true }); await goto(reducedPage, "?webgl=1"); const reduced = await reducedPage.evaluate(() => ({ webgl: document.querySelector(".hero").dataset.webgl, reason: document.querySelector(".hero").dataset.webglReason, canvasDisplay: getComputedStyle(document.querySelector("#coreCanvas")).display, posterOpacity: getComputedStyle(document.querySelector(".core-poster")).opacity, toggleHidden: document.querySelector("#motionToggle").hidden, })); assert(reduced.webgl === "fallback" && reduced.reason === "reduced-motion", "fallbacks", "reduced motion blocks WebGL", `${reduced.webgl}/${reduced.reason}`); assert(reduced.canvasDisplay === "none" && reduced.posterOpacity === "1", "fallbacks", "reduced motion shows static poster", `${reduced.canvasDisplay}/${reduced.posterOpacity}`); assert(reduced.toggleHidden, "fallbacks", "motion control hidden when no motion"); await reducedPage.close(); const failedGlPage = await newPage(); await failedGlPage.evaluateOnNewDocument(() => { const original = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function getContext(type, ...args) { if (type === "webgl2") return null; return original.call(this, type, ...args); }; }); await goto(failedGlPage, "?webgl=1"); const noGl = await failedGlPage.evaluate(() => ({ webgl: document.querySelector(".hero").dataset.webgl, reason: document.querySelector(".hero").dataset.webglReason, title: document.querySelector("h1").textContent.trim(), poster: getComputedStyle(document.querySelector(".core-poster")).opacity, })); assert(noGl.webgl === "fallback" && noGl.reason === "no-webgl2", "fallbacks", "WebGL2 creation failure falls back", `${noGl.webgl}/${noGl.reason}`); assert(noGl.title === "네 현장을 하나로" && noGl.poster === "1", "fallbacks", "DOM and poster survive WebGL failure", `${noGl.title}/${noGl.poster}`); await failedGlPage.close(); // 6. Explicit real WebGL path, state sync, pause/replay, budgets and context loss. const glPage = await newPage(); await glPage.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true }); await goto(glPage, "?webgl=1"); await glPage.waitForSelector('.hero[data-webgl="ready"]', { timeout: 45000 }); await glPage.waitForSelector('.hero[data-model="ready"]', { timeout: 45000 }); const glReady = await glPage.evaluate(() => ({ canvasOpacity: getComputedStyle(document.querySelector("#coreCanvas")).opacity, posterOpacity: getComputedStyle(document.querySelector(".core-poster")).opacity, drawCalls: Number(document.querySelector("#coreCanvas").dataset.drawCalls), dpr: Number(document.querySelector("#coreCanvas").dataset.dpr), toggleHidden: document.querySelector("#motionToggle").hidden, paused: document.querySelector("#motionToggle").getAttribute("aria-pressed"), model: document.querySelector("#coreCanvas").dataset.model, effects: document.querySelector("#coreCanvas").dataset.effects, glowRings: document.querySelector("#coreCanvas").dataset.glowRings, })); assert(glReady.canvasOpacity === "1" && glReady.posterOpacity === "0", "WebGL", "canvas replaces poster after first frame", `${glReady.canvasOpacity}/${glReady.posterOpacity}`); assert(Number.isFinite(glReady.drawCalls) && glReady.drawCalls <= 60, "WebGL", "mobile draw calls <=60", String(glReady.drawCalls)); assert(Number.isFinite(glReady.dpr) && glReady.dpr <= 1.35, "WebGL", "mobile DPR <=1.35", String(glReady.dpr)); assert(!glReady.toggleHidden && glReady.paused === "true", "WebGL", "automation begins paused with control", `${glReady.toggleHidden}/${glReady.paused}`); assert(glReady.model === "ready", "WebGL", "licensed hard-surface GLB is rendered", glReady.model); assert(glReady.effects.includes("field-shader") && glReady.effects.includes("bloom") && glReady.effects.includes("grain"), "WebGL", "shader and post-processing stack is active", glReady.effects); assert(glReady.glowRings === "2", "WebGL", "two depth-separated glow rings are active", glReady.glowRings); const coreLayoutCases = [ { width: 390, height: 844, mobile: true, tier: "mobile", offset: 0, minRatio: 0.495, maxRatio: 0.505 }, { width: 768, height: 900, mobile: false, tier: "tablet", offset: 0.9 }, { width: 1024, height: 768, mobile: false, tier: "compact", offset: 0.75 }, { width: 1440, height: 900, mobile: false, tier: "wide", offset: 0.72 }, ]; for (const expected of coreLayoutCases) { await glPage.setViewport({ width: expected.width, height: expected.height, isMobile: expected.mobile, hasTouch: expected.mobile }); await glPage.waitForFunction( (tier) => document.querySelector("#coreCanvas")?.dataset.layoutTier === tier, { timeout: 10000 }, expected.tier, ); const placement = await glPage.$eval("#coreCanvas", (canvas) => { const rect = canvas.getBoundingClientRect(); const heading = document.querySelector(".hero h1").getBoundingClientRect(); const ratio = Number(canvas.dataset.coreScreenRatio); return { tier: canvas.dataset.layoutTier, offset: Number(canvas.dataset.coreOffsetX), ratio, centerX: rect.left + ratio * rect.width, canvasLeft: rect.left, canvasRight: rect.right, heroHeadingRight: heading.right, overflow: document.documentElement.scrollWidth - innerWidth, actionRows: new Set( [...document.querySelectorAll(".hero-actions > *")] .filter((element) => getComputedStyle(element).display !== "none") .map((element) => Math.round(element.getBoundingClientRect().top)), ).size, motionHeight: document.querySelector("#motionToggle").getBoundingClientRect().height, }; }); const suite = `WebGL layout ${expected.width}`; assert(placement.tier === expected.tier, suite, "responsive layout tier is selected", placement.tier); assert(Math.abs(placement.offset - expected.offset) <= 0.01, suite, "core offset matches composition", String(placement.offset)); assert(Number.isFinite(placement.ratio) && placement.ratio >= 0 && placement.ratio <= 1, suite, "core screen ratio stays inside canvas", String(placement.ratio)); assert(placement.centerX >= placement.canvasLeft && placement.centerX <= placement.canvasRight, suite, "core projection center stays inside canvas", `${placement.centerX}/${placement.canvasLeft}-${placement.canvasRight}`); if (expected.mobile) { assert(placement.ratio >= expected.minRatio && placement.ratio <= expected.maxRatio, suite, "mobile core screen position stays centered", String(placement.ratio)); } else { assert(placement.centerX > placement.heroHeadingRight, suite, "desktop core projection clears hero heading", `${placement.centerX}/${placement.heroHeadingRight}`); } assert(placement.overflow <= 0, suite, "visible 3D controls create no horizontal overflow", String(placement.overflow)); assert(placement.actionRows === 1, suite, "hero actions stay on one row", String(placement.actionRows)); assert(placement.motionHeight >= 44, suite, "motion control keeps a 44px target", String(placement.motionHeight)); } await glPage.click('label[for="field-plant"]'); await glPage.waitForFunction(() => document.querySelector("#coreCanvas").dataset.coreState === "plant"); assert(await glPage.$eval("#coreCanvas", (canvas) => canvas.dataset.coreState === "plant"), "WebGL", "field selection reaches 3D state"); assert(await glPage.$eval("#coreStatus", (status) => status.textContent === "선택한 현장: 공장과 설비 현장" && status.classList.contains("sr-only")), "WebGL", "field state remains explicit for assistive technology"); await glPage.click("#motionToggle"); assert((await glPage.$eval("#motionToggle", (button) => button.getAttribute("aria-pressed"))) === "false", "WebGL", "motion replay control works"); assert((await glPage.$eval("#motionToggle", (button) => button.textContent.trim())) === "3D 모션 끄기", "WebGL", "motion control names the available action"); await wait(180); await glPage.click("#motionToggle"); assert((await glPage.$eval("#motionToggle", (button) => button.getAttribute("aria-pressed"))) === "true", "WebGL", "motion pause control works"); await glPage.evaluate(() => { const gl = document.querySelector("#coreCanvas").getContext("webgl2"); gl.getExtension("WEBGL_lose_context").loseContext(); }); await glPage.waitForFunction(() => document.querySelector(".hero").dataset.webgl === "fallback", { timeout: 5000 }); const lost = await glPage.evaluate(() => ({ reason: document.querySelector(".hero").dataset.webglReason, poster: getComputedStyle(document.querySelector(".core-poster")).opacity, hidden: document.querySelector("#motionToggle").hidden, })); assert(lost.reason === "context-lost" && lost.poster === "1" && lost.hidden, "WebGL", "context loss restores complete fallback", JSON.stringify(lost)); await glPage.close(); // 7. Product catalog: all four detailed systems, responsive layout and accessibility. const catalogPage = await newPage(); for (const width of [320, 1440]) { await catalogPage.setViewport({ width, height: width < 500 ? 844 : 900, isMobile: width < 500, hasTouch: width < 500 }); await catalogPage.goto(catalogUrl, { waitUntil: "networkidle0", timeout: 30000 }); await catalogPage.evaluate(() => document.fonts.ready); const catalog = await catalogPage.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth, h1: document.querySelectorAll("h1").length, specimens: [...document.querySelectorAll(".specimen")].map((item) => item.id), capabilities: document.querySelectorAll(".capability-list li").length, steps: document.querySelectorAll(".operation-flow li").length, mailto: [...document.querySelectorAll('.specimen-action a[href^="mailto:"]')].length, text: document.body.textContent, })); const suite = `catalog ${width}`; assert(catalog.scrollWidth <= catalog.width, suite, "no horizontal overflow", `${catalog.scrollWidth}/${catalog.width}`); assert(catalog.h1 === 1, suite, "one h1", String(catalog.h1)); assert(catalog.specimens.join(",") === "viven,edubill,xd-hub,xd-works", suite, "four product details are present", catalog.specimens.join(",")); assert(catalog.capabilities === 16, suite, "sixteen verified capabilities", String(catalog.capabilities)); assert(catalog.steps === 16, suite, "sixteen operating steps", String(catalog.steps)); assert(catalog.mailto === 4, suite, "four product inquiry actions", String(catalog.mailto)); assert(catalog.text.includes("1,000,000개") && catalog.text.includes("단일 공간 최대 100명") && catalog.text.includes("다통화 · 다과금 정책"), suite, "published product specifications remain available"); } for (const theme of ["light", "dark"]) { await catalogPage.emulateMediaFeatures([{ name: "prefers-color-scheme", value: theme }]); await catalogPage.goto(catalogUrl, { waitUntil: "networkidle0", timeout: 30000 }); await catalogPage.addScriptTag({ path: axePath }); const axe = await catalogPage.evaluate(async () => axe.run(document, { resultTypes: ["violations"] })); const axeDetail = axe.violations.map((item) => `${item.id}: ${item.nodes.map((node) => node.target.join(" ")).join(" | ")}`).join("; "); assert(axe.violations.length === 0, `catalog axe ${theme}`, "zero WCAG violations", axeDetail); } await catalogPage.close(); // 8. Runtime and transfer budgets. await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: "light" }]); await page.setViewport({ width: 1440, height: 900 }); await goto(page); await wait(900); const perf = await page.evaluate(() => window.__twentyozPerf); assert(perf.cls <= 0.1, "performance", "CLS <=0.1", String(perf.cls)); assert(perf.lcp > 0 && perf.lcp <= 2500, "performance", "LCP <=2.5s", String(perf.lcp)); const gzip = (relative) => zlib.gzipSync(fs.readFileSync(path.join(targetRoot, relative)), { level: 9 }).byteLength; const initialAssets = ["index.html", "styles/main.css", "styles/tokens.css", "scripts/core-boot.js", "assets/core-light.webp", "assets/pretendard-twentyoz.woff2"]; const firstViewportBytes = initialAssets.reduce((sum, relative) => sum + fs.statSync(path.join(targetRoot, relative)).size, 0); const coreGzip = gzip("assets/core.iife.js"); const modelBytes = fs.statSync(path.join(targetRoot, "assets/twentyoz-field-core.glb")).size; const bootGzip = gzip("scripts/core-boot.js"); assert(firstViewportBytes <= 1024 * 1024, "performance", "first viewport assets <=1MB", `${firstViewportBytes}B`); assert(coreGzip <= 200 * 1024, "performance", "WebGL bundle <=200KB gzip", `${coreGzip}B`); assert(modelBytes <= 750 * 1024, "performance", "optimized GLB <=750KB", `${modelBytes}B`); assert(coreGzip + modelBytes <= 1024 * 1024, "performance", "lazy 3D payload <=1MB", `${coreGzip + modelBytes}B`); assert(bootGzip <= 35 * 1024, "performance", "hero bootstrap <=35KB gzip", `${bootGzip}B`); assert(errors.length === 0, "runtime", "zero console and page errors", errors.join(" | ")); await page.close(); } finally { await browser.close(); await new Promise((resolve) => server.close(resolve)); } const lines = [...suites.entries()].map(([name, result]) => `${result.fail ? "FAIL" : "PASS"} ${name} — ${result.pass} passed${result.fail ? ` · ${result.fail} failed` : ""}`); const stamp = new Date().toISOString().replace("T", " ").slice(0, 16); fs.writeFileSync( path.join(here, "twentyoz-e2e-report.md"), [`# TWENTYOZ E2E — ${stamp}`, "", fail ? `**${fail} failed · deploy blocked**` : `**${pass} assertions passed · GREEN**`, "", ...lines.map((line) => `- ${line}`), ...(observations.length ? ["", "## Observations", "", ...observations.map((item) => `- ${item}`)] : []), ...(failures.length ? ["", "## Failures", "", ...failures.map((item) => `- ${item}`)] : [])].join("\n") + "\n", ); console.log(lines.join("\n")); if (failures.length) { console.log("\nFailures:"); failures.forEach((failure) => console.log(`FAIL ${failure}`)); } console.log(`\nTWENTYOZ E2E — PASS ${pass} / FAIL ${fail}`); process.exit(fail ? 1 : 0);