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
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
100 lines
3.8 KiB
TypeScript
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();
|
|
});
|