import { test, expect, type Page } from './fixtures'; // Playwright's WebKit has no real soft keyboard, so emulate iOS's visual-viewport behaviour: replace // window.visualViewport with a controllable fake BEFORE the app boots. The app's syncViewport // (app.svelte.ts) reads visualViewport.height/offsetTop and mirrors them into --vvh / --vv-top, which // position the pinned app-shell (app.css html.app-shell body). Driving the fake exercises the exact // code path the real keyboard triggers on iOS — where the layout viewport does NOT shrink and the // visual viewport instead offsets down toward the focused field. async function installFakeViewport(page: Page): Promise { await page.addInitScript(() => { const fake = new EventTarget() as EventTarget & { height: number; offsetTop: number; width: number }; fake.height = window.innerHeight; fake.offsetTop = 0; fake.width = window.innerWidth; Object.defineProperty(window, 'visualViewport', { configurable: true, value: fake }); (window as unknown as { __vv: typeof fake }).__vv = fake; }); } async function setViewport(page: Page, height: number, offsetTop: number): Promise { await page.evaluate( ([h, t]) => { const vv = (window as unknown as { __vv: { height: number; offsetTop: number; dispatchEvent(e: Event): boolean } }).__vv; vv.height = h; vv.offsetTop = t; vv.dispatchEvent(new Event('resize')); vv.dispatchEvent(new Event('scroll')); }, [height, offsetTop], ); } async function shell(page: Page): Promise<{ vvh: string; top: string; bodyTop: string }> { return page.evaluate(() => ({ vvh: getComputedStyle(document.documentElement).getPropertyValue('--vvh').trim(), top: getComputedStyle(document.documentElement).getPropertyValue('--vv-top').trim(), bodyTop: getComputedStyle(document.body).top, })); } test.describe('visual-viewport shell (soft-keyboard alignment)', () => { test('the pinned shell follows the visual viewport height AND offset', async ({ page }) => { await installFakeViewport(page); await page.goto('/'); await expect(page.locator('html.app-shell')).toBeAttached(); const innerH = await page.evaluate(() => window.innerHeight); // Keyboard closed: full height, no offset. await setViewport(page, innerH, 0); let s = await shell(page); expect(s.top).toBe('0px'); expect(s.bodyTop).toBe('0px'); // Keyboard open + iOS offset: the visual viewport shrinks AND offsets down. The pinned shell must // follow BOTH — the body's top must equal the offset (not stay at 0), else the top-anchored // content shows empty space below (the iOS bug this fixes). await setViewport(page, innerH - 300, 180); s = await shell(page); expect(s.vvh).toBe(`${innerH - 300}px`); expect(s.top).toBe('180px'); expect(s.bodyTop).toBe('180px'); // Keyboard closed again: the offset reverts to 0 — no stale shift left behind. await setViewport(page, innerH, 0); s = await shell(page); expect(s.top).toBe('0px'); expect(s.bodyTop).toBe('0px'); }); });