Maybe clean up
This commit is contained in:
parent
2165ed0c33
commit
db8d4597df
40 changed files with 404 additions and 332 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue