Set the note left, count its pages, choose its typeface, align its text

The column sat in the middle, so it moved as the window was resized and the
start of every line landed somewhere new. It is against the margin now.

The toolbar counts pages in the words the PDF reader uses, with a hairline where
each ends. Pages are drawn, never stored: the note stays one document, Markdown
has no page, and a check asserts none reaches the stored body. The current page
is the one under the middle of the window — measuring from the top claimed "page
1" while you stood at the foot of a two-page note, because a window is shorter
than a page.

The typeface is a per-account setting rather than a per-browser one, because a
choice about how your own notes are set should follow you to the phone. The
server takes serif, sans or mono and drops anything else; that column is written
by a browser, so it is an input like any other.

Alignment is a small extension of our own — one attribute, a parse rule, a render
rule — so the schema every stored note is validated against stays in files this
project owns. Markdown has no alignment: what CommonMark allows is embedded HTML,
so non-left blocks travel as `<div align="…">`, the one form most readers still
honour, and the importer puts it back on the paragraphs. Left is never written.

The full run caught a crash I had put in: PdfWorkspace used `noteFont` without
taking it as a prop, which the build cannot see and the type checker does not
reach in .jsx. It killed the notes panel beside a PDF, and `offline` failed with
it because the whole app was broken.

224 unit tests, 263 browser checks across twelve suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-11 17:17:13 +02:00
1 parent 830af6532a
commit fed53772aa
21 files changed
+637 -9

No files matched your search

+121
View File
@@ -3,6 +3,7 @@
* freshly typed content. */
import {
OUT,
api,
check,
createNote,
createNotebook,
@@ -130,6 +131,126 @@ try {
);
check((await p.locator(".ProseMirror").innerText()).includes("Grundtext"), "plain text survived a reload");
await p.screenshot({ path: `${OUT}/editor-after-reload.png` });
/* ── how many pages the writing fills ── */
// Pages are a reading aid, not a change to the document: the note stays one
// body, and Markdown has no page. The height of one is the same 3:4 the canvas
// uses, so "page 2" means the same thing writing or drawing.
await createNote(p);
await setTitle(p, "Langer Text");
check(
(await p.locator("[data-note-position]").innerText()).includes("of 1"),
"a fresh note is one page",
await p.locator("[data-note-position]").innerText()
);
await p.locator(".ProseMirror").click();
// Enough for several pages. With only two, the middle of the window never
// leaves the first one — a window is shorter than a page — so the check would
// be testing arithmetic rather than behaviour.
for (let i = 0; i < 220; i++) {
await p.keyboard.type(`Absatz ${i + 1}: Dichtungen altern durch Wärme.`);
await p.keyboard.press("Enter");
}
await p.waitForTimeout(3500);
const position = () => p.locator("[data-note-position]").innerText();
const scrollTo = async (fraction) => {
await p.evaluate((f) => {
const pm = document.querySelector(".ProseMirror");
let el = pm.parentElement;
while (el && getComputedStyle(el).overflowY !== "auto") el = el.parentElement;
el.scrollTop = (el.scrollHeight - el.clientHeight) * f;
}, fraction);
await p.waitForTimeout(900);
return position();
};
const total = Number((await position()).match(/of (\d+)/)?.[1] || 0);
check(total > 2, "writing past the end of a page makes more of them", await position());
check(
(await p.locator("[data-note-position]").count()) === 1,
"and the count is shown the way the PDF reader shows it"
);
check((await scrollTo(0)).startsWith("Page 1"), "at the top it says page one", await position());
const nearEnd = await scrollTo(0.9);
check(
Number(nearEnd.match(/Page (\d+)/)[1]) > 1,
"scrolling down moves the count on",
nearEnd
);
/* The note is still one document — pages are drawn, not stored. */
const stored = (await api(p, "GET", "/notes")).body[0];
const body = (await api(p, "GET", `/notes/${stored.id}`)).body.content;
check(!/"type"\s*:\s*"page"/.test(body), "no page ever reaches the stored document");
/* ── alignment ── */
// Left is the default and is never written into the document, so an untouched
// paragraph reads as left without carrying an attribute for it.
await createNote(p);
await setTitle(p, "Ausrichtung");
await typeInEditor(p, "Diese Zeile wandert.");
check(
(await p.locator('[data-align="left"]').count()) === 1,
"the toolbar offers alignment"
);
await p.locator('[data-align="center"]').click();
await p.waitForTimeout(1800);
check(
(await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)) === "center",
"choosing centre centres the paragraph"
);
await reopenNote(p, "Ausrichtung");
check(
(await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)) === "center",
"and it survives a reload, so it is in the document and not on the screen"
);
const alignedBody = (await api(p, "GET", "/notes")).body;
const alignedNote = alignedBody.find((n) => n.title === "Ausrichtung");
const alignedContent = (await api(p, "GET", `/notes/${alignedNote.id}`)).body.content;
check(/"textAlign"\s*:\s*"center"/.test(alignedContent), "stored on the paragraph itself");
await p.locator('[data-align="left"]').click();
await p.waitForTimeout(1800);
check(
(await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)) === "start",
"and going back to left leaves no alignment behind",
await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)
);
/* ── the typeface follows the account ── */
await p.getByTitle("Account").click();
await p.waitForTimeout(800);
await p.locator('[data-note-font="mono"]').click();
await p.waitForTimeout(1500);
await p.keyboard.press("Escape");
await p.waitForTimeout(1200);
check(
/mono/i.test(await p.locator(".ProseMirror").evaluate((e) => getComputedStyle(e).fontFamily)),
"choosing a typeface changes the note text",
await p.locator(".ProseMirror").evaluate((e) => getComputedStyle(e).fontFamily)
);
// On the account, not in this browser — so it is there on the next device.
const me = await api(p, "GET", "/auth/me");
check(me.body?.preferences?.noteFont === "mono", "and is stored on the account", JSON.stringify(me.body?.preferences));
await p.reload({ waitUntil: "networkidle" });
await p.waitForTimeout(3000);
// A reload lands on the dashboard, so the notebook has to be opened again.
await p.locator("button").filter({ hasText: "Editor Book" }).first().click();
await p.waitForTimeout(1500);
await p.locator("[data-note-row]").first().click();
await p.waitForTimeout(2500);
check(
/mono/i.test(await p.locator(".ProseMirror").evaluate((e) => getComputedStyle(e).fontFamily)),
"and survives a reload"
);
} catch (e) {
check(false, "suite ran to the end", e.message);
} finally {