#301: dump raw box metrics from the reflow guard
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m33s
CI / Auth e2e pack (pull_request) Failing after 8m16s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Successful in 1m15s

Two rounds now reported no element past the viewport edge while the
document still claimed 417px of overflow — a combination that rules out
every hypothesis I had, including my own filter.

So stop inferring. The guard now prints the html/body metrics, every
element whose own content is wider than its box (with its overflow-x, so
the intentional scrollers are distinguishable), and every box reaching
past the edge with no filtering at all. Diagnostics ride in the assertion
message, not the compared value, so they show up even when they match.
This commit is contained in:
Claude Opus 5 2026-08-01 08:10:31 +02:00
parent 55932b0828
commit 9c87a14f51

View File

@ -120,7 +120,8 @@ async function expectNoHorizontalScroll(page: Page, label: string): Promise<void
const report = await page.evaluate(() => {
const doc = document.documentElement;
const limit = doc.clientWidth;
const describe = (el: HTMLElement): string => {
const describe = (el: Element): string => {
const cls =
el.className && typeof el.className === 'string'
? `.${el.className.trim().split(/\s+/).join('.')}`
@ -128,58 +129,41 @@ async function expectNoHorizontalScroll(page: Page, label: string): Promise<void
return `${el.tagName.toLowerCase()}${cls}`;
};
/**
* Content inside a scroll container may exceed the viewport that is the
* remedy, not the defect. But only if the CONTAINER itself fits: a
* scroller that is wider than the viewport still pushes the page, and its
* children are then symptoms, not causes.
*/
const containedByFittingScroller = (el: HTMLElement): boolean => {
for (let node = el.parentElement; node && node !== doc; node = node.parentElement) {
const overflowX = getComputedStyle(node).overflowX;
if (overflowX === 'auto' || overflowX === 'scroll' || overflowX === 'hidden') {
return node.getBoundingClientRect().right <= limit + 1;
}
}
return false;
};
const offenders: string[] = [];
let widest: HTMLElement | null = null;
for (const el of Array.from(document.querySelectorAll<HTMLElement>('body *'))) {
const rect = el.getBoundingClientRect();
// 1 px Toleranz gegen Subpixel-Rundung.
if (rect.width > 0 && rect.right > limit + 1 && !containedByFittingScroller(el)) {
offenders.push(
`${describe(el)} (right=${Math.round(rect.right)}, width=${Math.round(rect.width)})`,
// Every element whose own content is wider than its box. One of these is
// the source; the ones that scroll it away on purpose are marked.
const overflowing: string[] = [];
for (const el of Array.from(document.querySelectorAll('*'))) {
if (el.scrollWidth > el.clientWidth + 1 && el.clientWidth > 0) {
const overflowX = getComputedStyle(el).overflowX;
overflowing.push(
`${describe(el)} client=${el.clientWidth} scroll=${el.scrollWidth} overflow-x=${overflowX}`,
);
if (!widest || rect.width > widest.getBoundingClientRect().width) widest = el;
}
}
// Where does the inflation start? The chain from body down to the widest
// offender, with each box's width — the first entry wider than the
// viewport is the element that actually needs constraining.
const chain: string[] = [];
for (let node: HTMLElement | null = widest; node && node !== doc; node = node.parentElement) {
chain.unshift(`${describe(node)} w=${Math.round(node.getBoundingClientRect().width)}`);
// Raw: every box reaching past the viewport, no filtering at all.
const past: string[] = [];
for (const el of Array.from(document.querySelectorAll('body *'))) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.right > limit + 1) {
past.push(`${describe(el)} right=${Math.round(rect.right)} w=${Math.round(rect.width)}`);
}
}
return {
scrollWidth: doc.scrollWidth,
clientWidth: limit,
offenders: offenders.slice(0, 20),
chain,
overflowBy: doc.scrollWidth - limit,
viewport: `html client=${limit} scroll=${doc.scrollWidth} | body client=${document.body.clientWidth} scroll=${document.body.scrollWidth} rect=${Math.round(document.body.getBoundingClientRect().width)}`,
overflowing: overflowing.slice(0, 15),
past: past.slice(0, 15),
};
});
expect(
{
overflowBy: report.scrollWidth - report.clientWidth,
offenders: report.offenders,
chain: report.chain,
},
const diagnosis = [
`${label}: horizontaler Überlauf bei 320 px`,
).toEqual({ overflowBy: 0, offenders: [], chain: [] });
report.viewport,
`eigener Inhaltsüberlauf: ${JSON.stringify(report.overflowing, null, 1)}`,
`Boxen über dem Rand: ${JSON.stringify(report.past, null, 1)}`,
].join('\n');
expect({ overflowBy: report.overflowBy }, diagnosis).toEqual({ overflowBy: 0 });
}
test.describe('reflow at 320px', () => {