import { expect, test } from "@playwright/test"; import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** * v1.4.19 A6 — Settings consistency snapshot at Pixel-5 (393 CSS px). * v1.4.27 — input height floor lifted from 36 px (`h-9`) to 40 px * (`h-10`) on `Input`, `Select` trigger, and the native `, and the formatted overlay that fronts the // sr-only native inside DateField / // DateTimeField. The touch-target sweep must measure those visible // affordances, not the hidden input, so exempt any input that is // visually hidden or collapsed to zero size. (inp) => !(inp.hidden || inp.height === 0 || inp.width === 0), ); expect(formInputs.length).toBeGreaterThan(0); for (const inp of formInputs) { expect.soft(inp.height, `${inp.tag}#${inp.id} (${inp.type})`).toBe(44); } }); test("/settings/account: action buttons do not overflow their cards", async ({ page, }) => { await page.goto("/settings/account", { waitUntil: "networkidle" }); await page.waitForLoadState("networkidle"); // The account page's action button(s) must live within (or above the // bottom of) the parent card. Since v1.18.1 the "restart onboarding" // tour button moved to Settings → Advanced, so account carries the // change-password button; the tour button is matched here too in case // it is present. On mobile the button stacks below the title — it's // allowed to extend the card's height, but not push past the right edge. const overflowCheck = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll("button")); const targets = buttons.filter((b) => { const t = (b.textContent || "").trim().toLowerCase(); return ( t.includes("change password") || t.includes("passwort ändern") || t.includes("restart onboarding") || t.includes("onboarding-tour") ); }); return targets.map((b) => { const card = b.closest(".bg-card"); const cardRect = card?.getBoundingClientRect(); const btnRect = b.getBoundingClientRect(); return { text: (b.textContent || "").trim(), btnRight: Math.round(btnRect.right), cardRight: cardRect ? Math.round(cardRect.right) : null, }; }); }); expect( overflowCheck.length, "expected to find the change-password action button on /settings/account", ).toBeGreaterThanOrEqual(1); for (const t of overflowCheck) { expect(t.cardRight, `card for "${t.text}"`).not.toBeNull(); // 1 px tolerance for sub-pixel rounding. expect .soft( t.btnRight, `"${t.text}" right=${t.btnRight} must stay within card right=${t.cardRight}`, ) .toBeLessThanOrEqual((t.cardRight ?? 0) + 1); } }); test("/settings/account: Sprache select shares one grid row with date-of-birth", async ({ page, }) => { await page.goto("/settings/account", { waitUntil: "networkidle" }); await page.waitForLoadState("networkidle"); // The v1.4.27 R1 settings audit pairs date-of-birth with language // in a single `grid sm:grid-cols-2` row so the profile form keeps // a uniform two-column rhythm and the language field no longer // sits alone at the bottom with a `sm:max-w-xs` clamp. const sharedGrid = await page.evaluate(() => { const lang = document.getElementById("language-select"); const dob = document.getElementById("dob"); if (!lang || !dob) return { found: false, sharedGrid: false }; const langGrid = lang.closest('[class*="grid"]'); const dobGrid = dob.closest('[class*="grid"]'); return { found: true, sharedGrid: langGrid !== null && langGrid === dobGrid, }; }); expect(sharedGrid.found, "language + dob fields must exist").toBe(true); expect(sharedGrid.sharedGrid, "language + dob must share a grid").toBe( true, ); }); test("/settings/dashboard: Compare-to trigger renders at 44 px", async ({ page, }) => { await page.goto("/settings/dashboard", { waitUntil: "networkidle" }); await page.waitForLoadState("networkidle"); const trigger = page.locator("#comparison-baseline"); await expect(trigger).toBeVisible(); const h = await trigger.evaluate((el) => el.getBoundingClientRect().height); expect(Math.round(h)).toBe(44); }); test("/settings/ai: every native select renders at 44 px", async ({ page, }) => { await page.goto("/settings/ai", { waitUntil: "networkidle" }); await page.waitForLoadState("networkidle"); const heights = await page .locator("section select") .evaluateAll((els) => els.map((el) => Math.round(el.getBoundingClientRect().height)), ); expect(heights.length).toBeGreaterThan(0); for (const h of heights) { expect.soft(h).toBe(44); } }); });