Files
InkNote/e2e/chapters.mjs
T
marcandClaude Opus 5 632ea5dcd9 Move a note in one press, and stop Draw inventing a page
Moving lived under a right-click, behind a submenu: two steps before the
list of places appeared at all, and on a phone a long press followed by a
flyout. There is a button on the row now, and it opens one screen with
the chapters of this notebook and the other notebooks together, marking
where the note already is.

The spare sheet in Draw mode was drawn as solid paper, so pressing Draw
on a one-page note made a second page appear from nowhere while the count
still said "of 1". It exists for a good reason — pages otherwise come
only from how far the writing runs, so drawing on page two would mean
writing two pages first — but it should look like the offer it is. Dashed
outline, half opacity, and it says what it is for. It counts once
something is on it.

And the two buttons above the list did not match: the one I added last
round was larger and bolder than the one it sat beside, and the older
one's target was nineteen pixels tall, which a finger misses. Both are
one shape now. A separator's label matches a chapter's name in size for
the same reason — two type treatments in one list is noise, and the
difference was not saying anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:43:51 +02:00

131 lines
5.1 KiB
JavaScript

/* Chapters — folders inside a notebook.
*
* The claim the feature rests on is the one under test: a note filed into a
* chapter is *in* it and not also loose in the notebook, and removing a chapter
* never removes what it held. Both look fine on screen while being wrong on the
* server, so the server is asked directly.
*/
import {
api,
check,
createNote,
createNotebook,
launch,
report,
setTitle,
withFreshUser,
} from "./lib.mjs";
const browser = await launch();
let session;
try {
session = await withFreshUser(browser, "chapters");
const p = session.page;
const titles = () =>
p.locator("[data-note-row] [data-note-title]").evaluateAll((els) => els.map((e) => e.textContent.trim()));
const addChapter = async (name) => {
await p.locator("[data-add-chapter]").click();
await p.waitForTimeout(600);
await p.getByLabel("Chapter name").fill(name);
await p.getByRole("button", { name: "Create", exact: true }).click();
await p.waitForTimeout(2000);
};
/* One press of the button on the row, then choose. It used to be a
right-click into a submenu — two steps before the places even appeared, and
on a phone a long press followed by a flyout. */
const moveTo = async (noteTitle, target) => {
const row = p.locator("[data-note-row]").filter({ hasText: noteTitle }).first();
await row.locator("xpath=preceding-sibling::button[@data-move-note]").first().click();
await p.waitForTimeout(700);
await p.locator("[data-move-target]").filter({ hasText: target }).first().click();
await p.waitForTimeout(2500);
};
await createNotebook(p, "Haus");
await p.locator("button").filter({ hasText: "Haus" }).first().click();
await p.waitForTimeout(700);
for (const name of ["Zählerstand", "Stromrechnung"]) {
await createNote(p);
await setTitle(p, name);
await p.waitForTimeout(600);
}
await p.waitForTimeout(1000);
/* ── making one ── */
check((await p.locator("[data-add-chapter]").count()) === 1, "a notebook can be given a chapter");
await addChapter("Rechnungen");
check((await p.locator("[data-chapter-row]").count()) === 1, "and it appears in the list");
const server = (await api(p, "GET", "/chapters")).body;
check(server.length === 1 && server[0].name === "Rechnungen", "the server has it", JSON.stringify(server));
/* ── filing a note ── */
check(
(await p.locator("[data-move-note]").count()) === 2,
"every note has a move button, without a right-click",
String(await p.locator("[data-move-note]").count())
);
await moveTo("Stromrechnung", "Rechnungen");
check(
(await titles()).join(",") === "Zählerstand",
"a note filed into a chapter leaves the notebook's own list",
(await titles()).join(", ")
);
await p.locator("[data-chapter-row]").first().click();
await p.waitForTimeout(1200);
check((await titles()).join(",") === "Stromrechnung", "and is inside the chapter", (await titles()).join(", "));
check((await p.locator("[data-chapter-back]").count()) === 1, "with a way back out");
const filed = (await api(p, "GET", "/notes")).body.find((n) => n.title === "Stromrechnung");
check(!!filed?.chapter_id, "the server filed it too", String(filed?.chapter_id));
/* A note made inside a chapter belongs to it, without being moved. */
await createNote(p);
await setTitle(p, "Wasserrechnung");
await p.waitForTimeout(1500);
const made = (await api(p, "GET", "/notes")).body.find((n) => n.title === "Wasserrechnung");
check(made?.chapter_id === filed?.chapter_id, "a note made inside a chapter is filed there", String(made?.chapter_id));
await p.locator("[data-chapter-back]").click();
await p.waitForTimeout(1200);
check(
(await titles()).join(",") === "Zählerstand",
"and going back shows the notebook again, without the chapter's notes",
(await titles()).join(", ")
);
/* ── a chapter is not a wastebasket ──
* Removing one must never remove what it held: the notes come back to the
* notebook, where they can be seen and filed again. */
await p.locator("[data-chapter-row]").first().click({ button: "right", force: true });
await p.waitForTimeout(600);
await p.getByText("Remove chapter", { exact: true }).click();
await p.waitForTimeout(700);
check(/stay/i.test(await p.locator("body").innerText()), "removing one says out loud that the notes stay");
await p.getByRole("button", { name: "Remove", exact: true }).click();
await p.waitForTimeout(2500);
check((await p.locator("[data-chapter-row]").count()) === 0, "the chapter is gone");
const after = (await titles()).sort().join(",");
check(
after === "Stromrechnung,Wasserrechnung,Zählerstand",
"and every note it held is back in the notebook",
after
);
const freed = (await api(p, "GET", "/notes")).body;
check(
freed.every((n) => !n.chapter_id),
"with nothing on the server still pointing at a chapter that is gone",
JSON.stringify(freed.map((n) => [n.title, n.chapter_id]))
);
} catch (e) {
check(false, "suite ran to the end", e.message);
} finally {
await session?.cleanup();
const code = report();
await browser.close();
process.exit(code);
}