Let a margin remark become a note, and lead to it ever after
Some remarks turn into a page of their own. "Notiz anlegen" on a margin card makes one, titles it from what was written, and goes there; the pin keeps its id so the passage leads to that note afterwards. The note is created with a link back to the document, so the connection reads both ways and the document lists it among its backlinks — a note born from a passage should not be a note with nothing on it saying which passage. An existing note can be linked instead, and a link can be let go of without touching the note. Both are offered on the phone sheet too. Two older bugs stood in the way and are fixed: `openLinkTarget` finds a note's notebook by looking it up in the notes list, and a note created in the same tick is not in the copy that callback closed over — so jumping to a fresh note went nowhere at all. Creating one now navigates by the notebook it already knows. A drawing save waits 700ms for the next stroke, and leaving the document inside that window dropped it. That is precisely what turning a remark into a note does, so the link would have been lost on the way out. The viewer flushes a pending write when it unmounts, which also covers drawing and navigating away quickly. 215 unit tests, 229 browser checks across eleven suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
011ba82a94
commit
502aecfc92
10 files changed
+541
-12
No files matched your search
+75
-1
@@ -7,6 +7,7 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { inflateSync } from "node:zlib";
|
||||
import {
|
||||
api,
|
||||
captureDownloads,
|
||||
check,
|
||||
clearDownloads,
|
||||
@@ -119,6 +120,7 @@ try {
|
||||
await p.waitForTimeout(4000);
|
||||
|
||||
check((await p.locator("[data-pdf-page]").count()) > 0, "the document is shown after importing");
|
||||
const pdfNoteId = (await api(p, "GET", "/notes")).body[0].id;
|
||||
check(
|
||||
(await p.locator("[data-pdf-position]").innerText()).includes("/ 2"),
|
||||
"both pages are counted",
|
||||
@@ -631,6 +633,73 @@ try {
|
||||
|
||||
|
||||
|
||||
/* ── a remark that turns into a note ── */
|
||||
await p.locator("[data-pdf-pin-mode]").click();
|
||||
await p.waitForTimeout(400);
|
||||
const pageL = await p.locator('[data-pdf-page="1"]').boundingBox();
|
||||
await p.mouse.click(pageL.x + pageL.width * 0.35, pageL.y + pageL.height * 0.2);
|
||||
await p.waitForTimeout(800);
|
||||
await p.getByLabel("Pinned note").fill("Zebrastreifen nachschlagen");
|
||||
await p.waitForTimeout(400);
|
||||
|
||||
const notesBefore = (await api(p, "GET", "/notes")).body.length;
|
||||
await p.locator("[data-pin-make-note]").click();
|
||||
await p.waitForTimeout(4000);
|
||||
|
||||
const notesAfter = (await api(p, "GET", "/notes")).body;
|
||||
check(notesAfter.length === notesBefore + 1, "a remark can become a note of its own", `${notesBefore} → ${notesAfter.length}`);
|
||||
|
||||
const made = notesAfter.find((n) => n.title === "Zebrastreifen nachschlagen");
|
||||
check(!!made, "titled from what was written in the margin");
|
||||
check(
|
||||
(await p.locator(".ProseMirror").count()) > 0,
|
||||
"and the app goes straight there rather than leaving it to be found"
|
||||
);
|
||||
|
||||
/* The link has to read in both directions, or a note born from a passage has
|
||||
nothing on it saying which passage. */
|
||||
const body = (await api(p, "GET", `/notes/${made.id}`)).body.content;
|
||||
check(body.includes(pdfNoteId), "the new note carries a link back to the document", body.slice(0, 120));
|
||||
|
||||
const back = await api(p, "GET", `/notes/${pdfNoteId}/backlinks`);
|
||||
check(
|
||||
Array.isArray(back.body) && back.body.some((b) => b.id === made.id),
|
||||
"so the document lists it among its backlinks",
|
||||
JSON.stringify(back.body).slice(0, 120)
|
||||
);
|
||||
|
||||
/* Back to the document: the pin now leads to the note. */
|
||||
await p.locator("[data-note-row]").filter({ hasText: "e2e-test" }).first().click();
|
||||
await p.waitForTimeout(4000);
|
||||
const goto = p.locator(`[data-pin-goto="${made.id}"]`);
|
||||
check((await goto.count()) === 1, "and the margin card links to it");
|
||||
check(
|
||||
(await goto.innerText()).includes("Zebrastreifen"),
|
||||
"showing the note's current title",
|
||||
await goto.innerText()
|
||||
);
|
||||
|
||||
await goto.click();
|
||||
await p.waitForTimeout(2500);
|
||||
check(
|
||||
(await p.getByLabel("Note title").inputValue()) === "Zebrastreifen nachschlagen",
|
||||
"clicking it goes to the note",
|
||||
await p.getByLabel("Note title").inputValue()
|
||||
);
|
||||
|
||||
/* Back once more, and let go of the link — the note must survive that. */
|
||||
await p.locator("[data-note-row]").filter({ hasText: "e2e-test" }).first().click();
|
||||
await p.waitForTimeout(4000);
|
||||
await p.locator(`[data-pin-goto="${made.id}"]`).locator("xpath=ancestor::*[@data-pin-card][1]").click();
|
||||
await p.waitForTimeout(700);
|
||||
await p.locator("[data-pin-unlink]").click();
|
||||
await p.waitForTimeout(2500);
|
||||
check((await p.locator(`[data-pin-goto="${made.id}"]`).count()) === 0, "the link can be let go of");
|
||||
check(
|
||||
(await api(p, "GET", "/notes")).body.some((n) => n.id === made.id),
|
||||
"and the note itself is still there"
|
||||
);
|
||||
|
||||
/* ── on a phone there is no margin to write in ── */
|
||||
// A page narrow enough to read leaves no white space beside it, so taking a
|
||||
// column for notes would make the document itself unreadable. The note comes
|
||||
@@ -656,7 +725,12 @@ try {
|
||||
// backdrop up, which no real phone ever sees. Dismissed here so the rest of
|
||||
// the section is testing the document and not an overlay.
|
||||
if ((await p.locator("[data-pdf-page]").count()) === 0) {
|
||||
await p.evaluate(() => document.querySelector("[data-note-row]")?.click());
|
||||
// By title: the notebook now holds the note that grew out of a remark too,
|
||||
// and the first row is not necessarily the document.
|
||||
await p.evaluate(() => {
|
||||
const rows = [...document.querySelectorAll("[data-note-row]")];
|
||||
(rows.find((r) => r.textContent.includes("e2e-test")) || rows[0])?.click();
|
||||
});
|
||||
await p.waitForTimeout(4500);
|
||||
}
|
||||
await p.mouse.click(410, 800);
|
||||
|
||||
Reference in new issue
Block a user