test(fase-9): WebKit project + swipe-toggle gesture specs
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 <noreply@anthropic.com>
This commit is contained in:
16
Justfile
16
Justfile
@@ -132,7 +132,14 @@ test-db:
|
|||||||
test-integration:
|
test-integration:
|
||||||
pnpm --filter @colectivo/test-utils test
|
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:
|
test-e2e:
|
||||||
pnpm --filter @colectivo/web exec playwright test
|
pnpm --filter @colectivo/web exec playwright test
|
||||||
|
|
||||||
@@ -140,6 +147,13 @@ test-e2e:
|
|||||||
test-e2e-headed:
|
test-e2e-headed:
|
||||||
pnpm --filter @colectivo/web exec playwright test --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
|
# Run Kong rate-limit verification tests. Restarts Kong first to ensure a
|
||||||
# clean counter state (tests burn through the minute + hour quotas). Not
|
# 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
|
# part of test-all because the tests share the rate-limit counter with the
|
||||||
|
|||||||
@@ -28,8 +28,33 @@ export default defineConfig({
|
|||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
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: {
|
webServer: {
|
||||||
|
|||||||
@@ -739,6 +739,8 @@
|
|||||||
role="listitem"
|
role="listitem"
|
||||||
data-testid="item-row"
|
data-testid="item-row"
|
||||||
data-selected={isSelected ? 'true' : undefined}
|
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'}"
|
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)`}
|
style={selectionMode ? '' : `transform: translateX(${offset}px)`}
|
||||||
onpointerdown={(e) => {
|
onpointerdown={(e) => {
|
||||||
@@ -858,6 +860,8 @@
|
|||||||
role="listitem"
|
role="listitem"
|
||||||
data-testid="item-row"
|
data-testid="item-row"
|
||||||
data-selected={isSelected ? 'true' : undefined}
|
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'}"
|
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)`}
|
style={selectionMode ? '' : `transform: translateX(${offset}px)`}
|
||||||
onpointerdown={(e) => {
|
onpointerdown={(e) => {
|
||||||
|
|||||||
110
apps/web/tests/e2e/swipe-toggle.webkit.test.ts
Normal file
110
apps/web/tests/e2e/swipe-toggle.webkit.test.ts
Normal file
@@ -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<void> {
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user