/** * 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(); } }); });