import { expect, test } from '@playwright/test'; import type { BrowserContext } from '@playwright/test'; import { contextForUser } from './helpers'; const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; async function personalPond(context: BrowserContext): Promise<{ id: string; slug: string }> { const ponds = await context.request.get('/api/v1/ponds'); const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal'); return { id: pond.id, slug: pond.slug }; } test('offline: edit, reload while offline, then reconnect converges (#38)', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const pond = await personalPond(owner); const created = await owner.request.post(`/api/v1/ponds/${pond.id}/pages`, { data: { title: `Offline ${Date.now()}` }, }); const { slug } = await created.json(); const page = await owner.newPage(); await page.goto(`/p/${pond.slug}/${slug}`); await page.getByRole('button', { name: /edit|bearbeiten/i }).click(); await expect(page.locator('.editor-connection')).toHaveAttribute('data-status', 'connected', { timeout: 15000, }); // The service worker must control the page before an offline reload can be // served from the cached app shell. await page.waitForFunction(() => navigator.serviceWorker?.controller != null, null, { timeout: 20000, }); const editor = page.locator('.ProseMirror'); await editor.click(); await page.keyboard.type('online base '); await expect(editor).toContainText('online base'); // Go offline and keep editing — the connection indicator flips and edits // continue locally. await owner.setOffline(true); await expect(page.locator('.editor-connection')).toHaveAttribute('data-status', 'offline', { timeout: 10000, }); await editor.type('offline extra '); await expect(editor).toContainText('offline extra'); // No service-worker caching of API responses: an API call offline fails // (network error) rather than being served a stale cached 200. const apiResult = await page.evaluate(async () => { try { await fetch('/api/v1/ponds', { cache: 'no-store' }); return 'ok'; } catch { return 'network-error'; } }); expect(apiResult).toBe('network-error'); // Give y-indexeddb a moment to flush the offline edit before the reload. await page.waitForTimeout(1000); // Reload the tab while offline: the app shell loads from the service worker, // the page resolves from the offline metadata cache, and its content comes // back from IndexedDB. await page.reload(); const reloaded = page.locator('.ProseMirror'); await expect(reloaded).toContainText('offline extra', { timeout: 20000 }); await expect(reloaded).toContainText('online base'); // Back online: a second participant converges on the merged content. await owner.setOffline(false); const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const adminPage = await admin.newPage(); await adminPage.goto(`/p/${pond.slug}/${slug}`); await expect(adminPage.locator('.ProseMirror')).toContainText('offline extra', { timeout: 25000, }); await owner.close(); await admin.close(); });