dorfteich/apps/web/e2e/offline.spec.ts
Claude Opus 4.8 41b259fae5
Some checks failed
CD / Build and push images (push) Successful in 54s
CI / Lint, typecheck, test (push) Successful in 1m59s
CI / Auth e2e pack (push) Failing after 2m12s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 11s
Harden the offline e2e for slower CI timing (#38)
The offline pack passed locally but flaked in CI. Make its timing-sensitive
steps robust without changing the feature:

- Before going offline, wait until the service worker not only controls the
  page but has actually populated Cache Storage (app-shell precache complete),
  so the offline reload is guaranteed to be servable from cache.
- After coming back online, wait for the reloaded tab to reconnect (so it has
  pushed its local state) before checking a second client converges.
- Raise the service-worker-ready and convergence timeouts, and the IndexedDB
  flush wait, for headroom on slower runners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-08 22:24:29 +02:00

100 lines
3.8 KiB
TypeScript

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 AND have finished precaching the
// app shell before an offline reload can be served from cache. Checking the
// Cache Storage is populated avoids a race where the SW controls the page but
// its install (precache) has not completed yet.
await page.waitForFunction(
async () => {
if (!navigator.serviceWorker?.controller) return false;
const names = await caches.keys();
for (const name of names) {
const cache = await caches.open(name);
if ((await cache.keys()).length > 0) return true;
}
return false;
},
null,
{ timeout: 30000 },
);
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(1500);
// 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: the reloaded tab reconnects and pushes its local state to the
// server; wait for that before checking a second participant converges.
await owner.setOffline(false);
await expect(page.locator('.editor-connection')).toHaveAttribute('data-status', 'connected', {
timeout: 30000,
});
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: 30000,
});
await owner.close();
await admin.close();
});