Checkpoint web parity through Chromium input tasks
This commit is contained in:
93
tools/web/check-chromium-dpr-consistency.mjs
Normal file
93
tools/web/check-chromium-dpr-consistency.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
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-04B/chromium-dpr-report.json");
|
||||
const manifestPath = path.join(root, "tests/golden/M14-04B/manifest.json");
|
||||
const basicBlend = path.join(root, "tests/files/web/basic_scene.blend");
|
||||
assert.ok(fs.existsSync(path.join(distRoot, "index.html")), "production dist is missing");
|
||||
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 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 = [];
|
||||
let expectedCssBounds = "";
|
||||
for (const deviceScaleFactor of [1, 1.5, 2, 3]) {
|
||||
const context = await browser.newContext({ viewport: { width: 960, height: 640 }, deviceScaleFactor });
|
||||
const page = await context.newPage();
|
||||
await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" });
|
||||
await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 });
|
||||
await page.setInputFiles("[data-testid=blend-file-input]", basicBlend);
|
||||
await page.getByText("BasicCube", { exact: true }).waitFor({ state: "visible", timeout: 20_000 });
|
||||
const canvas = page.locator("canvas.viewport-canvas");
|
||||
const bounds = await canvas.boundingBox();
|
||||
assert.ok(bounds);
|
||||
await canvas.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
element.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 }));
|
||||
});
|
||||
await page.waitForFunction(() => Boolean(document.querySelector(".blender-app")?.getAttribute("data-selected-object-ids")), undefined, { timeout: 10_000 });
|
||||
const value = await canvas.evaluate((element) => ({
|
||||
dpr: window.devicePixelRatio,
|
||||
css: element.getAttribute("data-viewport-css-size"),
|
||||
backing: element.getAttribute("data-viewport-backing-size"),
|
||||
pixelRatio: element.getAttribute("data-viewport-pixel-ratio"),
|
||||
width: element.width,
|
||||
height: element.height,
|
||||
selected: document.querySelector(".blender-app")?.getAttribute("data-selected-object-ids") ?? "",
|
||||
cssBounds: `${Math.round(element.getBoundingClientRect().width)}x${Math.round(element.getBoundingClientRect().height)}`,
|
||||
}));
|
||||
if (!expectedCssBounds) expectedCssBounds = value.css;
|
||||
assert.equal(value.css, expectedCssBounds);
|
||||
const [cssWidth, cssHeight] = expectedCssBounds.split("x").map(Number);
|
||||
assert.equal(value.backing, `${Math.floor(cssWidth * Math.min(deviceScaleFactor, 2))}x${Math.floor(cssHeight * Math.min(deviceScaleFactor, 2))}`);
|
||||
assert.equal(value.pixelRatio, String(Math.min(deviceScaleFactor, 2)));
|
||||
assert.equal(value.width, Math.floor(cssWidth * Math.min(deviceScaleFactor, 2)));
|
||||
assert.equal(value.height, Math.floor(cssHeight * Math.min(deviceScaleFactor, 2)));
|
||||
assert.match(value.selected, /object:/);
|
||||
results.push({ deviceScaleFactor, ...value });
|
||||
await context.close();
|
||||
}
|
||||
assert.equal(new Set(results.map((item) => item.selected)).size, 1);
|
||||
const [cssWidth, cssHeight] = expectedCssBounds.split("x").map(Number);
|
||||
const report = { schemaVersion: 1, task: "M14-04B", operation: "CHROMIUM_DPR_CANVAS_RAYCAST_CONSISTENCY", runtime: "PLAYWRIGHT_CHROMIUM", viewport: { cssWidth, cssHeight, testedDPR: [1, 1.5, 2, 3], maxDPR: 2 }, results, execution: "DISABLED", nextTask: "M14-04C" };
|
||||
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_04B_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-04A/manifest.json"), checker: path.join(root, "tools/web/check-chromium-dpr-consistency.mjs"), protocol: path.join(root, "web/protocol/viewport-dpr.ts"), unit: path.join(root, "web/tests/unit/viewport-dpr.test.mjs"), viewport: path.join(root, "web/app/src/three-adapter/viewport.ts"), offscreen: path.join(root, "web/app/src/three-adapter/offscreen-viewport.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-04B", parentTask: "M14-04A", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_DPR_CANVAS_RAYCAST_CONSISTENCY", artifacts, nextTask: "M14-04C" }, 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-04B", parentTask: "M14-04A", nextTask: "M14-04C" });
|
||||
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path);
|
||||
process.stdout.write(`chromium-dpr-ok dpr=1,1.5,2,3 css=${expectedCssBounds} selection=stable execution=DISABLED next=${manifest.nextTask}\n`);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
Reference in New Issue
Block a user