From 9c87a14f5181025c8dd0d264bc665b3c7672e50e Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Sat, 1 Aug 2026 08:10:31 +0200 Subject: [PATCH] #301: dump raw box metrics from the reflow guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/web/e2e/a11y.spec.ts | 70 +++++++++++++++------------------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/apps/web/e2e/a11y.spec.ts b/apps/web/e2e/a11y.spec.ts index 3a34089..3e3223d 100644 --- a/apps/web/e2e/a11y.spec.ts +++ b/apps/web/e2e/a11y.spec.ts @@ -120,7 +120,8 @@ async function expectNoHorizontalScroll(page: Page, label: string): Promise { 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 { - 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('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', () => {