schmelczer-dev/scripts/check-overflow.mjs

125 lines
3.6 KiB
JavaScript

import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const dist = path.resolve('dist');
const routes = [
'/',
'/articles/',
'/articles/greatai-ai-deployment-api/',
'/writing/',
'/writing/greatai-ai-deployment-api/',
'/projects/',
'/about/',
];
const widths = [320, 390, 430];
function contentType(file) {
if (file.endsWith('.html')) return 'text/html; charset=utf-8';
if (file.endsWith('.css')) return 'text/css; charset=utf-8';
if (file.endsWith('.js')) return 'text/javascript; charset=utf-8';
if (file.endsWith('.svg')) return 'image/svg+xml';
if (file.endsWith('.png')) return 'image/png';
if (file.endsWith('.jpg') || file.endsWith('.jpeg')) return 'image/jpeg';
if (file.endsWith('.webp')) return 'image/webp';
if (file.endsWith('.woff2')) return 'font/woff2';
return 'application/octet-stream';
}
async function resolveFile(url) {
const parsed = new URL(url, 'http://localhost');
const safePath = path
.normalize(decodeURIComponent(parsed.pathname))
.replace(/^\/+/, '')
.replace(/^(\.\.(\/|\\|$))+/, '');
const candidate = path.join(dist, safePath);
const candidates = [
candidate,
path.join(candidate, 'index.html'),
path.join(dist, `${safePath}.html`),
];
for (const file of candidates) {
try {
const fileStat = await stat(file);
if (fileStat.isFile()) return file;
} catch {
// Try the next candidate.
}
}
return path.join(dist, '404.html');
}
const server = createServer(async (req, res) => {
try {
const file = await resolveFile(req.url ?? '/');
const body = await readFile(file);
res.writeHead(200, { 'content-type': contentType(file) });
res.end(body);
} catch (error) {
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
res.end(String(error));
}
});
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 < 3; 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);
if (attempt === 2 || !/Execution context was destroyed|navigation/i.test(message)) {
throw error;
}
await page.waitForLoadState('load').catch(() => {});
}
}
}
try {
for (const width of widths) {
const page = await browser.newPage({
viewport: { width, height: 900 },
javaScriptEnabled: false,
});
for (const route of routes) {
await page.goto(`http://127.0.0.1:${port}${route}`, { waitUntil: 'load' });
if (route.startsWith('/writing/')) {
await page
.waitForURL((url) => url.pathname.startsWith('/articles/'), { timeout: 1000 })
.catch(() => {});
}
const result = await measureViewport(page);
if (result.scrollWidth > result.clientWidth + 1) {
failures.push(
`${route} overflows at ${width}px: ${result.scrollWidth}px > ${result.clientWidth}px`
);
}
}
await page.close();
}
} finally {
await browser.close();
server.close();
}
if (failures.length > 0) {
console.error(failures.join('\n'));
process.exit(1);
}
console.log('No horizontal overflow detected at 320px, 390px, or 430px.');