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,113 @@
|
||||
/* Full-text search over locally held bodies, and image attachments.
|
||||
* Search runs in the client because the server cannot read encrypted notes, so
|
||||
* the interesting part is whether the backfill fills the index at all. */
|
||||
import {
|
||||
OUT,
|
||||
check,
|
||||
createNote,
|
||||
createNotebook,
|
||||
launch,
|
||||
report,
|
||||
setTitle,
|
||||
typeInEditor,
|
||||
withFreshUser,
|
||||
} from "./lib.mjs";
|
||||
|
||||
/** A 3x3 red PNG, small enough to paste inline. */
|
||||
const PNG =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAFElEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC";
|
||||
|
||||
/* Rows in the note list carry data-note-row; matching on the emoji instead would
|
||||
* also pick up the sidebar's Trash entry, which wears the same one. */
|
||||
const titles = (p) =>
|
||||
p.$$eval("[data-note-row]", (els) =>
|
||||
// The button holds emoji, title and date on separate lines.
|
||||
els.map((e) => e.innerText.split("\n").filter(Boolean)[1]).filter(Boolean)
|
||||
);
|
||||
|
||||
const browser = await launch();
|
||||
let session;
|
||||
try {
|
||||
session = await withFreshUser(browser, "search");
|
||||
const p = session.page;
|
||||
await createNotebook(p, "Such Buch");
|
||||
|
||||
const seed = [
|
||||
["Notiz A", "Der Kühlschrank braucht eine neue Dichtung."],
|
||||
["Notiz B", "Steuererklärung abgeben, Belege liegen im Ordner."],
|
||||
["Notiz C", "Ein völlig eigenständiges Stichwort: Zebrastreifen."],
|
||||
["Dichtung", "Hier steht das Wort nur im Titel."],
|
||||
];
|
||||
for (const [title, body] of seed) {
|
||||
await createNote(p);
|
||||
await setTitle(p, title);
|
||||
await typeInEditor(p, body);
|
||||
}
|
||||
console.log(`seeded ${seed.length} notes`);
|
||||
// Give the background backfill time to index the bodies.
|
||||
await p.waitForTimeout(3000);
|
||||
|
||||
const search = async (q) => {
|
||||
await p.getByPlaceholder("Search notes...").fill(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(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");
|
||||
await p.screenshot({ path: `${OUT}/search-results.png` });
|
||||
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1000);
|
||||
check((await titles(p)).length === seed.length, "clearing the box restores the full list");
|
||||
|
||||
/* ── attachments ── */
|
||||
await p.locator("button").filter({ hasText: "Notiz A" }).first().click();
|
||||
await p.waitForSelector(".ProseMirror", { timeout: 15000 });
|
||||
await p.waitForTimeout(1200);
|
||||
await p.locator(".ProseMirror").click();
|
||||
await p.keyboard.press("Control+End");
|
||||
|
||||
await p.evaluate(async (b64) => {
|
||||
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||
const file = new File([bytes], "dot.png", { type: "image/png" });
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
document
|
||||
.querySelector(".ProseMirror")
|
||||
.dispatchEvent(new ClipboardEvent("paste", { clipboardData: dt, bubbles: true, cancelable: true }));
|
||||
}, PNG);
|
||||
await p.waitForTimeout(3500);
|
||||
|
||||
const img = p.locator(".ProseMirror img").first();
|
||||
check((await p.locator(".ProseMirror img").count()) > 0, "the pasted image was inserted");
|
||||
const src = (await img.getAttribute("src")) || "";
|
||||
check(src.startsWith("blob:"), "the image renders from a blob URL, not inline bytes", src.slice(0, 24));
|
||||
check(!src.startsWith("data:"), "no base64 payload sits in the document");
|
||||
await p.screenshot({ path: `${OUT}/attachment.png` });
|
||||
|
||||
await p.reload({ waitUntil: "networkidle" });
|
||||
await p.waitForTimeout(2500);
|
||||
await p.locator("button").filter({ hasText: "Notiz A" }).first().click();
|
||||
await p.waitForSelector(".ProseMirror", { timeout: 15000 });
|
||||
await p.waitForTimeout(2500);
|
||||
check((await p.locator(".ProseMirror img").count()) > 0, "the image is still there after a reload");
|
||||
const loaded = await p
|
||||
.locator(".ProseMirror img")
|
||||
.first()
|
||||
.evaluate((e) => e.complete && e.naturalWidth > 0)
|
||||
.catch(() => false);
|
||||
check(loaded, "the image actually loaded its pixels after the reload");
|
||||
} 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