All checks were successful
CD / Build and push images (push) Successful in 2m59s
CI / Lint, typecheck, test (push) Successful in 2m0s
CI / Auth e2e pack (push) Successful in 2m10s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
The editor now edits over the collaboration server instead of REST — the moment Dorfteich becomes collaborative (ADR 0003, realtime-collaboration.md). Web: - New `useCollabProvider` hook binds a page's Y.Doc to a HocuspocusProvider. The document loads and persists through the collab server (#35); there is no REST autosave and no REST seed (a REST seed would fork the doc lineage and duplicate content). The collab token is fetched lazily on every (re)connect via an async token function, so an expired token is replaced transparently and a permission change takes effect on the next reconnect. - Connection-state UI replaces the save indicator: connecting / connected ("Live") / reconnecting / offline, driven by provider status + navigator online state. Read-only (`ro`) tokens make the editor non-editable with a reason; an oversize-document stateless error (#35) surfaces a banner. - Removed `use-page-autosave.ts` and `yjs-base64.ts` (no longer used). API: - `PUT /pages/:id/state` is retired and returns 410 `rest_state_write_retired` (the criterion deferred here from #35). Collab is the sole writer of page state; the read paths remain. Removed the now-dead `saveState` service. e2e / CI: - The e2e static server proxies the `/collab` WebSocket upgrade (mirrors Caddy); vite dev gains a `/collab` ws proxy. The auth-e2e CI job starts the collab server and runs a new collab pack. - New `collab.spec.ts`: two browsers converge on one page (the milestone headline), and offline edits continue locally and sync on reconnect. The read-only live assertion is a `test.fixme` until real read-only grants exist — under interim access seeing and modifying coincide, so no `ro` token is issued yet (that arrives with #53). Reworked the api/trash tests and the content editor-basics test off the retired REST write path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
97 lines
4.2 KiB
TypeScript
97 lines
4.2 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import type { BrowserContext, Page } 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 };
|
|
}
|
|
|
|
/** Opens the page in edit mode and waits for the live connection to be up. */
|
|
async function openEditor(context: BrowserContext, pondSlug: string, slug: string): Promise<Page> {
|
|
const page = await context.newPage();
|
|
await page.goto(`/p/${pondSlug}/${slug}`);
|
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
|
await expect(page.locator('.ProseMirror')).toHaveAttribute('contenteditable', 'true');
|
|
await expect(page.locator('.editor-connection')).toHaveAttribute('data-status', 'connected', {
|
|
timeout: 15000,
|
|
});
|
|
return page;
|
|
}
|
|
|
|
test('two browsers editing one page converge (the milestone headline)', async ({ browser }) => {
|
|
// fixture-user owns the pond; fixture-admin is a site admin and may modify it,
|
|
// so both receive a read-write collab token under the interim access model.
|
|
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
|
const pond = await personalPond(owner);
|
|
const created = await owner.request.post(`/api/v1/ponds/${pond.id}/pages`, {
|
|
data: { title: `Collab Converge ${Date.now()}` },
|
|
});
|
|
const { slug } = await created.json();
|
|
|
|
const pageA = await openEditor(owner, pond.slug, slug);
|
|
const pageB = await openEditor(admin, pond.slug, slug);
|
|
const editorA = pageA.locator('.ProseMirror');
|
|
const editorB = pageB.locator('.ProseMirror');
|
|
|
|
await editorA.click();
|
|
await pageA.keyboard.type('AAA from owner ');
|
|
await expect(editorB).toContainText('AAA from owner', { timeout: 10000 });
|
|
|
|
await editorB.click();
|
|
await pageB.keyboard.type('BBB from admin ');
|
|
await expect(editorA).toContainText('BBB from admin', { timeout: 10000 });
|
|
|
|
await owner.close();
|
|
await admin.close();
|
|
});
|
|
|
|
test('offline edits continue locally and sync on reconnect', async ({ browser }) => {
|
|
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
|
const pond = await personalPond(owner);
|
|
const created = await owner.request.post(`/api/v1/ponds/${pond.id}/pages`, {
|
|
data: { title: `Collab Offline ${Date.now()}` },
|
|
});
|
|
const { slug } = await created.json();
|
|
|
|
const pageA = await openEditor(owner, pond.slug, slug);
|
|
const pageB = await openEditor(admin, pond.slug, slug);
|
|
const editorA = pageA.locator('.ProseMirror');
|
|
const editorB = pageB.locator('.ProseMirror');
|
|
|
|
// Admin drops offline: the indicator reflects it and editing stays local.
|
|
await admin.setOffline(true);
|
|
await expect(pageB.locator('.editor-connection')).toHaveAttribute('data-status', 'offline', {
|
|
timeout: 10000,
|
|
});
|
|
await editorB.click();
|
|
await pageB.keyboard.type('written while offline ');
|
|
await expect(editorB).toContainText('written while offline');
|
|
// The owner, still online, has not received the offline edit.
|
|
await expect(editorA).not.toContainText('written while offline');
|
|
|
|
// Back online: the provider reconnects (re-fetching a fresh token) and the
|
|
// offline edit converges to the other participant.
|
|
await admin.setOffline(false);
|
|
await expect(pageB.locator('.editor-connection')).toHaveAttribute('data-status', 'connected', {
|
|
timeout: 20000,
|
|
});
|
|
await expect(editorA).toContainText('written while offline', { timeout: 20000 });
|
|
|
|
await owner.close();
|
|
await admin.close();
|
|
});
|
|
|
|
// Read-only participants (live changes visible, typing blocked, reason shown)
|
|
// need a real read-only grant to obtain a `ro` collab token. Under the interim
|
|
// access model seeing and modifying coincide, so no user is issued a `ro` token
|
|
// yet — the `ro` UI is implemented but only becomes reachable with #53, where
|
|
// this live assertion belongs.
|
|
test.fixme('read-only participants see changes but cannot type (needs #53)', () => {});
|