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', () => {