Maybe clean up

This commit is contained in:
Andras Schmelczer 2026-05-25 09:49:09 +01:00
parent 2165ed0c33
commit db8d4597df
40 changed files with 404 additions and 332 deletions

View file

@ -5,10 +5,15 @@ import { chromium } from 'playwright';
const dist = path.resolve('dist');
const INDEX_FILE = 'index.html';
const MAX_NAV_RETRIES = 3;
const MAX_NAV_RETRIES = 4;
// Common device widths: iPhone SE / Galaxy S / iPhone 14 / iPad portrait /
// iPad landscape / common laptop / full HD desktop.
const VIEWPORT_WIDTHS = [320, 390, 430, 768, 1024, 1440, 1920];
const CLOSE_TIMEOUT_MS = 3000;
const LAUNCH_TIMEOUT_MS = 10000;
const CONTEXT_TIMEOUT_MS = 8000;
const PAGE_TIMEOUT_MS = 15000;
const MEASURE_TIMEOUT_MS = 25000;
const MIME = {
'.html': 'text/html; charset=utf-8',
@ -55,7 +60,7 @@ async function discoverRoutes() {
const rel = path.relative(dist, file).replaceAll(path.sep, '/');
if (rel === '404.html') continue;
// /writing/* are meta-refresh redirect stubs to /articles/*, not real
// pages measuring them would just remeasure /articles/.
// pages; measuring them would just remeasure /articles/.
if (rel.startsWith('writing/')) continue;
if (rel === INDEX_FILE) {
routes.add('/');
@ -115,50 +120,166 @@ const server = createServer(async (req, res) => {
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const { port } = server.address();
const browser = await chromium.launch({ headless: true });
const failures = [];
async function measureViewport(page) {
for (let attempt = 0; attempt < MAX_NAV_RETRIES; attempt += 1) {
try {
await page.waitForLoadState('load');
return await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
}));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const isLast = attempt === MAX_NAV_RETRIES - 1;
if (isLast || !/Execution context was destroyed|navigation/i.test(message)) {
throw error;
}
await page.waitForLoadState('load').catch(() => {});
function launchBrowser() {
return chromium.launch({
headless: true,
args: ['--disable-dev-shm-usage', '--disable-gpu', '--no-sandbox'],
});
}
async function withTimeout(promise, timeoutMs, label) {
let timeout;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timeout = setTimeout(() => reject(new Error(label)), timeoutMs);
}),
]);
} finally {
clearTimeout(timeout);
}
}
async function safeClosePage(page) {
await withTimeout(
page.close(),
CLOSE_TIMEOUT_MS,
'Timed out while closing Playwright page'
).catch(() => {});
}
async function safeCloseContext(context) {
await withTimeout(
context.close(),
CLOSE_TIMEOUT_MS,
'Timed out while closing Playwright context'
).catch(() => {});
}
async function safeCloseBrowser(browser) {
const childProcess = browser.process?.();
try {
await withTimeout(
browser.close(),
CLOSE_TIMEOUT_MS,
'Timed out while closing Chromium'
);
} catch {
childProcess?.kill('SIGKILL');
}
}
async function openBrowser() {
return withTimeout(
launchBrowser(),
LAUNCH_TIMEOUT_MS,
'Timed out while launching Chromium'
);
}
async function newMeasurementContext(browser, width) {
const context = await browser.newContext({
viewport: { width, height: 900 },
javaScriptEnabled: false,
});
await context.route('**/*', (route) => {
const type = route.request().resourceType();
if (['font', 'image', 'media'].includes(type)) {
route.abort('blockedbyclient');
} else {
route.continue();
}
});
return context;
}
async function openMeasurementContext(browser, width) {
return withTimeout(
newMeasurementContext(browser, width),
CONTEXT_TIMEOUT_MS,
`Timed out while creating ${width}px Playwright context`
);
}
async function measureViewport(page) {
await page.waitForLoadState('load', { timeout: 5000 }).catch(() => {});
return page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
}));
}
function shouldRetryNavigation(error) {
const message = error instanceof Error ? error.message : String(error);
return /ERR_INSUFFICIENT_RESOURCES|Execution context was destroyed|Target.*closed|has been closed|Timed out while|navigation/i.test(
message
);
}
async function measureRoute(context, route) {
let page;
try {
page = await withTimeout(
context.newPage(),
PAGE_TIMEOUT_MS,
`Timed out while creating page for ${route}`
);
return await withTimeout(
(async () => {
await page.goto(`http://127.0.0.1:${port}${route}`, {
waitUntil: 'domcontentloaded',
timeout: 15000,
});
return measureViewport(page);
})(),
MEASURE_TIMEOUT_MS,
`Timed out while measuring ${route}`
);
} finally {
if (page) await safeClosePage(page);
}
}
try {
for (const width of VIEWPORT_WIDTHS) {
const page = await browser.newPage({
viewport: { width, height: 900 },
javaScriptEnabled: false,
});
let browser;
let context;
try {
browser = await openBrowser();
context = await openMeasurementContext(browser, width);
for (const route of routes) {
let result;
for (const route of routes) {
await page.goto(`http://127.0.0.1:${port}${route}`, { waitUntil: 'load' });
const result = await measureViewport(page);
for (let attempt = 0; attempt < MAX_NAV_RETRIES; attempt += 1) {
try {
result = await measureRoute(context, route);
break;
} catch (error) {
if (!shouldRetryNavigation(error) || attempt === MAX_NAV_RETRIES - 1) {
throw error;
}
await safeCloseContext(context);
await safeCloseBrowser(browser);
browser = await openBrowser();
context = await openMeasurementContext(browser, width);
}
}
if (result.scrollWidth > result.clientWidth + 1) {
failures.push(
`${route} overflows at ${width}px: ${result.scrollWidth}px > ${result.clientWidth}px`
);
if (result.scrollWidth > result.clientWidth + 1) {
failures.push(
`${route} overflows at ${width}px: ${result.scrollWidth}px > ${result.clientWidth}px`
);
}
}
} finally {
if (context) await safeCloseContext(context);
if (browser) await safeCloseBrowser(browser);
}
await page.close();
}
} finally {
await browser.close();
server.close();
}