diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index cdae421..9dabf5d 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -576,7 +576,8 @@ test('a desktop postcode search opens on the map with the list beside it', async await expect(page.locator('.sc-popup')).toContainText(name); }); -for (const width of [360, 390, 430]) { +// 402 is the iPhone 17, where the toolbar overflowed (see below). +for (const width of [360, 390, 402, 430]) { test(`a phone opens on the map, with the list a tap away, at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 800 }); await searchByName(page, 'B1 1BB'); @@ -597,13 +598,34 @@ for (const width of [360, 390, 430]) { await page.locator('.sc-pin').first().dispatchEvent('click'); const sheet = page.locator('[class*="bottomSheet"]'); await expect(sheet).toBeVisible(); - const sheetBox = (await sheet.boundingBox())!; - expect(sheetBox.y + sheetBox.height).toBeLessThanOrEqual(barTop); - expect(sheetBox.y).toBeGreaterThanOrEqual(fabBox.y + fabBox.height); + // The sheet slides up over 0.3s; measure where it comes to rest, not a + // frame of the animation (which reads as below the tab bar). + await expect.poll(async () => { + const box = (await sheet.boundingBox())!; + return Math.round(box.y + box.height); + }).toBeLessThanOrEqual(barTop); + // The dock is anchored at the bottom, so the sheet pushes the button up: + // measure the button again now that the sheet is in. + const fabNow = (await toList.boundingBox())!; + expect((await sheet.boundingBox())!.y).toBeGreaterThanOrEqual(fabNow.y + fabNow.height); // MOBILE.md: no horizontal overflow, and 44px targets in the new chrome. expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)) .toBe(0); + // scrollWidth alone cannot see this page's overflow: .main clips on x, so + // the search summary ran 40px off an iPhone 17 screen with scrollWidth + // still equal to the viewport. Measure the toolbar's own edges instead; + // the controls row scrolls by design, so only its box is held to the edge. + const offscreen = await page.evaluate(() => { + const toolbar = document.querySelector('[class*="resultsToolbar"]'); + return [...(toolbar?.querySelectorAll('*') ?? [])] + .filter((el) => (el as HTMLElement).offsetParent + && !el.parentElement?.closest('[class*="controlsRow"]')) + .map((el) => ({ el: (el.className?.toString() || el.tagName).slice(0, 40), + right: Math.round(el.getBoundingClientRect().right) })) + .filter((o) => o.right > window.innerWidth); + }); + expect(offscreen).toEqual([]); const small = await page.evaluate(() => { const toolbar = document.querySelector('[class*="resultsToolbar"]'); const fabEl = document.querySelector('[class*="viewFab"]'); diff --git a/nextjs-app/components/FilterBar.module.css b/nextjs-app/components/FilterBar.module.css index 0df34a8..611b03c 100644 --- a/nextjs-app/components/FilterBar.module.css +++ b/nextjs-app/components/FilterBar.module.css @@ -522,8 +522,16 @@ * everything else, so it must never be the chip scrolled out of sight. */ @media (max-width: 640px) { + /* + * nowrap matters as much as column. The desktop rule wraps, and in a + * wrapping flex container each line is as wide as its widest item's content, + * not the container: the search summary ("SW196AR ยท within 1 mile Edit", + * about 410px) stretched the line, and the controls row with it, 40px past a + * 402px iPhone 17 screen. Single-line, stretch means the container's width. + */ .filterBar:not(.heroMode) { flex-direction: column; + flex-wrap: nowrap; align-items: stretch; gap: 0.625rem; }