Files
collective-lists/apps/web/tests/e2e/realtime.test.ts
Oier Bravo Urtasun 1cb408acc2 feat(fase-5.10): /lists/[id] row redesign — buttons + swipe-toggle + overlay
Replaces the checkbox/hover-delete model with an explicit gesture+overlay
flow on /lists/[id].

Row anatomy (new)
  [drag-handle]  [name as button]  [qty stepper  − N +]
  - No checkbox in normal mode.
  - Checked items render with strikethrough + muted text.
  - Drag handle always visible on mobile; revealed on hover on desktop
    (md:opacity-0 md:group-hover:opacity-100). The handle owns the drag
    (pointerdown → dragEnabled=true), so row swipes + taps keep working.

Swipe gestures (symmetric toggle, no delete)
  unchecked + swipe RIGHT → mark checked (green success reveal)
  checked   + swipe LEFT  → mark unchecked (neutral reveal)
  opposite direction for each state is a no-op (snaps back).
  SWIPE_COMMIT_THRESHOLD=120 (was 200 for delete); REVEAL_WIDTH=96.
  Symmetry means undo is the inverse swipe — no undoQueue on toggles.

Double-tap overlay (name + delete + confirm)
  Short tap: no-op. Two taps within DOUBLE_TAP_WINDOW_MS (300 ms) open a
  full-screen dialog (bottom sheet on mobile) with:
    - X close button (top-right)
    - name input
    - Delete button (red) → scheduleUndoable → UndoToast
    - Confirm button (primary) → updateItem({ name })
  Click outside / Escape / X all dismiss without saving.

Tests
  items.test.ts
    D-03 rewritten: double-tap opens overlay, value matches, X closes.
    D-04 rewritten: overlay Delete removes the row; toast locator scoped
      out of itemRow so it doesn't shadow the assertion.
  realtime.test.ts
    R-E-02 updated: Ana renames via overlay → Borja sees new name over
      Realtime UPDATE. (The check/uncheck path is now gestural only and
      chromium touch emulation is too flaky to drive it in E2E; Vitest
      integration R-02 still covers the UPDATE-row-payload invariant.)
  mobile-swipe-delete.test.ts stays describe.skip — will be renamed and
  rewritten for the new semantics once WebKit install lands in CI.

i18n
  list_qty_decrease, list_qty_increase, list_reorder_handle,
  list_edit_item, list_confirm, list_close — en/es.

Verification
  just test-e2e → 40 passed, 2 skipped (same as pre-change).
  Type-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:20:55 +02:00

94 lines
3.8 KiB
TypeScript

/**
* R-E2E series — Fase 2b.1 (dual-browser-context Realtime sync)
*
* Each test spins up two browser contexts (Ana + Borja), each with its own
* Keycloak session, and verifies that a mutation in one context is visible
* in the other without a page reload.
*/
import { test, expect, type Browser } 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 loggedInListPage(browser: Browser, user: (typeof USERS)[keyof typeof USERS]) {
const context = await browser.newContext();
const page = await context.newPage();
await loginAs(page, user);
await page.goto(SEED_LIST_PATH);
await expect(page.getByPlaceholder(ADD_ITEM_PLACEHOLDER)).toBeVisible({ timeout: 15_000 });
return { context, page };
}
test.describe('Realtime sync — two sessions on the same list', () => {
test('R-E-01: Ana adds an item → Borja sees it without refreshing', async ({ browser }) => {
const ana = await loggedInListPage(browser, USERS.ana);
const borja = await loggedInListPage(browser, USERS.borja);
try {
const itemName = `R-E-01-${Date.now()}`;
// Ana adds the item via her sticky form
const anaInput = ana.page.getByPlaceholder(ADD_ITEM_PLACEHOLDER);
await anaInput.fill(itemName);
await anaInput.press('Enter');
await expect(ana.page.getByText(itemName)).toBeVisible({ timeout: 5_000 });
// Borja should see it land via Realtime, no reload
await expect(borja.page.getByText(itemName)).toBeVisible({ timeout: 5_000 });
} finally {
await ana.context.close();
await borja.context.close();
}
});
test('R-E-02: Ana renames an item → Borja sees the new name via Realtime UPDATE', async ({ browser }) => {
// Fase 5.10 removed the checkbox/toggle button from the list detail
// view. Check/uncheck is a swipe gesture (unreliable under Chromium
// touch emulation). Renaming an item via the double-tap overlay fires
// the same realtime UPDATE path — this test still covers Ana → Borja
// sync; Vitest integration R-02 covers the UPDATE-row-payload invariant.
const ana = await loggedInListPage(browser, USERS.ana);
const borja = await loggedInListPage(browser, USERS.borja);
borja.page.on('console', (m) => {
if (m.type() === 'error') console.log(` borja [err] ${m.text()}`);
});
try {
const originalName = `R-E-02-orig-${Date.now()}`;
const renamedName = `R-E-02-new-${Date.now()}`;
const anaInput = ana.page.getByPlaceholder(ADD_ITEM_PLACEHOLDER);
await anaInput.fill(originalName);
await anaInput.press('Enter');
await expect(ana.page.getByText(originalName)).toBeVisible({ timeout: 5_000 });
await expect(borja.page.getByText(originalName)).toBeVisible({ timeout: 5_000 });
// Wait a beat so the optimistic tempId is swapped for the real one.
await ana.page.waitForTimeout(500);
// Open the overlay via the row's name button (double-click = double-tap).
const anaRowNameBtn = ana.page
.locator('[role="listitem"]')
.filter({ hasText: originalName })
.getByRole('button', { name: originalName })
.first();
await anaRowNameBtn.dblclick();
const overlay = ana.page.getByTestId('edit-overlay');
await expect(overlay).toBeVisible({ timeout: 3_000 });
const overlayInput = overlay.getByPlaceholder(/add item|añadir producto/i);
await overlayInput.fill(renamedName);
await overlay.getByRole('button', { name: /^confirm$|^confirmar$/i }).click();
// Ana's UI shows the new name, and Borja's catches up via Realtime.
await expect(ana.page.getByText(renamedName)).toBeVisible({ timeout: 5_000 });
await expect(borja.page.getByText(renamedName)).toBeVisible({ timeout: 10_000 });
} finally {
await ana.context.close();
await borja.context.close();
}
});
});