All checks were successful
CD / Build and push images (push) Successful in 2m0s
CI / Lint, typecheck, test (push) Successful in 1m42s
CI / Auth e2e pack (push) Successful in 1m50s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
TipTap is bound to the canonical ProseMirror schema (packages/shared, #24) via a generic bridge (spec-utils.ts) that re-derives every node/mark's attrs/parseDOM/toDOM from editorSchema instead of duplicating them, so the editor's schema stays byte-for-byte identical to what the api decodes Yjs states against — guarded by a schema- fidelity + real Yjs round-trip test (@tiptap/y-tiptap client encoding against y-prosemirror server decoding). Route /p/:pondSlug/:pageSlug (RequireAuth) resolves the page via a new GET /ponds/:pondId/pages/:slug endpoint, binds a local Y.Doc via @tiptap/extension-collaboration (fragment "default"), and offers a view/edit mode toggle (sidebar auto-hides in edit mode via a small AppLayout context). Page state saves debounced to PUT /pages/:id/state with a truthful saving/saved/error(retrying) indicator; title saves separately via PATCH /pages/:id. Toolbar covers headings, marks, lists, blockquote, code block, hr, table (insert/row/column/header ops via prosemirror-tables), a minimal link mark, and an image placeholder (real upload is #27/#28). Closes #25 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
4.0 KiB
TypeScript
98 lines
4.0 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
import { contextForUser } from './helpers';
|
|
|
|
/**
|
|
* TipTap editor pack (issue #25). Runs against the local dev stack (api +
|
|
* web); no Mailpit needed. Creates its own page per test via the api (the
|
|
* sidebar/"new page" flow is issue #26) and navigates straight to
|
|
* `/p/:pondSlug/:pageSlug`.
|
|
*/
|
|
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
|
|
|
async function createPage(
|
|
context: Awaited<ReturnType<typeof contextForUser>>,
|
|
title: string,
|
|
): Promise<{ pondSlug: string; pageSlug: string }> {
|
|
const ponds = await context.request.get('/api/v1/ponds');
|
|
const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal');
|
|
const created = await context.request.post(`/api/v1/ponds/${pond.id}/pages`, {
|
|
data: { title },
|
|
});
|
|
const page = await created.json();
|
|
return { pondSlug: pond.slug, pageSlug: page.slug };
|
|
}
|
|
|
|
test('typing persists across reload and undo/redo work', async ({ browser }) => {
|
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const { pondSlug, pageSlug } = await createPage(context, `E2E Editor ${Date.now()}`);
|
|
const page = await context.newPage();
|
|
|
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
|
|
|
const content = page.locator('.ProseMirror');
|
|
await expect(content).toHaveAttribute('contenteditable', 'true');
|
|
await content.click();
|
|
await page.keyboard.type('Hello editor');
|
|
await expect(page.getByRole('status')).toHaveText(/saved|gespeichert/i, { timeout: 10000 });
|
|
|
|
await page.reload();
|
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
|
await expect(content).toHaveAttribute('contenteditable', 'true');
|
|
await expect(content).toContainText('Hello editor');
|
|
|
|
await content.click();
|
|
await page.keyboard.type(' more');
|
|
await expect(content).toContainText('Hello editor more');
|
|
await page.keyboard.press('ControlOrMeta+z');
|
|
await expect(content).toContainText('Hello editor');
|
|
await expect(content).not.toContainText('Hello editor more');
|
|
await page.keyboard.press('ControlOrMeta+y');
|
|
await expect(content).toContainText('Hello editor more');
|
|
|
|
await context.close();
|
|
});
|
|
|
|
test('edit mode hides the sidebar; leaving edit mode restores it', async ({ browser }) => {
|
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const { pondSlug, pageSlug } = await createPage(context, `E2E Sidebar ${Date.now()}`);
|
|
const page = await context.newPage();
|
|
|
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
|
// CSS selector, not getByRole: aria-hidden removes the element from the
|
|
// accessibility tree, which would make a role-based locator "disappear"
|
|
// exactly when we need to assert that attribute.
|
|
const sidebar = page.locator('nav.sidebar');
|
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
|
|
|
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
|
|
|
|
await page.getByRole('button', { name: /read|lesen/i }).click();
|
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
|
|
|
|
await context.close();
|
|
});
|
|
|
|
test('a save failure shows a truthful error and retries once online again', async ({ browser }) => {
|
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const { pondSlug, pageSlug } = await createPage(context, `E2E Offline ${Date.now()}`);
|
|
const page = await context.newPage();
|
|
|
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
|
await page.locator('.ProseMirror').click();
|
|
|
|
await context.setOffline(true);
|
|
await page.keyboard.type('offline text');
|
|
await expect(page.getByRole('status')).toHaveText(/failed|retrying|fehlgeschlagen/i, {
|
|
timeout: 10000,
|
|
});
|
|
|
|
await context.setOffline(false);
|
|
await expect(page.getByRole('status')).toHaveText(/saved|gespeichert/i, { timeout: 10000 });
|
|
|
|
await context.close();
|
|
});
|