Put the interface into English, and give the notebook one way in
The interface had drifted into two languages — the frame in English, everything added later in German — which is worse than either, since the same idea appeared under two names depending on when that part was written. Sorting by title still uses the German collation, so Übung 2 comes before Übung 10: that is about the notes, not the chrome. Two archive folders were renamed with it (bilder/ → images/, zeichnungen/ → drawings/). The importer reads both spellings, and both the old and new marker line for a drawing — archives exported before this are on disk, and an import that quietly dropped their drawings would be the worst kind of tidy-up. Making a note, uploading a PDF and importing Markdown were three buttons side by side, two of them icons you had to hover to identify. They are the same act — putting something new in this notebook — so they are now one + menu. The search box moves out of the sidebar and into the note list, labelled with the notebook it narrows. Nothing changed about what it does: it has always searched only the notebook you are in, but from the sidebar it read as searching everything. A separator no longer carries a sentence explaining the order the sort menu already names. Renaming joins removing on the right-click menu — a left-click on the label started an edit, so the row could not be touched without falling into one. 218 unit tests, 238 browser checks across eleven suites. Two of those caught real breakage from this change rather than needing a translation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a7cf26b28
commit
52bd2b8f3d
27 files changed
+469
-336
No files matched your search
+11
-1
@@ -4,7 +4,17 @@
|
||||
* typed — but the state it buries has to survive as a version, or the other
|
||||
* device's work is simply gone. That is the whole point of the check, so this
|
||||
* verifies it end to end rather than trusting the route in isolation. */
|
||||
import { api, check, createNote, createNotebook, launch, report, setTitle, typeInEditor, withFreshUser } from "./lib.mjs";
|
||||
import {
|
||||
api,
|
||||
check,
|
||||
createNote,
|
||||
createNotebook,
|
||||
launch,
|
||||
report,
|
||||
setTitle,
|
||||
typeInEditor,
|
||||
withFreshUser,
|
||||
} from "./lib.mjs";
|
||||
|
||||
const browser = await launch();
|
||||
let session;
|
||||
|
||||
+5
-5
@@ -49,18 +49,18 @@ try {
|
||||
const before = await cellCount();
|
||||
await p.locator(".ProseMirror table td").first().click({ button: "right" });
|
||||
await p.waitForTimeout(600);
|
||||
check((await p.getByText("Zeile darunter einfügen").count()) > 0, "right-click in a table opens the table menu");
|
||||
check((await p.getByText("Tabelle löschen").count()) > 0, "and offers deleting the table");
|
||||
check((await p.getByText("Insert row below").count()) > 0, "right-click in a table opens the table menu");
|
||||
check((await p.getByText("Delete table").count()) > 0, "and offers deleting the table");
|
||||
await p.screenshot({ path: `${OUT}/editor-table-menu.png` });
|
||||
|
||||
await p.getByText("Zeile darunter einfügen").click();
|
||||
await p.getByText("Insert row below").click();
|
||||
await p.waitForTimeout(1000);
|
||||
check((await cellCount()) > before, "the menu really added a row", `${before} → ${await cellCount()}`);
|
||||
|
||||
const withRow = await cellCount();
|
||||
await p.locator(".ProseMirror table td").first().click({ button: "right" });
|
||||
await p.waitForTimeout(600);
|
||||
await p.getByText("Zeile löschen").click();
|
||||
await p.getByText("Delete row").click();
|
||||
await p.waitForTimeout(1000);
|
||||
check((await cellCount()) < withRow, "and removing one works too", `${withRow} → ${await cellCount()}`);
|
||||
|
||||
@@ -68,7 +68,7 @@ try {
|
||||
await p.locator(".ProseMirror p").first().click({ button: "right" });
|
||||
await p.waitForTimeout(500);
|
||||
check(
|
||||
(await p.getByText("Zeile darunter einfügen").count()) === 0,
|
||||
(await p.getByText("Insert row below").count()) === 0,
|
||||
"right-click outside a table shows no table menu"
|
||||
);
|
||||
await p.keyboard.press("Escape");
|
||||
|
||||
+10
-10
@@ -6,11 +6,11 @@
|
||||
* can be unpacked and read here. */
|
||||
import {
|
||||
OUT,
|
||||
captureDownloads,
|
||||
check,
|
||||
clearDownloads,
|
||||
createNote,
|
||||
createNotebook,
|
||||
captureDownloads,
|
||||
clearDownloads,
|
||||
downloads,
|
||||
insertTable,
|
||||
lastDownloadName,
|
||||
@@ -148,7 +148,7 @@ try {
|
||||
const zip = readZip(new Uint8Array(Buffer.from(files[0].base64, "base64")));
|
||||
const names = Object.keys(zip);
|
||||
check(names.some((n) => n.endsWith(".md")), "the archive holds the Markdown file", names.join(", "));
|
||||
check(names.some((n) => /bilder\/.*\.png$/.test(n)), "and the image beside it", names.join(", "));
|
||||
check(names.some((n) => /images\/.*\.png$/.test(n)), "and the image beside it", names.join(", "));
|
||||
|
||||
await p.keyboard.press("Escape");
|
||||
await p.waitForTimeout(600);
|
||||
@@ -170,24 +170,24 @@ try {
|
||||
await clearDownloads(p);
|
||||
await openExportDialog(p);
|
||||
check(
|
||||
(await p.getByText("Was soll mit?").count()) > 0,
|
||||
(await p.getByText("What should come along?").count()) > 0,
|
||||
"a note with a drawing is asked what should come along"
|
||||
);
|
||||
check(
|
||||
(await p.getByRole("button", { name: /Nur die Zeichnung/ }).count()) > 0,
|
||||
(await p.getByRole("button", { name: /Only the drawing/ }).count()) > 0,
|
||||
"the drawing-only option is offered"
|
||||
);
|
||||
await p.screenshot({ path: `${OUT}/export-wizard.png` });
|
||||
|
||||
// drawing only → PNG
|
||||
await p.getByRole("button", { name: /Nur die Zeichnung/ }).click();
|
||||
await p.getByRole("button", { name: /Only the drawing/ }).click();
|
||||
await p.waitForTimeout(500);
|
||||
check((await p.getByRole("button", { name: /PNG-Bild/ }).count()) > 0, "PNG is offered for the drawing");
|
||||
check((await p.getByRole("button", { name: /PNG image/ }).count()) > 0, "PNG is offered for the drawing");
|
||||
check(
|
||||
(await p.locator("[data-export-dialog]").getByRole("button", { name: /Markdown/ }).count()) === 0,
|
||||
"Markdown is not offered for a drawing, since it cannot hold one"
|
||||
);
|
||||
await p.getByRole("button", { name: /PNG-Bild/ }).click();
|
||||
await p.getByRole("button", { name: /PNG image/ }).click();
|
||||
await p.waitForSelector("[data-export-done]", { timeout: 30000 });
|
||||
await p.waitForTimeout(800);
|
||||
|
||||
@@ -203,9 +203,9 @@ try {
|
||||
check(png.length > 1000, "the image has actual content, not an empty canvas", `${png.length} bytes`);
|
||||
|
||||
// back, then text only → Markdown must carry no drawing
|
||||
await p.getByRole("button", { name: /Zurück/ }).click();
|
||||
await p.getByRole("button", { name: /Back/ }).click();
|
||||
await p.waitForTimeout(500);
|
||||
await p.getByRole("button", { name: /Nur den Notiztext/ }).click();
|
||||
await p.getByRole("button", { name: /Only the note text/ }).click();
|
||||
await p.waitForTimeout(500);
|
||||
await clearDownloads(p);
|
||||
await p.locator("[data-export-dialog]").getByRole("button", { name: /Markdown/ }).click();
|
||||
|
||||
+19
-1
@@ -171,12 +171,30 @@ export async function createNotebook(page, name) {
|
||||
await page.waitForTimeout(1200);
|
||||
}
|
||||
|
||||
/** Making a note, importing a PDF and importing Markdown share one + menu. */
|
||||
export async function openAddMenu(page) {
|
||||
await page.locator("[data-add-menu]").click();
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
export async function createNote(page) {
|
||||
await page.getByTitle("New Note").click();
|
||||
await openAddMenu(page);
|
||||
await page.locator('[data-add-option="New note"]').click();
|
||||
await page.waitForSelector(".ProseMirror", { timeout: 15000 });
|
||||
await page.waitForTimeout(1200);
|
||||
}
|
||||
|
||||
/** Opens the search box in the note list and types into it. */
|
||||
export async function search(page, query) {
|
||||
const box = page.locator('input[aria-label="Search in this notebook"]');
|
||||
if ((await box.count()) === 0) {
|
||||
await page.locator("[data-search-toggle]").click();
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
await box.fill(query);
|
||||
await page.waitForTimeout(1300);
|
||||
}
|
||||
|
||||
/** The sidebar search box is also a text input, so the title needs its own handle. */
|
||||
export async function setTitle(page, title) {
|
||||
await page.getByLabel("Note title").fill(title);
|
||||
|
||||
+3
-4
@@ -16,6 +16,7 @@ import {
|
||||
createNotebook,
|
||||
launch,
|
||||
report,
|
||||
search,
|
||||
setTitle,
|
||||
typeInEditor,
|
||||
withFreshUser,
|
||||
@@ -114,12 +115,10 @@ try {
|
||||
|
||||
/* Searching is local by design — the server holds only ciphertext — so it has
|
||||
to keep working when the server is not there at all. */
|
||||
await p.getByPlaceholder("Search notes...").fill("Kaffee");
|
||||
await p.waitForTimeout(1500);
|
||||
await search(p, "Kaffee");
|
||||
const hits = await p.locator("[data-note-row]").count();
|
||||
check(hits === 1, "search still finds a note by its text, offline", `${hits} hits`);
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(800);
|
||||
await search(p, "");
|
||||
|
||||
/* Writing offline is queued, not lost. */
|
||||
await p.locator("[data-note-row]").filter({ hasText: "Einkauf" }).first().click();
|
||||
|
||||
+26
-18
@@ -12,7 +12,9 @@ import {
|
||||
createNote,
|
||||
createNotebook,
|
||||
launch,
|
||||
openAddMenu,
|
||||
report,
|
||||
search,
|
||||
setTitle,
|
||||
typeInEditor,
|
||||
withFreshUser,
|
||||
@@ -106,13 +108,11 @@ try {
|
||||
);
|
||||
|
||||
/* The point of the whole feature. */
|
||||
await p.getByPlaceholder("Search notes...").fill("Beta");
|
||||
await p.waitForTimeout(1200);
|
||||
await search(p, "Beta");
|
||||
const searched = await titles();
|
||||
check(searched.length < 3, "searching narrows the list", searched.join(", "));
|
||||
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1500);
|
||||
await search(p, "");
|
||||
check(
|
||||
(await titles()).join(",") === arranged.join(","),
|
||||
"and clearing the search shows exactly the arrangement again",
|
||||
@@ -150,15 +150,21 @@ try {
|
||||
await p.waitForTimeout(1800);
|
||||
check((await p.locator("[data-separator]").count()) === 1, "a separator can be added");
|
||||
|
||||
/* Renaming is right-click, like removing. A left-click on the label used to
|
||||
start an edit, so the row could not be touched without falling into one. */
|
||||
const label = p.getByLabel("Separator label");
|
||||
await p.locator("[data-separator] button").first().click();
|
||||
await p.waitForTimeout(300);
|
||||
check((await label.count()) === 0, "clicking a separator does not drop straight into renaming");
|
||||
|
||||
await p.locator("[data-separator]").click({ button: "right" });
|
||||
await p.waitForTimeout(600);
|
||||
await p.getByText("Rename", { exact: true }).click();
|
||||
await p.waitForTimeout(500);
|
||||
await label.fill("Wichtig");
|
||||
await label.press("Enter");
|
||||
await p.waitForTimeout(1800);
|
||||
check(
|
||||
/wichtig/i.test(await p.locator("[data-separator]").innerText()),
|
||||
"and named",
|
||||
"and it can be named from the same menu",
|
||||
await p.locator("[data-separator]").innerText()
|
||||
);
|
||||
|
||||
@@ -197,14 +203,14 @@ try {
|
||||
|
||||
await p.locator("[data-separator]").click({ button: "right" });
|
||||
await p.waitForTimeout(600);
|
||||
await p.getByText("Trenner entfernen").click();
|
||||
await p.getByText("Remove separator").click();
|
||||
await p.waitForTimeout(600);
|
||||
check(
|
||||
/Notizen bleiben/.test(await p.locator("body").innerText()),
|
||||
/notes stay/i.test(await p.locator("body").innerText()),
|
||||
"right-clicking it asks first, and says the notes are not affected"
|
||||
);
|
||||
|
||||
await p.getByRole("button", { name: "Entfernen" }).click();
|
||||
await p.getByRole("button", { name: "Remove" }).click();
|
||||
await p.waitForTimeout(2000);
|
||||
check((await p.locator("[data-separator]").count()) === 0, "and then it is gone");
|
||||
check((await titles()).length === 3, "with every note still there", (await titles()).join(", "));
|
||||
@@ -251,7 +257,7 @@ try {
|
||||
check(markdown.includes("- [x] erledigt"), "the export carries the ticked box", JSON.stringify(markdown.slice(-60)));
|
||||
check(markdown.startsWith("# Beta"), "and the title as a heading");
|
||||
|
||||
await p.locator("[data-export-dialog] button").filter({ hasText: /Schließen|Fertig|Close/ }).first().click().catch(() => {});
|
||||
await p.locator("[data-export-dialog] button").filter({ hasText: /Close|Done/ }).first().click().catch(() => {});
|
||||
await p.keyboard.press("Escape");
|
||||
await p.waitForTimeout(600);
|
||||
|
||||
@@ -270,14 +276,15 @@ try {
|
||||
}, markdown);
|
||||
|
||||
const chooser = p.waitForEvent("filechooser");
|
||||
await p.locator("[data-import-button]").click();
|
||||
await openAddMenu(p);
|
||||
await p.locator('[data-add-option="Import Markdown"]').click();
|
||||
const fc = await chooser;
|
||||
await fc.setFiles({ name: "Beta.md", mimeType: "text/markdown", buffer: Buffer.from(markdown) });
|
||||
await p.waitForTimeout(4000);
|
||||
|
||||
const importedText = await p.locator("body").innerText();
|
||||
check(/importiert/i.test(importedText), "the import reports what it did", importedText.slice(0, 200).replace(/\n/g, " "));
|
||||
await p.getByRole("button", { name: /Fertig/ }).click();
|
||||
check(/imported/i.test(importedText), "the import reports what it did", importedText.slice(0, 200).replace(/\n/g, " "));
|
||||
await p.getByRole("button", { name: /Done/ }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
const after = (await api(p, "GET", "/notes")).body.length;
|
||||
@@ -307,7 +314,7 @@ try {
|
||||
check(await field.isVisible(), "the link dialog is a real dialog in the page");
|
||||
|
||||
await field.fill("javascript:alert(1)");
|
||||
await p.getByRole("button", { name: /Einfügen/ }).click();
|
||||
await p.getByRole("button", { name: /^Insert$/ }).click();
|
||||
await p.waitForTimeout(400);
|
||||
check(
|
||||
await field.isVisible(),
|
||||
@@ -315,7 +322,7 @@ try {
|
||||
);
|
||||
|
||||
await field.fill("example.com");
|
||||
await p.getByRole("button", { name: /Einfügen/ }).click();
|
||||
await p.getByRole("button", { name: /^Insert$/ }).click();
|
||||
await p.waitForTimeout(1800);
|
||||
const href = await p.locator(".ProseMirror a[href]").first().getAttribute("href");
|
||||
check(href === "https://example.com", "a bare domain is taken as https", String(href));
|
||||
@@ -341,7 +348,8 @@ try {
|
||||
await p.keyboard.press("Escape");
|
||||
await p.locator("h2").first().click();
|
||||
await p.waitForTimeout(500);
|
||||
await p.getByTitle("New Note").click();
|
||||
await openAddMenu(p);
|
||||
await p.locator('[data-add-option="New note"]').click();
|
||||
await p.waitForTimeout(900);
|
||||
|
||||
const books0 = (await api(p, "GET", "/notebooks")).body;
|
||||
@@ -352,7 +360,7 @@ try {
|
||||
// sort order, so "the last one" is not a stable way to mean a particular book.
|
||||
const target = books0.find((b) => b.name === "Zweites Buch");
|
||||
await p.locator(`[data-notebook-option="${target.id}"]`).click();
|
||||
await p.getByRole("button", { name: /Weiter/ }).click();
|
||||
await p.getByRole("button", { name: /Continue/ }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
// By title, not by position: the list is ordered by a timestamp with one
|
||||
|
||||
+28
-32
@@ -15,7 +15,9 @@ import {
|
||||
downloads,
|
||||
lastDownloadName,
|
||||
launch,
|
||||
openAddMenu,
|
||||
report,
|
||||
search,
|
||||
withFreshUser,
|
||||
} from "./lib.mjs";
|
||||
|
||||
@@ -115,14 +117,15 @@ try {
|
||||
|
||||
/* ── import ── */
|
||||
const chooser = p.waitForEvent("filechooser");
|
||||
await p.getByTitle("Import PDF").click();
|
||||
await openAddMenu(p);
|
||||
await p.locator('[data-add-option="Upload PDF"]').click();
|
||||
await (await chooser).setFiles(PDF_PATH);
|
||||
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"),
|
||||
(await p.locator("[data-pdf-position]").innerText()).includes("of 2"),
|
||||
"both pages are counted",
|
||||
await p.locator("[data-pdf-position]").innerText()
|
||||
);
|
||||
@@ -188,18 +191,15 @@ try {
|
||||
|
||||
/* ── the document is findable ── */
|
||||
await p.waitForTimeout(2500); // extraction runs after the first page is shown
|
||||
await p.getByPlaceholder("Search notes...").fill("Zebrastreifen");
|
||||
await p.waitForTimeout(1500);
|
||||
await search(p, "Zebrastreifen");
|
||||
check(
|
||||
(await p.locator("[data-note-row]").count()) === 1,
|
||||
"searching a word from inside the PDF finds the note",
|
||||
`${await p.locator("[data-note-row]").count()} rows`
|
||||
);
|
||||
await p.getByPlaceholder("Search notes...").fill("Wortdasnichtvorkommt");
|
||||
await p.waitForTimeout(1200);
|
||||
await search(p, "Wortdasnichtvorkommt");
|
||||
check((await p.locator("[data-note-row]").count()) === 0, "and a word that is not in it finds nothing");
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1000);
|
||||
await search(p, "");
|
||||
await p.locator("[data-note-row]").first().click();
|
||||
await p.waitForTimeout(3000);
|
||||
|
||||
@@ -262,12 +262,12 @@ try {
|
||||
}, n);
|
||||
|
||||
const before = await inkBounds(1);
|
||||
await p.getByTitle("Größer").click();
|
||||
await p.getByTitle("Zoom in").click();
|
||||
await p.waitForTimeout(2500);
|
||||
const after = await inkBounds(1);
|
||||
const drift = Math.max(Math.abs(before.x - after.x), Math.abs(before.y - after.y), Math.abs(before.w - after.w));
|
||||
check(drift < 0.02, "the stroke stays on the same spot when zoomed", `drift ${drift.toFixed(3)}`);
|
||||
await p.getByTitle("Kleiner").click();
|
||||
await p.getByTitle("Zoom out").click();
|
||||
await p.waitForTimeout(1500);
|
||||
|
||||
/* ── hiding it ── */
|
||||
@@ -325,11 +325,9 @@ try {
|
||||
await p.screenshot({ path: "/out/pdf-with-notes.png" });
|
||||
|
||||
/* And they are searchable, alongside the document's own text. */
|
||||
await p.getByPlaceholder("Search notes...").fill("Randbemerkung");
|
||||
await p.waitForTimeout(1500);
|
||||
await search(p, "Randbemerkung");
|
||||
check((await p.locator("[data-note-row]").count()) === 1, "notes written beside a PDF are searchable");
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1000);
|
||||
await search(p, "");
|
||||
await p.locator("[data-note-row]").first().click();
|
||||
await p.waitForTimeout(3000);
|
||||
|
||||
@@ -345,14 +343,14 @@ try {
|
||||
|
||||
await openExport();
|
||||
check(
|
||||
(await p.getByText("Was soll heraus?").count()) > 0,
|
||||
(await p.getByText("What should come out?").count()) > 0,
|
||||
"a PDF note is asked what should come out, not offered note formats"
|
||||
);
|
||||
await p.screenshot({ path: "/out/pdf-export-wizard.png" });
|
||||
|
||||
/* The untouched original. */
|
||||
await clearDownloads(p);
|
||||
await p.getByRole("button", { name: /PDF im Original/ }).click();
|
||||
await p.getByRole("button", { name: /The original PDF/ }).click();
|
||||
await p.waitForSelector("[data-export-done]", { timeout: 30000 });
|
||||
await p.waitForTimeout(900);
|
||||
let files = await downloads(p);
|
||||
@@ -369,7 +367,7 @@ try {
|
||||
/* The same document with the strokes written in. */
|
||||
await openExport();
|
||||
await clearDownloads(p);
|
||||
await p.getByRole("button", { name: /PDF mit deinen Anmerkungen/ }).click();
|
||||
await p.getByRole("button", { name: /PDF with your annotations/ }).click();
|
||||
await p.waitForSelector("[data-export-done]", { timeout: 30000 });
|
||||
await p.waitForTimeout(1500);
|
||||
files = await downloads(p);
|
||||
@@ -378,7 +376,7 @@ try {
|
||||
check(stamped.subarray(0, 5).toString() === "%PDF-", "the annotated file is a PDF", stamped.subarray(0, 8).toString());
|
||||
check(stamped.length > originalBytes.length, "and carries more than the original did", `${stamped.length} vs ${originalBytes.length}`);
|
||||
check(!stamped.equals(originalBytes), "so it is not simply a copy");
|
||||
check(/mit Notizen/.test(await lastDownloadName(p)), "named so the two cannot be confused", await lastDownloadName(p));
|
||||
check(/annotated/.test(await lastDownloadName(p)), "named so the two cannot be confused", await lastDownloadName(p));
|
||||
|
||||
/* The stored file must still be the original — drawing on it changes nothing
|
||||
on disk, or a clean copy could never be had again. */
|
||||
@@ -502,7 +500,7 @@ try {
|
||||
check(levels.cardLeft >= levels.pageRight - 2, "and beside the page rather than over it");
|
||||
|
||||
await field.fill("Drehmoment beachten");
|
||||
await p.getByRole("button", { name: "Fertig" }).click();
|
||||
await p.getByRole("button", { name: "Done" }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
/* It has to be on the passage, not near it — so the position is checked
|
||||
@@ -520,7 +518,7 @@ try {
|
||||
|
||||
/* Stored in PDF points, so zooming must not move it off its passage — the same
|
||||
property the strokes have. */
|
||||
await p.getByTitle("Größer").click();
|
||||
await p.getByTitle("Zoom in").click();
|
||||
await p.waitForTimeout(2000);
|
||||
const zoomed = await p.evaluate(() => {
|
||||
const pin = document.querySelector("[data-pin]").getBoundingClientRect();
|
||||
@@ -532,20 +530,18 @@ try {
|
||||
"and stays there when the document is zoomed",
|
||||
`${zoomed.x.toFixed(2)} / ${zoomed.y.toFixed(2)}`
|
||||
);
|
||||
await p.getByTitle("Kleiner").click();
|
||||
await p.getByTitle("Zoom out").click();
|
||||
await p.waitForTimeout(1200);
|
||||
|
||||
/* Searchable: it lives in the page overlay, not in the note body, so nothing
|
||||
else would ever index it. */
|
||||
await p.getByPlaceholder("Search notes...").fill("Drehmoment");
|
||||
await p.waitForTimeout(1500);
|
||||
await search(p, "Drehmoment");
|
||||
check(
|
||||
(await p.locator("[data-note-row]").count()) === 1,
|
||||
"a pinned note can be searched for",
|
||||
String(await p.locator("[data-note-row]").count())
|
||||
);
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1000);
|
||||
await search(p, "");
|
||||
|
||||
/* It belongs to the note, so it has to come back from the server. */
|
||||
await p.reload({ waitUntil: "networkidle" });
|
||||
@@ -575,7 +571,7 @@ try {
|
||||
await p.waitForTimeout(400);
|
||||
await p.locator("[data-pdf-mode]").click();
|
||||
await p.waitForTimeout(500);
|
||||
await p.getByTitle("Seite leeren").click();
|
||||
await p.getByTitle("Clear page").click();
|
||||
await p.waitForTimeout(2000);
|
||||
check((await p.locator("[data-pin]").count()) === 1, "emptying the page's ink leaves the pinned note");
|
||||
await p.locator("[data-pdf-mode]").click();
|
||||
@@ -595,7 +591,7 @@ try {
|
||||
await p.mouse.click(page1b.x + page1b.width * 0.75, page1b.y + page1b.height * 0.7);
|
||||
await p.waitForTimeout(700);
|
||||
await p.getByLabel("Pinned note").fill("Randbemerkung Zebrastreifen");
|
||||
await p.getByRole("button", { name: "Fertig" }).click();
|
||||
await p.getByRole("button", { name: "Done" }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
/* The server keeps its own judgement of "is this overlay empty". It counted
|
||||
@@ -618,7 +614,7 @@ try {
|
||||
await p.getByTitle("Export").click();
|
||||
await p.waitForSelector("[data-export-dialog]", { timeout: 10000 });
|
||||
await p.waitForTimeout(800);
|
||||
await p.getByRole("button", { name: /PDF mit deinen Anmerkungen/ }).click();
|
||||
await p.getByRole("button", { name: /PDF with your annotations/ }).click();
|
||||
await p.waitForSelector("[data-export-done]", { timeout: 30000 });
|
||||
await p.waitForTimeout(1500);
|
||||
const withPins = Buffer.from((await downloads(p))[0].base64, "base64");
|
||||
@@ -643,7 +639,7 @@ try {
|
||||
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.getByRole("button", { name: "Fertig" }).click();
|
||||
await p.getByRole("button", { name: "Done" }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
await p.locator("[data-pdf-toggle-notes]").click();
|
||||
@@ -671,7 +667,7 @@ try {
|
||||
await linkEl.getAttribute("data-kind")
|
||||
);
|
||||
check(
|
||||
(await linkEl.innerText()).includes("S. 1"),
|
||||
(await linkEl.innerText()).includes("p. 1"),
|
||||
"labelled with the page it points at",
|
||||
await linkEl.innerText()
|
||||
);
|
||||
@@ -788,7 +784,7 @@ try {
|
||||
await p.mouse.click(pageM.x + pageM.width * 0.5, pageM.y + pageM.height * 0.4);
|
||||
await p.waitForTimeout(800);
|
||||
await p.getByLabel("Pinned note").fill("Am Rand notiert");
|
||||
await p.getByRole("button", { name: "Fertig" }).click();
|
||||
await p.getByRole("button", { name: "Done" }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
const wideMargins = await p.locator("[data-pdf-margin]").count();
|
||||
@@ -865,7 +861,7 @@ try {
|
||||
`top ${Math.round(sheet.y)} of 820`
|
||||
);
|
||||
|
||||
await p.getByRole("button", { name: "Fertig" }).click();
|
||||
await p.getByRole("button", { name: "Done" }).click();
|
||||
await p.waitForTimeout(800);
|
||||
check((await p.locator("[data-pin-sheet]").count()) === 0, "and closes again");
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createNotebook,
|
||||
launch,
|
||||
report,
|
||||
search,
|
||||
setTitle,
|
||||
typeInEditor,
|
||||
withFreshUser,
|
||||
@@ -47,23 +48,23 @@ try {
|
||||
// Give the background backfill time to index the bodies.
|
||||
await p.waitForTimeout(3000);
|
||||
|
||||
const search = async (q) => {
|
||||
await p.getByPlaceholder("Search notes...").fill(q);
|
||||
/** Types into the note list's search box and reports what is left showing. */
|
||||
const find = async (q) => {
|
||||
await search(p, q);
|
||||
await p.waitForTimeout(1200);
|
||||
return titles(p);
|
||||
};
|
||||
|
||||
check(JSON.stringify(await search("Zebrastreifen")) === '["Notiz C"]', "a word only in the body finds its note", JSON.stringify(await titles(p)));
|
||||
const hits = await search("Dichtung");
|
||||
check(JSON.stringify(await find("Zebrastreifen")) === '["Notiz C"]', "a word only in the body finds its note", JSON.stringify(await titles(p)));
|
||||
const hits = await find("Dichtung");
|
||||
check(hits.length === 2, "a word in one title and another body finds both", JSON.stringify(hits));
|
||||
check(hits[0] === "Dichtung", "the title match ranks first", JSON.stringify(hits));
|
||||
check(JSON.stringify(await search("Belege Ordner")) === '["Notiz B"]', "multiple terms are an AND");
|
||||
check((await search("Belege Kühlschrank")).length === 0, "terms from different notes match nothing");
|
||||
check(JSON.stringify(await search("kuhlschrank")) === '["Notiz A"]', "search ignores case and umlauts");
|
||||
check(JSON.stringify(await find("Belege Ordner")) === '["Notiz B"]', "multiple terms are an AND");
|
||||
check((await find("Belege Kühlschrank")).length === 0, "terms from different notes match nothing");
|
||||
check(JSON.stringify(await find("kuhlschrank")) === '["Notiz A"]', "search ignores case and umlauts");
|
||||
await p.screenshot({ path: `${OUT}/search-results.png` });
|
||||
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1000);
|
||||
await search(p, "");
|
||||
check((await titles(p)).length === seed.length, "clearing the box restores the full list");
|
||||
|
||||
/* ── attachments ── */
|
||||
|
||||
Reference in new issue
Block a user