import { expect, type Page } from "@playwright/test"; export const narrowMobileViewport = { width: 390, height: 844 }; export const compactMobileViewport = { width: 320, height: 568 }; export async function expectNoHorizontalOverflow(page: Page): Promise { const metrics = await page.evaluate(() => { const root = document.documentElement; const body = document.body; const viewportWidth = window.innerWidth; const overflowingElements = Array.from(document.body.querySelectorAll("*")) .flatMap((element) => { const rect = element.getBoundingClientRect(); const overflowsViewport = rect.left < -1 || rect.right > viewportWidth + 1; const overflowsOwnBox = element.scrollWidth > element.clientWidth + 1 && rect.width > 0; if (!overflowsViewport && !overflowsOwnBox) return []; const selector = [ element.tagName.toLowerCase(), element.id ? `#${element.id}` : "", element.dataset.uiId ? `[data-ui-id="${element.dataset.uiId}"]` : "", element.className && typeof element.className === "string" ? `.${element.className.trim().split(/\s+/u).slice(0, 3).join(".")}` : "", ].join(""); return [ { clientWidth: element.clientWidth, left: Math.round(rect.left), right: Math.round(rect.right), scrollWidth: element.scrollWidth, selector, }, ]; }) .slice(0, 5); return { bodyScrollWidth: body.scrollWidth, clientWidth: root.clientWidth, rootScrollWidth: root.scrollWidth, overflowingElements, viewportWidth, }; }); const overflowDetails = metrics.overflowingElements.length > 0 ? `; overflowing elements: ${JSON.stringify(metrics.overflowingElements)}` : ""; expect( metrics.rootScrollWidth, `root scrollWidth ${metrics.rootScrollWidth} exceeds viewport ${metrics.viewportWidth}${overflowDetails}` ).toBeLessThanOrEqual(metrics.viewportWidth + 1); expect( metrics.bodyScrollWidth, `body scrollWidth ${metrics.bodyScrollWidth} exceeds viewport ${metrics.viewportWidth}${overflowDetails}` ).toBeLessThanOrEqual(metrics.viewportWidth + 1); expect(metrics.clientWidth).toBeLessThanOrEqual(metrics.viewportWidth); }