/** * Smoke test for the layout-viewport anchor guard. * * Bug: on an iPhone running the installed PWA, playing a video sometimes left * the bottom-nav buttons unresponsive. iOS WebKit can scroll the document's * layout viewport behind the app's back (exiting native video fullscreen, * keyboard dismissal, scrollIntoView walking up into ) even though the * body is overflow:hidden. Fixed elements are then still drawn in place but * their hit-testing regions are offset by the stray scroll amount, so taps on * the nav do nothing — and no user gesture can scroll the document back. * * The guard (setupViewportAnchorGuard in app.js) snaps the document back to 0 * whenever it ends up scrolled, except while an input is focused (so it never * fights the on-screen keyboard); it re-anchors on blur instead. * * The static test page doesn't overflow, so each test injects a tall spacer * and relaxes the overflow clamp — simulating the scrollable-document state * iOS leaves behind. */ const { test, expect } = require('@playwright/test'); test.describe('Viewport anchor guard — stray document scroll', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await page.waitForSelector('.app', { state: 'attached' }); await page.evaluate(() => { const spacer = document.createElement('div'); spacer.style.height = '3000px'; document.body.appendChild(spacer); document.documentElement.style.overflow = 'visible'; document.body.style.overflow = 'visible'; }); }); test('snaps the document back to 0 after a stray window scroll', async ({ page }) => { await page.evaluate(() => { // boot() focuses the search input; release it so the guard is active. if (document.activeElement) document.activeElement.blur(); window.scrollTo(0, 400); }); await page.waitForFunction(() => window.scrollY === 0); expect(await page.evaluate(() => window.scrollY)).toBe(0); }); test('leaves the scroll alone while an input is focused (keyboard), re-anchors on blur', async ({ page }) => { await page.evaluate(() => { document.getElementById('searchInput').focus(); window.scrollTo(0, 300); }); // Guard must not fight the keyboard-driven scroll while editing. await page.waitForTimeout(200); expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(0); await page.evaluate(() => document.getElementById('searchInput').blur()); await page.waitForFunction(() => window.scrollY === 0); }); test('re-anchors when native video fullscreen exits (webkitendfullscreen)', async ({ page }) => { await page.evaluate(() => { if (document.activeElement) document.activeElement.blur(); window.scrollTo(0, 250); document.getElementById('video').dispatchEvent(new Event('webkitendfullscreen')); }); await page.waitForFunction(() => window.scrollY === 0); expect(await page.evaluate(() => window.scrollY)).toBe(0); }); });