/* 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); }