/** * 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 checks an item → Borja sees it checked', async ({ browser }) => { 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 itemName = `R-E-02-${Date.now()}`; 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 }); await expect(borja.page.getByText(itemName)).toBeVisible({ timeout: 5_000 }); // Ana checks the item. Wait 500ms after add to give the realtime // echo + dedupe time to settle; otherwise the row might still have // the temp UUID in the DOM and the button id changes under us. await ana.page.waitForTimeout(500); const anaRow = ana.page.locator('[role="listitem"]').filter({ hasText: itemName }).first(); await anaRow.getByRole('button', { name: /toggle item/i }).click(); // Verify Ana's own UI flipped (she checked it locally) await expect( ana.page .locator('[role="listitem"]') .filter({ hasText: itemName }) .getByRole('button', { name: /uncheck item/i }) ).toBeVisible({ timeout: 5_000 }); // And Borja should see it checked too via Realtime UPDATE await expect( borja.page .locator('[role="listitem"]') .filter({ hasText: itemName }) .getByRole('button', { name: /uncheck item/i }) ).toBeVisible({ timeout: 10_000 }); } finally { await ana.context.close(); await borja.context.close(); } }); });