Neuer Block-Atom task_overview (Markdown-Fence dorfteich-tasks, HTML-Placeholder). Shared extractTaskRows liest Task-Zeilen mit Text, Mentions (#150) und Start-/Zieldaten (#152); TasksService sammelt zur Lesezeit den Teilbaum (rekursiv via collectSubtreeIds, canAccessPage- Filter je Quellseite) aus Basis-State + page_updates-Log — KEINE abgeleitete Tabelle nötig (Teilbäume sind klein, kein Drift). Neuer auth-Endpoint GET /read/:pond/:slug/tasks; die öffentliche Ansicht expandiert den Placeholder serverseitig zur statischen Tabelle (Instanz-Sprache). NodeView mit Live-Tabelle und Rückschreib-Checkboxen (optimistisch, Override bis der debounced Collab-Persist nachzieht); Einfügen über die Block-Auswahl (eingebauter Eintrag). Unit- + DB-Tests, neuer CI-Pack tasks.spec (voller Loop inkl. Rückschreiben end-to-end), User-Guide-Doku en+de. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
import { contextForUser } from './helpers';
|
|
|
|
/**
|
|
* Tasks pack (issues #150/#152/#153/#154): task lines get stable ids, the
|
|
* task overview block collects the page + subtree into a table with mention
|
|
* and date columns, and checking a box in the overview writes back to the
|
|
* source page through the collab server. Language-independent selectors.
|
|
*/
|
|
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
|
|
|
type Ctx = Awaited<ReturnType<typeof contextForUser>>;
|
|
|
|
async function personalPond(context: Ctx): 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('task overview collects subtree tasks and toggles write back', async ({ browser }) => {
|
|
test.setTimeout(120_000);
|
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const pond = await personalPond(context);
|
|
const ts = Date.now();
|
|
|
|
const parentRes = await context.request.post(`/api/v1/ponds/${pond.id}/pages`, {
|
|
data: { title: `Tasks Parent ${ts}` },
|
|
});
|
|
const parent = await parentRes.json();
|
|
const childRes = await context.request.post(`/api/v1/ponds/${pond.id}/pages`, {
|
|
data: { title: `Tasks Child ${ts}`, parentId: parent.id },
|
|
});
|
|
const child = await childRes.json();
|
|
|
|
const page = await context.newPage();
|
|
|
|
// Child: one task line with a due date (typed, so it gets an id, #153).
|
|
await page.goto(`/p/${pond.slug}/${child.slug}`);
|
|
await page.locator('.editor-page__mode-toggle').click();
|
|
const body = page.locator('.editor-content .ProseMirror');
|
|
await body.click();
|
|
await page.locator('.editor-toolbar button', { hasText: '☑' }).click();
|
|
await page.keyboard.type('Kabel prüfen >>31.12.2026 ');
|
|
await expect(page.locator('.editor-content .dt-date--due')).toBeVisible();
|
|
// The collab server persists debounced (~2 s) — wait until the task shows
|
|
// up in the collection before moving on.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const res = await context.request.get(`/api/v1/read/${pond.slug}/${child.slug}/tasks`);
|
|
return JSON.stringify(await res.json());
|
|
},
|
|
{ timeout: 20_000 },
|
|
)
|
|
.toContain('Kabel prüfen');
|
|
|
|
// Parent: a task line plus the overview block.
|
|
await page.goto(`/p/${pond.slug}/${parent.slug}`);
|
|
await page.locator('.editor-page__mode-toggle').click();
|
|
await page.locator('.editor-content .ProseMirror').click();
|
|
await page.locator('.editor-toolbar button', { hasText: '☑' }).click();
|
|
await page.keyboard.type('Bühne buchen');
|
|
await page.keyboard.press('Enter');
|
|
// Leave the task list (an empty item converts back to a paragraph).
|
|
await page.keyboard.press('Enter');
|
|
await page.locator('.editor-toolbar__block-select').selectOption('builtin/tasks');
|
|
await expect(page.locator('.dt-transclusion-card')).toBeVisible();
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const res = await context.request.get(`/api/v1/read/${pond.slug}/${parent.slug}/tasks`);
|
|
return ((await res.json()) as { tasks: unknown[] }[]).reduce(
|
|
(sum, p) => sum + p.tasks.length,
|
|
0,
|
|
);
|
|
},
|
|
{ timeout: 20_000 },
|
|
)
|
|
.toBe(2);
|
|
|
|
// Read mode: the overview table lists both tasks with the date column.
|
|
await page.reload();
|
|
const table = page.locator('.dt-task-overview-table');
|
|
await expect(table).toBeVisible();
|
|
await expect(table).toContainText('Bühne buchen');
|
|
await expect(table).toContainText('Kabel prüfen');
|
|
await expect(table.locator('tbody tr')).toHaveCount(2);
|
|
|
|
// Toggle the child's task from the overview — it writes back through the
|
|
// collab server; after the refetch the box stays checked.
|
|
const childRow = table.locator('tbody tr', { hasText: 'Kabel prüfen' });
|
|
const checkbox = childRow.locator('input[type="checkbox"]');
|
|
await expect(checkbox).toBeEnabled();
|
|
await checkbox.check();
|
|
// Write-back travels api → NOTIFY → collab → debounced persist.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const res = await context.request.get(`/api/v1/read/${pond.slug}/${child.slug}/tasks`);
|
|
const pages = (await res.json()) as { tasks: { checked: boolean }[] }[];
|
|
return pages[0]?.tasks[0]?.checked ?? false;
|
|
},
|
|
{ timeout: 20_000 },
|
|
)
|
|
.toBe(true);
|
|
await page.reload();
|
|
await expect(
|
|
page
|
|
.locator('.dt-task-overview-table tbody tr', { hasText: 'Kabel prüfen' })
|
|
.locator('input[type="checkbox"]'),
|
|
).toBeChecked();
|
|
|
|
// The source page itself now shows the checked box.
|
|
await page.goto(`/p/${pond.slug}/${child.slug}`);
|
|
await expect(page.locator('.editor-content li[data-type="task_item"] input')).toBeChecked();
|
|
|
|
await context.close();
|
|
});
|