From eda317721c07795873889b19b026d16fc29fa055 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 05:58:50 +0100 Subject: [PATCH] test: find cut-off placeholders, and let the calendar's sideways strip of weeks scroll --- integration/tests/25-do-layout-fit.spec.ts | 7 ++++--- integration/tests/helpers/layout-fit.ts | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/integration/tests/25-do-layout-fit.spec.ts b/integration/tests/25-do-layout-fit.spec.ts index 77ec19577..9fc15ecba 100644 --- a/integration/tests/25-do-layout-fit.spec.ts +++ b/integration/tests/25-do-layout-fit.spec.ts @@ -99,14 +99,15 @@ for (const scenario of SCENARIOS) { await check(page, scenario, 'mail-list', found); await item.click(); await page.getByRole('button', { name: text.showDetails }).first().click(); - const routing = page.locator('section', { has: page.getByText(text.routing, { exact: true }) }).first(); + // Sections nest: the last match is the innermost, the routing section itself. + const routing = page.locator('section', { has: page.getByText(text.routing, { exact: true }) }).last(); await expect(routing).toBeVisible(); await check(page, scenario, 'mail-details', found); // The sender's address gets a column of its own width, not a sliver, and a date // takes one line. Rows: From, To, Sent, Received. - const fit = await routing.evaluate((section) => { - const values = Array.from(section.querySelectorAll('dl > dd')); + const fit = await routing.locator('dl').first().evaluate((list) => { + const values = Array.from(list.querySelectorAll(':scope > dd')); const from = values[0].getBoundingClientRect(); const sent = values[values.length - 2]; const lineHeight = parseFloat(getComputedStyle(sent).lineHeight) || 20; diff --git a/integration/tests/helpers/layout-fit.ts b/integration/tests/helpers/layout-fit.ts index 127b2d785..067a7b8cc 100644 --- a/integration/tests/helpers/layout-fit.ts +++ b/integration/tests/helpers/layout-fit.ts @@ -7,7 +7,8 @@ * Not counted: text that's meant to be cut (text-overflow: ellipsis, line clamp), * text inside a box that scrolls only sideways, text that's wholly outside its box (a * closed drawer, a slide), text in a box 1-2px wide (visually hidden), screen-reader-only text, and anything marked - * data-layout-fit-ignore. Frames (a message body) aren't looked into. + * data-layout-fit-ignore. Frames (a message body) aren't looked into. A field's + * placeholder counts too, unless it ends in an ellipsis. */ import type { Page } from '@playwright/test'; @@ -58,6 +59,10 @@ export async function findCutOffText(page: Page): Promise { if (Number(s.opacity) === 0 || s.visibility === 'hidden') { meant = true; break; } if (!clipsX(s)) continue; if (scrollsX(s)) { meant = true; break; } + // A strip of pages that scrolls both ways (the calendar's weeks side by side): + // the next page peeking in is meant. Text spilling over by accident is a few + // pixels to a column, never half the box again. + if (s.overflowX !== 'hidden' && s.overflowX !== 'clip' && at.scrollWidth > at.clientWidth * 1.5) { meant = true; break; } // A 1px box is the visually-hidden pattern: read out, not shown. if (at.clientWidth <= 2) { meant = true; break; } const r = at.getBoundingClientRect(); @@ -75,6 +80,19 @@ export async function findCutOffText(page: Page): Promise { out.push({ where: describe(el), text: text.slice(0, 60), px: Math.round(px) }); } } + // A placeholder cut off mid-word: "Termine durchs". With text-overflow: ellipsis + // it ends in "…" and says there's more. + const canvas = document.createElement('canvas').getContext('2d'); + for (const input of Array.from(document.querySelectorAll('input[placeholder]'))) { + if (input.value || !input.placeholder || input.closest('[aria-hidden="true"], [data-layout-fit-ignore]')) continue; + const s = getComputedStyle(input); + if (s.display === 'none' || s.visibility === 'hidden' || input.offsetParent === null) continue; + if (s.textOverflow === 'ellipsis' || !canvas) continue; + canvas.font = `${s.fontStyle} ${s.fontWeight} ${s.fontSize} ${s.fontFamily}`; + const room = input.clientWidth - parseFloat(s.paddingLeft) - parseFloat(s.paddingRight); + const px = canvas.measureText(input.placeholder).width - room; + if (px > 1.5) out.push({ where: describe(input), text: input.placeholder.slice(0, 60), px: Math.round(px) }); + } return out; }); }