Files
heros_web/scripts/verify-public-deploy.mjs

127 lines
5.1 KiB
JavaScript

import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { chromium } from 'playwright';
const targetUrl = withQaParams(process.env.PUBLIC_QA_URL ?? 'https://comtropy.synology.me/heros_web/');
const screenshotPath = process.env.PUBLIC_QA_SCREENSHOT ?? 'dist/public-deploy-qa.png';
const relevantLogPattern = /asset|audio|cannot|failed|map|missing|portrait|sprite|story|texture|unit/i;
mkdirSync(dirname(screenshotPath), { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const consoleMessages = [];
const pageErrors = [];
const requestFailures = [];
page.on('console', (message) => {
consoleMessages.push({ type: message.type(), text: message.text() });
});
page.on('pageerror', (error) => {
pageErrors.push(error.message);
});
page.on('requestfailed', (request) => {
requestFailures.push({
url: request.url(),
type: request.resourceType(),
failure: request.failure()?.errorText ?? 'unknown'
});
});
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForFunction(() => document.querySelector('canvas') !== null, undefined, { timeout: 90000 });
await page.waitForFunction(() => {
const canvas = document.querySelector('canvas');
return Boolean(canvas && canvas.width >= 960 && canvas.height >= 540);
}, undefined, { timeout: 90000 });
await page.waitForTimeout(3000);
await page.screenshot({ path: screenshotPath, fullPage: true });
const state = await page.evaluate(() => {
const canvas = document.querySelector('canvas');
return {
title: document.title,
canvasCount: document.querySelectorAll('canvas').length,
canvas: canvas
? {
width: canvas.width,
height: canvas.height,
clientWidth: canvas.clientWidth,
clientHeight: canvas.clientHeight,
pixelProbe: sampleCanvasPixels(canvas)
}
: undefined
};
function sampleCanvasPixels(canvas) {
const context = canvas.getContext('2d', { willReadFrequently: true });
if (!context) {
return { readable: false, sampleCount: 0, nonTransparentRatio: 0, distinctColorBuckets: 0 };
}
const stepX = Math.max(1, Math.floor(canvas.width / 64));
const stepY = Math.max(1, Math.floor(canvas.height / 64));
const colorBuckets = new Set();
let sampleCount = 0;
let nonTransparentCount = 0;
for (let y = Math.floor(stepY / 2); y < canvas.height; y += stepY) {
for (let x = Math.floor(stepX / 2); x < canvas.width; x += stepX) {
const [r, g, b, a] = context.getImageData(x, y, 1, 1).data;
sampleCount += 1;
if (a > 8) {
nonTransparentCount += 1;
}
colorBuckets.add(`${r >> 4},${g >> 4},${b >> 4},${a >> 5}`);
}
}
return {
readable: true,
sampleCount,
nonTransparentRatio: sampleCount > 0 ? nonTransparentCount / sampleCount : 0,
distinctColorBuckets: colorBuckets.size
};
}
});
assert(state.title?.trim().length > 0, `Expected deployed page title: ${JSON.stringify(state)}`);
assert(state.canvasCount === 1, `Expected exactly one game canvas: ${JSON.stringify(state)}`);
assert(state.canvas?.width >= 960 && state.canvas?.height >= 540, `Expected desktop-size game canvas: ${JSON.stringify(state)}`);
assert(state.canvas?.pixelProbe?.readable === true, `Expected readable canvas pixels: ${JSON.stringify(state)}`);
assert(state.canvas.pixelProbe.sampleCount >= 500, `Expected enough canvas pixel samples: ${JSON.stringify(state)}`);
assert(state.canvas.pixelProbe.nonTransparentRatio > 0.5, `Expected non-empty canvas pixels: ${JSON.stringify(state)}`);
assert(state.canvas.pixelProbe.distinctColorBuckets >= 12, `Expected visually varied canvas pixels: ${JSON.stringify(state)}`);
const relevantLogs = consoleMessages.filter(
(message) => message.type === 'error' || (isWarning(message.type) && relevantLogPattern.test(message.text))
);
const blockingRequestFailures = requestFailures.filter((request) => request.failure !== 'net::ERR_ABORTED');
assert(relevantLogs.length === 0, `Unexpected public deploy console issues: ${JSON.stringify(relevantLogs, null, 2)}`);
assert(pageErrors.length === 0, `Unexpected public deploy page errors: ${JSON.stringify(pageErrors, null, 2)}`);
assert(blockingRequestFailures.length === 0, `Unexpected public deploy request failures: ${JSON.stringify(blockingRequestFailures, null, 2)}`);
console.log(`Verified public deploy at ${targetUrl}`);
console.log(`Captured ${screenshotPath}`);
} finally {
await browser.close();
}
function withQaParams(url) {
const parsed = new URL(url);
parsed.searchParams.set('renderer', 'canvas');
parsed.searchParams.set('v', process.env.PUBLIC_QA_VERSION ?? String(Date.now()));
return parsed.toString();
}
function isWarning(type) {
return type === 'warning' || type === 'warn';
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}