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:
1 parent
41ba99c017
commit
311e992496
13 files changed
+915
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user