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 { 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 { 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/'); });