beda6ccd3d
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
iOS Safari/WKWebView does not shrink the layout viewport for the soft keyboard (Android Chrome does): the visual viewport shrinks AND offsets down toward the focused field, and the values do not cleanly revert. The pinned shell tracked only the height (--vvh), not the offset, so its top-anchored content misaligned on iOS — empty space below the form, worst on a repeat focus. Fix (one place; covers NewGame, Chat, Feedback): - app.svelte.ts syncViewport: also mirror visualViewport.offsetTop into --vv-top, and scroll the focused field into view on the keyboard-open transition (iOS does not reliably scroll a pinned document to it). - app.css: the pinned app-shell body follows top=--vv-top + height=--vvh (was inset:0), so it stays on the visible area. - e2e/viewport.spec.ts emulates the visual-viewport resize+offset (a fake window.visualViewport) to verify the shell follows, on Chromium+WebKit. Hotseat: returning to the lobby now re-locks the current seat (its PIN is re-prompted) — LocalSource.relock, cleared in Game.svelte onDestroy. Root-caused a crash it exposed: a $props() value (id) reads back undefined during Svelte teardown, so isLocalGameId(id) threw and aborted onDestroy (breaking navigation) — read the id from the loaded view instead.
70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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');
|
|
});
|
|
});
|