|
| 1 | +import { expect, type Page } from "@playwright/test"; |
| 2 | + |
| 3 | +// Layout checks shared by the Chromatic snapshots and any spec that wants to look at the page at |
| 4 | +// something other than the Desktop Chrome default the suite runs under. |
| 5 | + |
| 6 | +/** |
| 7 | + * The viewports a page is looked at across: the two device classes in both orientations, plus the |
| 8 | + * desktop width the suite started at (the Desktop Chrome default in playwright.shared.ts). |
| 9 | + * |
| 10 | + * The Chromatic run applies these per test rather than as Playwright projects, since Chromatic |
| 11 | + * keys an archive by the test's title alone: run as projects, each viewport would write over the |
| 12 | + * last one's manifest and only the project that ran last would reach Chromatic at all. Naming the |
| 13 | + * viewport in the test title keeps them apart. |
| 14 | + */ |
| 15 | +export const VIEWPORTS = [ |
| 16 | + { name: "desktop", width: 1280, height: 720 }, |
| 17 | + { name: "tablet portrait", width: 768, height: 1024 }, |
| 18 | + { name: "tablet landscape", width: 1024, height: 768 }, |
| 19 | + { name: "mobile portrait", width: 390, height: 844 }, |
| 20 | + { name: "mobile landscape", width: 844, height: 390 }, |
| 21 | +] as const; |
| 22 | + |
| 23 | +/** |
| 24 | + * Asserts that the page does not scroll sideways at the current viewport, and names the elements |
| 25 | + * that reach past its right edge when it does. This is the failure a narrow viewport hits first |
| 26 | + * (a row of controls that cannot wrap runs off the screen) and it is far easier to act on as a |
| 27 | + * named element than as a pixel diff in a snapshot. |
| 28 | + * |
| 29 | + * The right edge rather than the width: a control pushed out of a grid it no longer fits is not |
| 30 | + * itself wider than the screen, only hanging off the side of it, and it is the sideways scroll |
| 31 | + * that the visitor meets either way. |
| 32 | + */ |
| 33 | +export async function expectNoHorizontalOverflow(page: Page): Promise<void> { |
| 34 | + const { scrollWidth, clientWidth, offenders } = await page.evaluate(() => { |
| 35 | + const root = document.documentElement; |
| 36 | + const offenders: string[] = []; |
| 37 | + document.querySelectorAll("body *").forEach((el) => { |
| 38 | + // Shapes inside an inline <svg> are drawn in its own coordinate space and can report widths |
| 39 | + // in the thousands; the <svg> that holds them is what is checked. ownerSVGElement is null on |
| 40 | + // that outermost <svg> and set only on what is nested inside it. |
| 41 | + if ((el as SVGElement).ownerSVGElement) return; |
| 42 | + const { width, right } = el.getBoundingClientRect(); |
| 43 | + if (width > 0 && right > root.clientWidth + 1) { |
| 44 | + const id = el.id ? `#${el.id}` : ""; |
| 45 | + const names = typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean) : []; |
| 46 | + const classes = names.length ? `.${names.join(".")}` : ""; |
| 47 | + offenders.push( |
| 48 | + `${el.tagName.toLowerCase()}${id}${classes} (${Math.round(width)}px wide, ends at ${Math.round(right)}px)`, |
| 49 | + ); |
| 50 | + } |
| 51 | + }); |
| 52 | + return { scrollWidth: root.scrollWidth, clientWidth: root.clientWidth, offenders }; |
| 53 | + }); |
| 54 | + expect(offenders, `Elements past the right edge of the ${clientWidth}px viewport`).toEqual([]); |
| 55 | + expect(scrollWidth, `Page scrolls sideways at ${clientWidth}px`).toBeLessThanOrEqual(clientWidth + 1); |
| 56 | +} |
0 commit comments