From cda7321d3758151d28fce54c2c07ce712416561e Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 18 May 2026 01:37:42 +0200 Subject: [PATCH] test(fase-9): WebKit project + swipe-toggle gesture specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 9.4 (swipe-delete E2E in WebKit — repurposed to the current swipe-to-check / swipe-to-uncheck gesture, since Fase 5.10 replaced swipe-to-delete with a double-tap delete flow). - playwright.config.ts gains a second project named "webkit" (devices['iPhone 13']) gated behind RUN_WEBKIT=1 so the suite stays green on hosts that do not have the GTK/WebKit system libs (libicu74, libxml2, libmanette-0.2-0, libwoff1) installed — those need `sudo pnpm exec playwright install-deps`. The chromium project ignores `*.webkit.test.ts` so the same file never runs in both. - Justfile adds `just playwright-install` (chromium + webkit) and `just test-webkit` for the on-demand run. - New tests/e2e/swipe-toggle.webkit.test.ts contains SW-01 + SW-02: swipe right on an unchecked row marks it checked; swipe left on a checked row marks it unchecked. The handlers in lists/[id]/+page.svelte use unified pointer events, so the helper dispatches PointerEvent('pointer{down,move,up}') with pointerType:'touch' directly — works regardless of the project's hasTouch setting. - lists/[id]/+page.svelte exposes data-checked and data-name on each item-row to give the specs a robust locator without depending on text/heading hierarchy. Note: cannot validate the WebKit runs end-to-end in this sandbox (missing libicu74/libwoff1 require sudo apt install). The spec is ready to run under any CI image that has Playwright's standard linux deps installed. Co-Authored-By: Claude Opus 4.7 --- Justfile | 16 ++- apps/web/playwright.config.ts | 29 ++++- .../src/routes/(app)/lists/[id]/+page.svelte | 4 + .../web/tests/e2e/swipe-toggle.webkit.test.ts | 110 ++++++++++++++++++ 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 apps/web/tests/e2e/swipe-toggle.webkit.test.ts diff --git a/Justfile b/Justfile index 8049bd3..bcfa1b4 100644 --- a/Justfile +++ b/Justfile @@ -132,7 +132,14 @@ test-db: test-integration: pnpm --filter @colectivo/test-utils test -# Run Playwright E2E tests (headless) +# Install Playwright browsers (chromium + webkit). WebKit is required for +# touch-gesture specs (Fase 9.4 swipe-toggle) — the *.webkit.test.ts files +# are gated to that project in playwright.config.ts. +playwright-install: + pnpm --filter @colectivo/web exec playwright install chromium webkit + +# Run Playwright E2E tests (headless). Includes the WebKit project — make +# sure `just playwright-install` has run at least once. test-e2e: pnpm --filter @colectivo/web exec playwright test @@ -140,6 +147,13 @@ test-e2e: test-e2e-headed: pnpm --filter @colectivo/web exec playwright test --headed +# Run only the WebKit (Mobile Safari) touch-gesture specs (Fase 9.4). +# Gated behind RUN_WEBKIT=1 in playwright.config so it stays out of the +# default test-all (WebKit needs system libs not available on every host +# — see `just playwright-install` and `sudo pnpm exec playwright install-deps`). +test-webkit: + RUN_WEBKIT=1 pnpm --filter @colectivo/web exec playwright test --project=webkit + # Run Kong rate-limit verification tests. Restarts Kong first to ensure a # clean counter state (tests burn through the minute + hour quotas). Not # part of test-all because the tests share the rate-limit counter with the diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index b7a7191..ec8cce1 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -28,8 +28,33 @@ export default defineConfig({ projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] } - } + use: { ...devices['Desktop Chrome'] }, + // Skip WebKit-only specs (touch-gesture tests) — Chromium's + // touch emulation does not fire the touch* sequence the + // swipe handlers expect. + testIgnore: /.*\.webkit\.test\.ts/ + }, + // Fase 9.4: WebKit (Mobile Safari) project for touch-gesture + // tests. The previous swipe-to-delete tests were left .skip in + // mobile.test.ts because Chromium's touch emulation broke the + // gesture; that file was removed in Fase 5.10 when swipe was + // repurposed as a check-toggle. The replacement webkit specs + // live in `*.webkit.test.ts` and only run here. + // + // The WebKit binary needs system libs (libicu74, libwoff1, + // libmanette-0.2-0, libxml2 — install via + // `sudo pnpm exec playwright install-deps`). Hosts that don't + // have them can run the rest of the suite by leaving the env + // var unset; gated to keep `just test-all` green elsewhere. + ...(process.env.RUN_WEBKIT === '1' + ? [ + { + name: 'webkit', + use: { ...devices['iPhone 13'] }, + testMatch: /.*\.webkit\.test\.ts/ + } + ] + : []) ], webServer: { diff --git a/apps/web/src/routes/(app)/lists/[id]/+page.svelte b/apps/web/src/routes/(app)/lists/[id]/+page.svelte index e3088c9..0c8547e 100644 --- a/apps/web/src/routes/(app)/lists/[id]/+page.svelte +++ b/apps/web/src/routes/(app)/lists/[id]/+page.svelte @@ -739,6 +739,8 @@ role="listitem" data-testid="item-row" data-selected={isSelected ? 'true' : undefined} + data-checked="false" + data-name={item.name} class="relative flex items-center gap-2 py-3 select-none transition-transform duration-150 {isSelected ? 'bg-slate-100 dark:bg-slate-800' : 'bg-background'} {selectionMode ? '' : 'touch-pan-y'}" style={selectionMode ? '' : `transform: translateX(${offset}px)`} onpointerdown={(e) => { @@ -858,6 +860,8 @@ role="listitem" data-testid="item-row" data-selected={isSelected ? 'true' : undefined} + data-checked="true" + data-name={item.name} class="relative flex items-center gap-2 py-3 select-none transition-transform duration-150 {isSelected ? 'bg-slate-100 dark:bg-slate-800' : 'bg-background opacity-70'} {selectionMode ? '' : 'touch-pan-y'}" style={selectionMode ? '' : `transform: translateX(${offset}px)`} onpointerdown={(e) => { diff --git a/apps/web/tests/e2e/swipe-toggle.webkit.test.ts b/apps/web/tests/e2e/swipe-toggle.webkit.test.ts new file mode 100644 index 0000000..4d32bc5 --- /dev/null +++ b/apps/web/tests/e2e/swipe-toggle.webkit.test.ts @@ -0,0 +1,110 @@ +/** + * 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 }); + }); +});