The interface had drifted into two languages — the frame in English, everything added later in German — which is worse than either, since the same idea appeared under two names depending on when that part was written. Sorting by title still uses the German collation, so Übung 2 comes before Übung 10: that is about the notes, not the chrome. Two archive folders were renamed with it (bilder/ → images/, zeichnungen/ → drawings/). The importer reads both spellings, and both the old and new marker line for a drawing — archives exported before this are on disk, and an import that quietly dropped their drawings would be the worst kind of tidy-up. Making a note, uploading a PDF and importing Markdown were three buttons side by side, two of them icons you had to hover to identify. They are the same act — putting something new in this notebook — so they are now one + menu. The search box moves out of the sidebar and into the note list, labelled with the notebook it narrows. Nothing changed about what it does: it has always searched only the notebook you are in, but from the sidebar it read as searching everything. A separator no longer carries a sentence explaining the order the sort menu already names. Renaming joins removing on the right-click menu — a left-click on the label started an edit, so the row could not be touched without falling into one. 218 unit tests, 238 browser checks across eleven suites. Two of those caught real breakage from this change rather than needing a translation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
141 lines
5.5 KiB
JavaScript
141 lines
5.5 KiB
JavaScript
/* 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);
|
|
}
|