Put the interface into English, and give the notebook one way in

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>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-11 02:17:22 +02:00
1 parent 0a7cf26b28
commit 52bd2b8f3d
27 files changed
+469 -336

No files matched your search

+26 -18
View File
@@ -12,7 +12,9 @@ import {
createNote,
createNotebook,
launch,
openAddMenu,
report,
search,
setTitle,
typeInEditor,
withFreshUser,
@@ -106,13 +108,11 @@ try {
);
/* The point of the whole feature. */
await p.getByPlaceholder("Search notes...").fill("Beta");
await p.waitForTimeout(1200);
await search(p, "Beta");
const searched = await titles();
check(searched.length < 3, "searching narrows the list", searched.join(", "));
await p.getByPlaceholder("Search notes...").fill("");
await p.waitForTimeout(1500);
await search(p, "");
check(
(await titles()).join(",") === arranged.join(","),
"and clearing the search shows exactly the arrangement again",
@@ -150,15 +150,21 @@ try {
await p.waitForTimeout(1800);
check((await p.locator("[data-separator]").count()) === 1, "a separator can be added");
/* Renaming is right-click, like removing. A left-click on the label used to
start an edit, so the row could not be touched without falling into one. */
const label = p.getByLabel("Separator label");
await p.locator("[data-separator] button").first().click();
await p.waitForTimeout(300);
check((await label.count()) === 0, "clicking a separator does not drop straight into renaming");
await p.locator("[data-separator]").click({ button: "right" });
await p.waitForTimeout(600);
await p.getByText("Rename", { exact: true }).click();
await p.waitForTimeout(500);
await label.fill("Wichtig");
await label.press("Enter");
await p.waitForTimeout(1800);
check(
/wichtig/i.test(await p.locator("[data-separator]").innerText()),
"and named",
"and it can be named from the same menu",
await p.locator("[data-separator]").innerText()
);
@@ -197,14 +203,14 @@ try {
await p.locator("[data-separator]").click({ button: "right" });
await p.waitForTimeout(600);
await p.getByText("Trenner entfernen").click();
await p.getByText("Remove separator").click();
await p.waitForTimeout(600);
check(
/Notizen bleiben/.test(await p.locator("body").innerText()),
/notes stay/i.test(await p.locator("body").innerText()),
"right-clicking it asks first, and says the notes are not affected"
);
await p.getByRole("button", { name: "Entfernen" }).click();
await p.getByRole("button", { name: "Remove" }).click();
await p.waitForTimeout(2000);
check((await p.locator("[data-separator]").count()) === 0, "and then it is gone");
check((await titles()).length === 3, "with every note still there", (await titles()).join(", "));
@@ -251,7 +257,7 @@ try {
check(markdown.includes("- [x] erledigt"), "the export carries the ticked box", JSON.stringify(markdown.slice(-60)));
check(markdown.startsWith("# Beta"), "and the title as a heading");
await p.locator("[data-export-dialog] button").filter({ hasText: /Schließen|Fertig|Close/ }).first().click().catch(() => {});
await p.locator("[data-export-dialog] button").filter({ hasText: /Close|Done/ }).first().click().catch(() => {});
await p.keyboard.press("Escape");
await p.waitForTimeout(600);
@@ -270,14 +276,15 @@ try {
}, markdown);
const chooser = p.waitForEvent("filechooser");
await p.locator("[data-import-button]").click();
await openAddMenu(p);
await p.locator('[data-add-option="Import Markdown"]').click();
const fc = await chooser;
await fc.setFiles({ name: "Beta.md", mimeType: "text/markdown", buffer: Buffer.from(markdown) });
await p.waitForTimeout(4000);
const importedText = await p.locator("body").innerText();
check(/importiert/i.test(importedText), "the import reports what it did", importedText.slice(0, 200).replace(/\n/g, " "));
await p.getByRole("button", { name: /Fertig/ }).click();
check(/imported/i.test(importedText), "the import reports what it did", importedText.slice(0, 200).replace(/\n/g, " "));
await p.getByRole("button", { name: /Done/ }).click();
await p.waitForTimeout(2500);
const after = (await api(p, "GET", "/notes")).body.length;
@@ -307,7 +314,7 @@ try {
check(await field.isVisible(), "the link dialog is a real dialog in the page");
await field.fill("javascript:alert(1)");
await p.getByRole("button", { name: /Einfügen/ }).click();
await p.getByRole("button", { name: /^Insert$/ }).click();
await p.waitForTimeout(400);
check(
await field.isVisible(),
@@ -315,7 +322,7 @@ try {
);
await field.fill("example.com");
await p.getByRole("button", { name: /Einfügen/ }).click();
await p.getByRole("button", { name: /^Insert$/ }).click();
await p.waitForTimeout(1800);
const href = await p.locator(".ProseMirror a[href]").first().getAttribute("href");
check(href === "https://example.com", "a bare domain is taken as https", String(href));
@@ -341,7 +348,8 @@ try {
await p.keyboard.press("Escape");
await p.locator("h2").first().click();
await p.waitForTimeout(500);
await p.getByTitle("New Note").click();
await openAddMenu(p);
await p.locator('[data-add-option="New note"]').click();
await p.waitForTimeout(900);
const books0 = (await api(p, "GET", "/notebooks")).body;
@@ -352,7 +360,7 @@ try {
// sort order, so "the last one" is not a stable way to mean a particular book.
const target = books0.find((b) => b.name === "Zweites Buch");
await p.locator(`[data-notebook-option="${target.id}"]`).click();
await p.getByRole("button", { name: /Weiter/ }).click();
await p.getByRole("button", { name: /Continue/ }).click();
await p.waitForTimeout(2500);
// By title, not by position: the list is ordered by a timestamp with one