import { expect, test } from "@playwright/test"; const viewports = [ { name: "desktop-1440x900", width: 1440, height: 900 }, { name: "desktop-1280x720", width: 1280, height: 720 }, { name: "mobile-narrow", width: 360, height: 640 }, { name: "mobile-extra-narrow", width: 320, height: 568 }, ] as const; test.describe("M7-14 responsive layout", () => { for (const viewport of viewports) { test(`${viewport.name} keeps controls inside their layout bands`, async ({ page }) => { await page.setViewportSize({ width: viewport.width, height: viewport.height }); await page.goto("/"); await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 }); const report = await page.evaluate(() => { const visible = (element: Element): element is HTMLElement => { const node = element as HTMLElement; const style = getComputedStyle(node); return style.display !== "none" && style.visibility !== "hidden" && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0; }; const bands = [".topbar", ".workspace-toolbar", ".storage-budget-panel", ".status-bar"] .map((selector) => document.querySelector(selector)) .filter((element): element is HTMLElement => Boolean(element) && visible(element)); const bandViolations = bands.flatMap((band) => { const bandRect = band.getBoundingClientRect(); const horizontalConstrained = !["auto", "scroll", "hidden"].includes(getComputedStyle(band).overflowX); return [...band.querySelectorAll("button, input, select, output, span")] .filter(visible) .filter((control) => { const rect = control.getBoundingClientRect(); return rect.top < bandRect.top - 1 || rect.bottom > bandRect.bottom + 1 || (horizontalConstrained && (rect.left < bandRect.left - 1 || rect.right > bandRect.right + 1)); }) .map((control) => `${band.className}:${control.textContent?.trim() || control.getAttribute("aria-label") || control.tagName}`); }); const textOverflow = [...document.querySelectorAll(".topbar button, .topbar select, .workspace-toolbar button, .workspace-toolbar > span, .storage-budget-panel span, .storage-budget-panel output, .editor-header button")] .filter(visible) .filter((element) => element.scrollWidth > element.clientWidth + 1) .map((element) => element.textContent?.trim() || element.getAttribute("aria-label") || element.tagName); const root = document.documentElement; return { bandViolations, textOverflow, rootScrollWidth: root.scrollWidth, viewportWidth: window.innerWidth }; }); expect(report.bandViolations, JSON.stringify(report)).toEqual([]); expect(report.textOverflow, JSON.stringify(report)).toEqual([]); expect(report.rootScrollWidth, JSON.stringify(report)).toBeLessThanOrEqual(report.viewportWidth); }); } });