Move the browser suites into the repository

They lived in a scratch directory under /tmp, which was cleared overnight;
four of the five were simply gone. Whatever they were worth, they were
worth nothing kept there.

Rewritten against a shared lib.mjs. The old ones registered their users
through the sign-up form, which no longer exists — each suite now creates
a throwaway account through the admin API and deletes it afterwards, so a
run leaves the database as it found it. The previous generation left nine
accounts behind; those are gone too.

Three test hooks went into the app, each of which earns its place beyond
testing: aria-label on the note title (the sidebar search box is also a
text input, and a screen reader could not tell them apart either),
data-note-row on list rows, data-user on admin rows.

Two suites were flaky in ways worth naming, because both were the test
misreading the app rather than the app misbehaving:
- ticking a task then inserting a note link put the link inside the task
  list, since Control+End lands there. Verified against the server that
  the tick itself persists correctly; the suite now leaves the list first
  and asserts where the link ended up.
- the note-title lookup matched the sidebar's Trash entry, which wears the
  same emoji as a note row.

56 checks across five suites, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-08 14:30:08 +02:00
1 parent 41ba99c017
commit 311e992496
13 files changed
+915

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
/* 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");
const id = await p.evaluate(async () => {
const r = await fetch("/api/notes", {
headers: { Authorization: "Bearer " + localStorage.getItem("inknote_token") },
});
return (await r.json())[0].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);
}