/** * O-series — Fase 2b.2 (offline-first) * * Uses Playwright's `context.setOffline(true)` to simulate a dropped * connection, then verifies that: * (a) mutations still work locally (optimistic UI + enqueue) * (b) a banner tells the user they're offline * (c) going back online flushes the queue and the items persist server-side */ import { test, expect } 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; test.describe('Offline queue + reconnect flush', () => { test('O-01: going offline shows the banner and optimistic adds stay visible', async ({ page, context }) => { await loginAs(page, USERS.borja); await page.goto(SEED_LIST_PATH); await expect(page.getByPlaceholder(ADD_ITEM_PLACEHOLDER)).toBeVisible({ timeout: 15_000 }); // Go offline. The SyncBanner subscribes to `navigator.onLine` via the // isOnline store and should render within a tick. await context.setOffline(true); await expect(page.getByTestId('sync-banner-offline')).toBeVisible({ timeout: 3_000 }); const itemName = `O-01-${Date.now()}`; const input = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); await input.fill(itemName); await input.press('Enter'); // Item renders locally even though the server call failed. await expect(page.getByText(itemName)).toBeVisible({ timeout: 3_000 }); // Cleanup: bring the page back online so the next test starts fresh. await context.setOffline(false); }); test('OF-01: going offline shows the persistent chip + a queue badge', async ({ page, context }) => { await loginAs(page, USERS.borja); await page.goto(SEED_LIST_PATH); await expect(page.getByPlaceholder(ADD_ITEM_PLACEHOLDER)).toBeVisible({ timeout: 15_000 }); // Online → chip hidden. await expect(page.getByTestId('pwa-offline-chip')).toHaveCount(0); await context.setOffline(true); // At least one chip (mobile + desktop both mount; only the visible one // matters but locator-count-based assertions need both). await expect(page.getByTestId('pwa-offline-chip').first()).toBeVisible({ timeout: 3_000 }); const itemName = `OF-01-${Date.now()}`; const input = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); await input.fill(itemName); await input.press('Enter'); // pending_ops should now contain at least the INSERT for the new item. await expect(page.getByTestId('pwa-offline-chip-badge').first()).toHaveText(/\d+/, { timeout: 3_000 }); await expect(page.getByText(itemName)).toBeVisible({ timeout: 3_000 }); await context.setOffline(false); await expect(page.getByTestId('pwa-offline-chip')).toHaveCount(0, { timeout: 5_000 }); }); test('O-02: going back online flushes the queue to the server', async ({ browser, context }) => { const page = await context.newPage(); await loginAs(page, USERS.borja); await page.goto(SEED_LIST_PATH); await expect(page.getByPlaceholder(ADD_ITEM_PLACEHOLDER)).toBeVisible({ timeout: 15_000 }); await context.setOffline(true); await expect(page.getByTestId('sync-banner-offline')).toBeVisible({ timeout: 3_000 }); const itemName = `O-02-${Date.now()}`; const input = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); await input.fill(itemName); await input.press('Enter'); await expect(page.getByText(itemName)).toBeVisible({ timeout: 3_000 }); // Back online — the queue's 'online' listener flushes pending ops. await context.setOffline(false); await expect(page.getByTestId('sync-banner-offline')).not.toBeVisible({ timeout: 5_000 }); // Verify server-side persistence with a FRESH independent context: // reload would use the same browser state; we want to prove the row // actually made it to the DB by loading the list from a new session. const verifyContext = await browser.newContext(); const verifyPage = await verifyContext.newPage(); await loginAs(verifyPage, USERS.ana); await verifyPage.goto(SEED_LIST_PATH); await expect(verifyPage.getByText(itemName)).toBeVisible({ timeout: 15_000 }); await verifyContext.close(); }); });