Files
lobo d7810fbf94
Build, Test & Push Frontend / quality-check (push) Successful in 31s
Build, Test & Push Frontend / docker (push) Successful in 24s
fix(layout): fit names on phones without sideways scroll
Display and game names shrink to 0.875rem, then end with an ellipsis
(FitText + fit-text utility, CSS container units, full text in title).
Rows and buttons wrap at large fonts, header points chip can shrink.
Guard: scripts/check-overflow.js.
2026-10-03 12:18:31 +02:00

66 lines
4.1 KiB
JavaScript

/*
* Sideways scroll check for phones. Not part of the gate, run by hand on the local stack (see README, "Overflow check").
* A Playwright function, run with the Playwright MCP tool browser_run_code_unsafe (filename = this file) or
* `playwright-cli run-code`. Log in as an admin in that browser first. A logged-in browser redirects the logged-out
* pages, so check those in a second run with LOGGED_OUT = true in a clean browser (no session).
* A hit is: scrollWidth != clientWidth, an element sticking out on the left (never raises scrollWidth), or a
* redirect away from the page.
* Content clipped inside an overflow-hidden ancestor is not checked (same rule as scrollWidth).
*/
async (page) => {
const BASE = 'http://localhost:5173';
const WIDTHS = [360, 390, 412, 414];
const FONT_PERCENT = 100; // set 130 for the large system font run
const DARK = false; // set true for dark mode
const SKIN = ''; // e.g. 'pastel' to check a skin
const LOGGED_OUT = false; // true: only the logged-out pages, run in a browser without a session
const GAME_ID = 620; // any appid in the library
const PROFILE_NAME = 'Dev'; // display name of the logged-in user, the profile must be public
const PAGES = LOGGED_OUT ? ['/', '/login', '/register', '/reset-password', '/impressum', `/u/${encodeURIComponent(PROFILE_NAME)}`] : [
'/dashboard', '/library', `/library/${GAME_ID}`, '/wishlist', '/play-next', '/stats', '/import', '/points',
'/settings', '/whats-new', '/welcome', '/audit/games', '/audit/links', '/audit/log', '/admin/users',
'/admin/hints', '/admin/news', '/admin/broadcasts', `/u/${encodeURIComponent(PROFILE_NAME)}`,
];
const hits = [];
let checked = 0;
for (const width of WIDTHS) {
await page.setViewportSize({width, height: 800});
for (const path of PAGES) {
await page.goto(BASE + path); // no networkidle: some pages poll
await page.waitForTimeout(1500);
if (FONT_PERCENT !== 100) await page.addStyleTag({content: `html{font-size:${FONT_PERCENT}%}`});
await page.evaluate(([dark, skin]) => {
document.documentElement.classList.toggle('dark', dark);
if (skin) document.documentElement.dataset.skin = skin;
}, [DARK, SKIN]);
await page.waitForTimeout(300);
const result = await page.evaluate(() => {
const root = document.documentElement;
const describe = (element) => `${element.tagName.toLowerCase()}.${String(element.className).slice(0, 60)}`;
const all = [...document.querySelectorAll('body *')].map((element) => [element, element.getBoundingClientRect()]);
const right = all.filter(([, box]) => box.right > root.clientWidth).sort((a, b) => b[1].right - a[1].right);
// Clipped by an overflow:hidden/auto ancestor (an effect preview, a scroller): not visible, not a hit.
const clipped = (element) => {
for (let parent = element.parentElement; parent && parent !== document.body; parent = parent.parentElement) {
if (getComputedStyle(parent).overflowX !== 'visible') return true;
}
return false;
};
const left = all.filter(([element, box]) => box.width > 0 && box.left < 0 && !clipped(element));
if (root.scrollWidth === root.clientWidth && left.length === 0) return null;
return {
scrollWidth: root.scrollWidth, clientWidth: root.clientWidth,
widest: right.slice(0, 5).map(([element]) => describe(element)),
offLeft: left.slice(0, 5).map(([element]) => describe(element)),
};
});
checked++;
const landed = await page.evaluate(() => location.pathname);
if (decodeURIComponent(landed) !== decodeURIComponent(path)) hits.push({path, width, redirectedTo: landed});
if (result) hits.push({path, width, ...result});
}
}
return {checked, hits};
}