Files
marcandClaude Opus 5 9507761bce Colour, a page you can see the edge of, and a document on its own
Five of the nine things on the list, and one finding that came out of them.

Colour: a marker pen and an ink colour, written as marks here rather than
installed — the schema of a note decides what can be stored in it forever. A
highlight is a <mark> so a reader that drops the colour still shows the word
as marked, and marked text is painted dark whatever the theme. Both travel as
HTML in Markdown, like underline, and both are read back.

The PDF fills the screen: the browser's own full screen for the window frame,
an overlay for the sidebar and the list, Escape and a visible button for the
way out.

The dashboard leads with the recent notes, sorted by time whatever order the
notebooks are in. A tap on the blank part of a page puts the caret at the
nearest position instead of nowhere. A drag from the left edge shows the
sidebar. The page has an edge on a phone, where it had none.

The finding: at the speed a robot types, a long note loses keystrokes between
the editor and the pagination measuring after each one — 87 of 220
paragraphs. Nothing is lost at 50 characters a second, so the page-count
check now pastes its bulk text rather than measuring typing throughput. And
the history suite asked for "the first note in the list" without waiting for
the note to reach the server; it asks by name now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:40:21 +02:00

114 lines
4.3 KiB
JavaScript

/* Version history: the state before a write is kept, and restoring one brings
* the old text back into the open editor without a reload. */
import {
OUT,
api,
check,
createNote,
createNotebook,
launch,
report,
setTitle,
typeInEditor,
withFreshUser,
} from "./lib.mjs";
const LONG = "Der ausführliche erste Entwurf, der später verloren gehen soll. ".repeat(6);
const browser = await launch();
let session;
try {
session = await withFreshUser(browser, "hist");
const p = session.page;
await createNotebook(p, "Verlauf Buch");
await createNote(p);
await setTitle(p, "Mit Verlauf");
await typeInEditor(p, LONG);
check((await p.locator(".ProseMirror").innerText()).includes("erste Entwurf"), "the long draft is in the note");
/* The note by its name, and not before the server has it.
*
* This used to take the first row of `/api/notes` the moment the text was
* typed, which is two assumptions: that the first save had already landed, and
* that the newest note is the one meant. Under load neither holds — the id
* belonged to another note, and the version this suite is about was then
* looked for in the wrong place. */
const id = await p.evaluate(async () => {
const mine = async () => {
const r = await fetch("/api/notes", {
headers: { Authorization: "Bearer " + localStorage.getItem("inknote_token") },
});
return (await r.json()).find((n) => n.title === "Mit Verlauf");
};
for (let tries = 0; tries < 20; tries++) {
const note = await mine();
if (note) return note.id;
await new Promise((r) => setTimeout(r, 500));
}
return null;
});
check(!!id, "the note reached the server", String(id));
/* Replace it with a stub. Shrinking a note by more than half is treated as a
* large deletion and bypasses the 5-minute coalescing window, so a version is
* kept immediately — exactly the case worth being able to undo. */
await p.locator(".ProseMirror").click();
await p.keyboard.press("Control+A");
await p.keyboard.type("Kurz.");
await p.waitForTimeout(3000);
check((await p.locator(".ProseMirror").innerText()).trim().startsWith("Kurz"), "the note now shows the stub");
const versions = await api(p, "GET", `/notes/${id}/versions`);
check(versions.status === 200 && versions.body.length > 0, "a version was kept", `${versions.body?.length} versions`);
check(
versions.body.some((v) => v.reason === "large-deletion"),
"the large deletion was recorded as such",
JSON.stringify(versions.body?.map((v) => v.reason))
);
/* ── restore through the interface ── */
await p.getByTitle("Version history").click();
await p.waitForTimeout(1800);
check((await p.locator("text=Restore this version").count()) > 0, "the history dialog opens");
await p.screenshot({ path: `${OUT}/history-open.png` });
const preview = await p.locator("text=erste Entwurf").count();
check(preview > 0, "the preview shows the lost text");
await p.getByRole("button", { name: "Restore this version" }).click();
await p.waitForTimeout(2500);
check((await p.locator("text=Restore this version").count()) === 0, "the dialog closed after restoring");
check(
(await p.locator(".ProseMirror").innerText()).includes("erste Entwurf"),
"the editor shows the restored text without a reload"
);
/* ── and it really went to the server ── */
await p.reload({ waitUntil: "networkidle" });
await p.waitForTimeout(2500);
await p.locator("button").filter({ hasText: "Mit Verlauf" }).first().click();
await p.waitForSelector(".ProseMirror", { timeout: 15000 });
await p.waitForTimeout(2000);
check(
(await p.locator(".ProseMirror").innerText()).includes("erste Entwurf"),
"the restored text survived a reload"
);
/* Restoring is itself a change, so the stub must now be recoverable too. */
const after = await api(p, "GET", `/notes/${id}/versions`);
check(
after.body.some((v) => v.reason === "before-restore"),
"restoring kept a version of what it replaced",
JSON.stringify(after.body?.map((v) => v.reason))
);
await p.screenshot({ path: `${OUT}/history-restored.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);
}