import { describe, it, expect, beforeEach } from "vitest"; import * as search from "./search"; import { serializeNoteContent } from "./editor/content"; const body = (text: string) => serializeNoteContent({ type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text }] }], }); const seed = () => { search.removeEntry("a"); search.removeEntry("b"); search.removeEntry("c"); search.setEntry("a", "Haushalt", body("Der Kühlschrank braucht eine neue Dichtung.")); search.setEntry("b", "Finanzen", body("Steuererklärung abgeben, Belege liegen im Ordner.")); search.setEntry("c", "Dichtung", body("Nur der Titel enthält das Wort.")); }; beforeEach(seed); describe("search", () => { it("returns null for an empty query, meaning 'no search'", () => { expect(search.search("")).toBeNull(); expect(search.search(" ")).toBeNull(); }); it("finds a word that appears only in the body", () => { const hits = search.search("Belege"); expect(hits?.map((h) => h.id)).toEqual(["b"]); }); it("ranks a title match above a body match", () => { const hits = search.search("Dichtung"); expect(hits?.map((h) => h.id)).toEqual(["c", "a"]); }); it("requires every term to match", () => { expect(search.search("Belege Ordner")?.map((h) => h.id)).toEqual(["b"]); expect(search.search("Belege Kühlschrank")).toEqual([]); }); it("ignores case", () => { expect(search.search("BELEGE")?.map((h) => h.id)).toEqual(["b"]); }); it("ignores diacritics, so an ASCII keyboard still finds umlauts", () => { expect(search.search("kuhlschrank")?.map((h) => h.id)).toEqual(["a"]); expect(search.search("Steuererklarung")?.map((h) => h.id)).toEqual(["b"]); }); it("returns a snippet around the match", () => { const hit = search.search("Dichtung")?.find((h) => h.id === "a"); expect(hit?.snippet).toMatch(/Dichtung/); }); it("respects the result limit", () => { for (let i = 0; i < 30; i++) search.setEntry(`x${i}`, `Note ${i}`, body("common word")); expect(search.search("common", { limit: 5 })?.length).toBe(5); for (let i = 0; i < 30; i++) search.removeEntry(`x${i}`); }); it("stops matching a note once it is removed from the index", () => { expect(search.search("Belege")?.length).toBe(1); search.removeEntry("b"); expect(search.search("Belege")).toEqual([]); }); it("reflects an updated body immediately", () => { search.setEntry("a", "Haushalt", body("Völlig anderer Inhalt jetzt.")); expect(search.search("Kühlschrank")).toEqual([]); expect(search.search("anderer")?.map((h) => h.id)).toEqual(["a"]); }); }); describe("contentToText", () => { it("reads a v2 document", () => { expect(search.contentToText(body("hello world"))).toBe("hello world"); }); it("reads a v1 body, HTML and task list alike", () => { const v1 = JSON.stringify({ html: "

legacy text

", tasks: [{ id: "t", text: "buy milk", done: false }], }); const text = search.contentToText(v1); expect(text).toContain("legacy"); expect(text).toContain("buy milk"); }); it("does not execute or leak markup from a v1 body", () => { const text = search.contentToText(JSON.stringify({ html: "

safe

" })); expect(text).toContain("safe"); expect(text).not.toContain("