/** * SW-series: swipe-to-toggle gestures in Mobile Safari (Fase 9.4). * * History: pre-Fase 5.10 there were two `.skip` swipe-to-delete tests in * `mobile.test.ts` because Chromium's touch emulation misfires the * touch* events the gesture handlers expect. That file was removed when * swipe was repurposed from delete → check-toggle; the gap stayed open. * * Fase 9.4 closes the gap by adding a WebKit project to playwright.config * and these two specs (the only ones gated on touch fidelity): * * SW-01: an unchecked item + swipe right → mark checked. * SW-02: a checked item + swipe left → mark unchecked. * * The swipe handlers in `apps/web/src/routes/(app)/lists/[id]/+page.svelte` * listen to `pointer*` events (unified pointer model), so we synthesise * the gesture by dispatching PointerEvents with pointerType:'touch' * directly. We chose this over `page.touchscreen.tap` so the same code * works whether the project uses `hasTouch: true` (WebKit/iPhone 13) or * not — it's the event payload the handler cares about. */ import { test, expect, type Page } from '@playwright/test'; import { USERS } from '../fixtures/users.js'; import { loginAs } from '../fixtures/login.js'; const SEED_LIST_PATH = '/lists/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; const ADD_ITEM_PLACEHOLDER = /add item|añadir producto/i; async function swipeRow(page: Page, itemName: string, dx: number): Promise { await page.evaluate( ({ name, dxLocal }) => { const row = document.querySelector( `[data-testid="item-row"][data-name="${CSS.escape(name)}"]` ) as HTMLElement | null; if (!row) throw new Error(`row ${name} not found`); const r = row.getBoundingClientRect(); const sx = r.left + r.width / 2; const sy = r.top + r.height / 2; const makeEvent = (type: string, clientX: number) => new PointerEvent(type, { bubbles: true, cancelable: true, composed: true, pointerType: 'touch', pointerId: 1, isPrimary: true, clientX, clientY: sy, button: type === 'pointerup' || type === 'pointerdown' ? 0 : -1, buttons: type === 'pointerdown' || type === 'pointermove' ? 1 : 0 }); row.dispatchEvent(makeEvent('pointerdown', sx)); for (let i = 1; i <= 8; i++) { row.dispatchEvent(makeEvent('pointermove', sx + (dxLocal * i) / 8)); } row.dispatchEvent(makeEvent('pointerup', sx + dxLocal)); }, { name: itemName, dxLocal: dx } ); } test.describe('Swipe-to-toggle — Mobile Safari (Fase 9.4)', () => { test.beforeEach(async ({ page }) => { await loginAs(page, USERS.borja); await page.goto(SEED_LIST_PATH); await expect(page.getByPlaceholder(ADD_ITEM_PLACEHOLDER)).toBeVisible({ timeout: 15_000 }); }); test('SW-01: unchecked item + swipe right → marked checked', async ({ page }) => { const itemName = `SW-01-${Date.now()}`; const input = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); await input.fill(itemName); await input.press('Enter'); const unchecked = page.locator( `[data-testid="item-row"][data-name="${itemName}"][data-checked="false"]` ); await expect(unchecked).toBeVisible({ timeout: 10_000 }); await swipeRow(page, itemName, 180); const checked = page.locator( `[data-testid="item-row"][data-name="${itemName}"][data-checked="true"]` ); await expect(checked).toBeVisible({ timeout: 10_000 }); }); test('SW-02: checked item + swipe left → marked unchecked', async ({ page }) => { const itemName = `SW-02-${Date.now()}`; const input = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); await input.fill(itemName); await input.press('Enter'); await expect( page.locator(`[data-testid="item-row"][data-name="${itemName}"][data-checked="false"]`) ).toBeVisible({ timeout: 10_000 }); // Check it first via a swipe right. await swipeRow(page, itemName, 180); await expect( page.locator(`[data-testid="item-row"][data-name="${itemName}"][data-checked="true"]`) ).toBeVisible({ timeout: 10_000 }); // Now swipe left to uncheck. await swipeRow(page, itemName, -180); await expect( page.locator(`[data-testid="item-row"][data-name="${itemName}"][data-checked="false"]`) ).toBeVisible({ timeout: 10_000 }); }); });