/* Editor: tables, task lists, links between notes, and whether any of it * survives a reload — the place where a stale server response used to eat * freshly typed content. */ import { OUT, check, createNote, createNotebook, insertTable, launch, reopenNote, report, setTitle, typeInEditor, withFreshUser, } from "./lib.mjs"; const browser = await launch(); let session; try { session = await withFreshUser(browser, "editor"); const p = session.page; await createNotebook(p, "Editor Book"); await createNote(p); await typeInEditor(p, "Grundtext"); /* ── tables ── */ await insertTable(p); check((await p.locator(".ProseMirror table").count()) > 0, "table inserted"); check((await p.locator(".ProseMirror table th").count()) >= 3, "table has a header row"); /* ── task list ── */ await p.locator(".ProseMirror").click(); await p.keyboard.press("Control+End"); await p.keyboard.press("Enter"); await p.getByTitle("Task list").click(); await p.waitForTimeout(400); await p.keyboard.type("Milch kaufen"); await p.waitForTimeout(1200); check((await p.locator('.ProseMirror input[type="checkbox"]').count()) > 0, "task list inserted"); await p.locator('.ProseMirror input[type="checkbox"]').first().click(); await p.waitForTimeout(1500); await p.screenshot({ path: `${OUT}/editor-content.png` }); /* ── right-click menu on the table ── */ const cellCount = () => p.locator(".ProseMirror table td, .ProseMirror table th").count(); const before = await cellCount(); await p.locator(".ProseMirror table td").first().click({ button: "right" }); await p.waitForTimeout(600); check((await p.getByText("Insert row below").count()) > 0, "right-click in a table opens the table menu"); check((await p.getByText("Delete table").count()) > 0, "and offers deleting the table"); await p.screenshot({ path: `${OUT}/editor-table-menu.png` }); await p.getByText("Insert row below").click(); await p.waitForTimeout(1000); check((await cellCount()) > before, "the menu really added a row", `${before} → ${await cellCount()}`); const withRow = await cellCount(); await p.locator(".ProseMirror table td").first().click({ button: "right" }); await p.waitForTimeout(600); await p.getByText("Delete row").click(); await p.waitForTimeout(1000); check((await cellCount()) < withRow, "and removing one works too", `${withRow} → ${await cellCount()}`); // Outside a table the browser's own menu must stay available. await p.locator(".ProseMirror p").first().click({ button: "right" }); await p.waitForTimeout(500); check( (await p.getByText("Insert row below").count()) === 0, "right-click outside a table shows no table menu" ); await p.keyboard.press("Escape"); await p.waitForTimeout(400); /* ── a second note to link to ── */ await setTitle(p, "Quelle"); await createNote(p); await setTitle(p, "Ziel"); await typeInEditor(p, "Ich bin das Ziel."); await p.locator("button").filter({ hasText: "Quelle" }).first().click(); await p.waitForSelector(".ProseMirror", { timeout: 15000 }); await p.waitForTimeout(1200); /* ── note link via the @ menu ── */ // Control+End lands inside the task list, where a plain Enter would only make // another task. A second Enter on the empty item leaves the list, so the link // goes into a paragraph of its own and the checked task above stays untouched. await p.locator(".ProseMirror").click(); await p.keyboard.press("Control+End"); await p.keyboard.press("Enter"); await p.keyboard.press("Enter"); await p.waitForTimeout(300); await p.keyboard.type("@Ziel"); await p.waitForTimeout(1400); const menuOpen = (await p.locator("text=Ziel").count()) > 0; check(menuOpen, "@ opens the link menu"); await p.keyboard.press("Enter"); await p.waitForTimeout(1800); const linkCount = await p.locator('.ProseMirror [data-note-link], .ProseMirror .note-link').count(); check(linkCount > 0, "note link inserted into the document", `${linkCount} found`); await p.screenshot({ path: `${OUT}/editor-link.png` }); /* ── the server learned about it ── */ await p.waitForTimeout(1500); await p.locator("button").filter({ hasText: "Ziel" }).first().click(); await p.waitForSelector(".ProseMirror", { timeout: 15000 }); await p.waitForTimeout(2000); const backlink = await p.locator("text=Quelle").count(); check(backlink > 0, "the target shows a backlink to the source"); /* ── everything survives a cold reload ── */ await reopenNote(p, "Quelle"); check((await p.locator(".ProseMirror table").count()) > 0, "table survived a reload"); check( (await p.locator('.ProseMirror [data-note-link], .ProseMirror .note-link').count()) > 0, "note link survived a reload" ); check((await p.locator('.ProseMirror input[type="checkbox"]').count()) > 0, "task list survived a reload"); check( (await p.locator('.ProseMirror input[type="checkbox"]:checked').count()) === 1, "the ticked task stayed ticked, and only that one" ); check( (await p.locator(".ProseMirror li").filter({ hasText: "Ziel" }).count()) === 0, "the note link went into a paragraph, not into the task list" ); check((await p.locator(".ProseMirror").innerText()).includes("Grundtext"), "plain text survived a reload"); await p.screenshot({ path: `${OUT}/editor-after-reload.png` }); } catch (e) { check(false, "suite ran to the end", e.message); } finally { await session?.cleanup(); const code = report(); await browser.close(); process.exit(code); }