import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import http from "node:http"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const distRoot = path.join(root, "web/dist"); const reportPath = path.join(root, "tests/golden/M14-04G/chromium-layout-report.json"); const manifestPath = path.join(root, "tests/golden/M14-04G/manifest.json"); const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); const viewports = [ { name: "desktop-1440x900", width: 1440, height: 900 }, { name: "desktop-1280x720", width: 1280, height: 720 }, { name: "tablet-834x1112", width: 834, height: 1112 }, { name: "phone-390x844", width: 390, height: 844 }, ]; const server = http.createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); const file = path.resolve(distRoot, relative); if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } response.statusCode = 200; response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); fs.createReadStream(file).pipe(response); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); assert.ok(address && typeof address === "object"); let browser; try { const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] }); const results = []; for (const backend of ["main", "offscreen"]) { for (const deviceScaleFactor of [1, 2]) { for (const viewport of viewports) { const context = await browser.newContext({ viewport, deviceScaleFactor, hasTouch: viewport.width < 900, isMobile: viewport.width < 600 }); const page = await context.newPage(); await page.goto(`http://127.0.0.1:${address.port}/?offscreen=${backend === "offscreen" ? "1" : "0"}`, { waitUntil: "load" }); await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 }); const measurement = 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 rect = (element) => { const value = element.getBoundingClientRect(); return { left: value.left, top: value.top, right: value.right, bottom: value.bottom, width: value.width, height: value.height }; }; const selectors = [".topbar", ".workspace-toolbar", ".storage-budget-panel", ".workspace-grid", ".viewport-area", ".timeline-area", ".status-bar"]; const bands = Object.fromEntries(selectors.map((selector) => { const element = document.querySelector(selector); return [selector, element && visible(element) ? rect(element) : null]; })); const bandViolations = []; for (const selector of [".topbar", ".workspace-toolbar", ".storage-budget-panel", ".status-bar"]) { const band = document.querySelector(selector); if (!band || !visible(band)) continue; const bandRect = band.getBoundingClientRect(); const horizontalConstrained = !["auto", "scroll", "hidden"].includes(getComputedStyle(band).overflowX); for (const control of band.querySelectorAll("button, input, select, output, span")) { if (!visible(control)) continue; const controlRect = control.getBoundingClientRect(); if (controlRect.top < bandRect.top - 1 || controlRect.bottom > bandRect.bottom + 1 || (horizontalConstrained && (controlRect.left < bandRect.left - 1 || controlRect.right > bandRect.right + 1))) { bandViolations.push(`${selector}:${control.textContent?.trim() || control.getAttribute("aria-label") || control.tagName}`); } } } const textOverflow = [...document.querySelectorAll(".topbar button, .topbar select, .workspace-toolbar button, .workspace-toolbar > span, .storage-budget-panel span, .storage-budget-panel output, .editor-header button")] .filter(visible) .filter((element) => element.scrollWidth > element.clientWidth + 1) .map((element) => element.textContent?.trim() || element.getAttribute("aria-label") || element.tagName); const root = document.documentElement; const app = document.querySelector(".blender-app"); return { bands, bandViolations, textOverflow, rootScrollWidth: root.scrollWidth, rootClientWidth: root.clientWidth, bodyScrollWidth: document.body.scrollWidth, viewportWidth: window.innerWidth, appRect: app ? rect(app) : null, rendererBackend: document.querySelector("canvas.viewport-canvas")?.dataset.rendererBackend ?? "MISSING", }; }); assert.equal(measurement.rendererBackend, backend === "offscreen" ? "offscreen-worker" : "webgl-pbr"); assert.deepEqual(measurement.bandViolations, [], JSON.stringify({ backend, deviceScaleFactor, viewport, measurement })); assert.deepEqual(measurement.textOverflow, [], JSON.stringify({ backend, deviceScaleFactor, viewport, measurement })); assert.ok(measurement.rootScrollWidth <= measurement.viewportWidth + 1, JSON.stringify({ backend, deviceScaleFactor, viewport, measurement })); assert.ok(measurement.bodyScrollWidth <= measurement.viewportWidth + 1, JSON.stringify({ backend, deviceScaleFactor, viewport, measurement })); assert.ok(measurement.appRect && measurement.appRect.right <= viewport.width + 1 && measurement.appRect.bottom <= viewport.height + 1, JSON.stringify({ backend, deviceScaleFactor, viewport, measurement })); results.push({ backend, deviceScaleFactor, viewport, measurement }); await context.close(); } } } const report = { schemaVersion: 1, task: "M14-04G", operation: "CHROMIUM_RESPONSIVE_LAYOUT_BOUNDARY", runtime: "PLAYWRIGHT_CHROMIUM", viewports, deviceScaleFactors: [1, 2], backends: ["main", "offscreen"], resultCount: results.length, results, execution: "DISABLED", nextTask: "M14-04H" }; const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); if (process.env.UPDATE_M14_04G_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-04F/manifest.json"), checker: path.join(root, "tools/web/check-chromium-layout.mjs"), app: path.join(root, "web/app/src/app/App.tsx"), css: path.join(root, "web/app/src/app/app-shell.css"), mainViewport: path.join(root, "web/app/src/three-adapter/viewport.ts"), offscreenViewport: path.join(root, "web/app/src/three-adapter/offscreen-viewport.ts"), referenceE2E: path.join(root, "web/tests/e2e/responsive-layout.spec.ts"), package: path.join(root, "web/package.json"), report: reportPath, }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04G", parentTask: "M14-04F", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_RESPONSIVE_LAYOUT_BOUNDARY", artifacts, nextTask: "M14-04H" }, null, 2)}\n`); } assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04G", parentTask: "M14-04F", nextTask: "M14-04H" }); for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); process.stdout.write(`chromium-layout-ok backends=main,offscreen viewports=4 dpr=1,2 results=${results.length} execution=DISABLED next=${manifest.nextTask}\n`); } finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); }