Files
scrabble-game/ui/e2e/backswipe.spec.ts
T
Ilia Denisov 6d263ff7c6
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 41s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
UI: interactive back-swipe (left-band, snapshot backdrop)
Replace the 24px edge-swipe with a router-level interactive back gesture: a
rightward drag in the left ~38% band tracks the finger while a cheap themed
backdrop parallaxes in, and on release commits the back-navigation (past ~40%
of the width or a rightward flick) or snaps back. The live parent mounts only
on commit, which suppresses that one route slide for a seamless hand-off.

Pure decision logic (shouldArm/lockAxis/decideCommit + the per-gesture
controller) lives in lib/backswipe.ts with unit tests; the DOM wiring, hit-test
and settle animation in lib/backswipe.svelte.ts. The gesture yields to native
vertical scroll, skips the rack / draggable pending tiles / text inputs / the
board while zoomed-in, and is disabled inside Telegram (native BackButton owns
it). parentPath in App.svelte is now the single source for both the Telegram
button and the swipe.

Tests: backswipe unit (13) + e2e (3; Chromium+WebKit: commit, zoomed-in skip,
vertical scroll). Docs: UI_DESIGN.md Back bullet.
2026-06-11 16:41:36 +02:00

91 lines
3.4 KiB
TypeScript

import { expect, test, type Page } from './fixtures';
// The interactive back-swipe (left-band rightward drag → parent screen). It ignores mouse
// pointers and the Playwright desktop projects have no touch input, so these specs dispatch
// PointerEvents with pointerType:'touch' straight at the window capture listener. The
// controller hit-tests via elementFromPoint, so the start coordinate picks the element under
// the press — starting on the board lets us assert the zoom-out-only rule directly.
//
// A phone-sized viewport: the gesture's left band is a fraction of the screen width, so on a
// wide desktop the centred board sits outside it. Phones are the real target and there the
// full-width board fills the band.
test.use({ viewport: { width: 390, height: 844 } });
const MID_Y = 422; // mid-board on the 844-tall viewport
const START_X = 30; // inside the left band, over the board's left column
async function openAnnGame(page: Page): Promise<void> {
await page.goto('/');
await page.getByRole('button', { name: /guest/i }).click();
await page.getByRole('button', { name: /Ann/ }).click();
await expect(page.locator('[data-cell]').first()).toBeVisible();
await page.waitForTimeout(400); // let the open slide settle before measuring/aiming
}
/** touchDrag dispatches a single-finger touch drag from (x0,y0) to (x1,y1) over the window. */
async function touchDrag(page: Page, x0: number, y0: number, x1: number, y1: number): Promise<void> {
await page.evaluate(
({ x0, y0, x1, y1 }) => {
const steps = 8;
const fire = (type: string, x: number, y: number) =>
window.dispatchEvent(
new PointerEvent(type, {
pointerType: 'touch',
clientX: x,
clientY: y,
bubbles: true,
cancelable: true,
}),
);
fire('pointerdown', x0, y0);
for (let i = 1; i <= steps; i++) {
fire('pointermove', x0 + ((x1 - x0) * i) / steps, y0 + ((y1 - y0) * i) / steps);
}
fire('pointerup', x1, y1);
},
{ x0, y0, x1, y1 },
);
}
test('a left-band rightward swipe on a zoomed-out board returns to the lobby', async ({ page }) => {
await openAnnGame(page);
// Drag from the left band well past the commit threshold.
await touchDrag(page, START_X, MID_Y, 300, MID_Y);
// Back at the lobby: the URL returns to the root and the seeded game row is shown again.
await expect(page).toHaveURL(/#\/$/);
await expect(page.getByRole('button', { name: /Ann/ })).toBeVisible();
});
test('a swipe on a zoomed-in board does not navigate (board pan owns it)', async ({ page }) => {
await openAnnGame(page);
// Double-tap an empty cell to zoom in, then wait for the zoomed viewport.
await page
.locator('[data-cell]:not(.filled)')
.nth(20)
.evaluate((el: HTMLElement) => {
el.click();
el.click();
});
await expect(page.locator('.viewport.zoomed')).toBeVisible();
await page.waitForTimeout(300);
await touchDrag(page, START_X, MID_Y, 300, MID_Y);
// Still on the game: the swipe was suppressed over the zoomed board.
await page.waitForTimeout(300);
expect(page.url()).toContain('/game/');
});
test('a vertical drag in the band scrolls instead of navigating', async ({ page }) => {
await openAnnGame(page);
// A downward-dominant drag must lose to the native scroll and never go back.
await touchDrag(page, START_X, 260, START_X + 20, 700);
await page.waitForTimeout(300);
expect(page.url()).toContain('/game/');
});