From 151a2c284d2e8b32867c3e4fe46be7d39fedae19 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 17:12:01 +0300 Subject: [PATCH] test(server/note_map): backlinks with excerpts --- apps/server/src/routes/api/note_map.spec.ts | 99 +++++++++++++++++++++ apps/server/src/routes/api/note_map.ts | 2 +- apps/server/src/test/becca_easy_mocking.ts | 92 +++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/routes/api/note_map.spec.ts create mode 100644 apps/server/src/test/becca_easy_mocking.ts diff --git a/apps/server/src/routes/api/note_map.spec.ts b/apps/server/src/routes/api/note_map.spec.ts new file mode 100644 index 000000000..c3696fc3a --- /dev/null +++ b/apps/server/src/routes/api/note_map.spec.ts @@ -0,0 +1,99 @@ +import { trimIndentation } from "@triliumnext/commons"; +import { buildNote, buildNotes } from "../../test/becca_easy_mocking"; +import note_map from "./note_map"; + +describe("Note map service", () => { + it("correctly identifies backlinks", () => { + const note = buildNote({ id: "dUtgloZIckax", title: "Backlink text" }); + buildNotes([ + { + title: "First", + id: "first", + "~internalLink": "dUtgloZIckax", + content: trimIndentation`\ +

+ The quick brownie +

+

+ + Backlink text + +

+
+ +
+ ` + }, + { + title: "Second", + id: "second", + "~internalLink": "dUtgloZIckax", + content: trimIndentation`\ +

+ + Backlink text + +

+

+ + First + +

+

+ + Second + +

+ ` + } + ]); + + const backlinksResponse = note_map.getBacklinks({ + params: { + noteId: note.noteId + } + } as any); + expect(backlinksResponse).toMatchObject([ + { + excerpts: [ + trimIndentation`\ + ` + ], + noteId: "first", + }, + { + excerpts: [ + trimIndentation`\ + ` + ], + noteId: "second" + } + ]); + }); +}); diff --git a/apps/server/src/routes/api/note_map.ts b/apps/server/src/routes/api/note_map.ts index 3a2bed5b2..616724960 100644 --- a/apps/server/src/routes/api/note_map.ts +++ b/apps/server/src/routes/api/note_map.ts @@ -251,7 +251,7 @@ function removeImages(document: Document) { const EXCERPT_CHAR_LIMIT = 200; type ElementOrText = Element | Text; -function findExcerpts(sourceNote: BNote, referencedNoteId: string) { +export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { const html = sourceNote.getContent(); const document = new JSDOM(html).window.document; diff --git a/apps/server/src/test/becca_easy_mocking.ts b/apps/server/src/test/becca_easy_mocking.ts new file mode 100644 index 000000000..579ded515 --- /dev/null +++ b/apps/server/src/test/becca_easy_mocking.ts @@ -0,0 +1,92 @@ +import utils from "../services/utils.js"; +import BNote from "../becca/entities/bnote.js"; +import BAttribute from "../becca/entities/battribute.js"; + +type AttributeDefinitions = { [key in `#${string}`]: string; }; +type RelationDefinitions = { [key in `~${string}`]: string; }; + +interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { + id?: string | undefined; + title: string; + content?: string; +} + +/** + * Creates the given notes with the given title and optionally one or more attributes. + * + * For a label to be created, simply pass on a key prefixed with `#` and any desired value. + * + * The notes and attributes will be injected in the froca. + * + * @param notes + * @returns an array containing the IDs of the created notes. + * @example + * buildNotes([ + * { title: "A", "#startDate": "2025-05-05" }, + * { title: "B", "#startDate": "2025-05-07" } + * ]); + */ +export function buildNotes(notes: NoteDefinition[]) { + const ids: string[] = []; + + for (const noteDef of notes) { + ids.push(buildNote(noteDef).noteId); + } + + return ids; +} + +export function buildNote(noteDef: NoteDefinition) { + const note = new BNote({ + noteId: noteDef.id ?? utils.randomString(12), + title: noteDef.title, + type: "text", + mime: "text/html", + isProtected: false, + blobId: "" + }); + + // Handle content. + if (noteDef.content) { + note.getContent = () => noteDef.content!; + } + + // Handle labels and relations. + let position = 0; + for (const [ key, value ] of Object.entries(noteDef)) { + const attributeId = utils.randomString(12); + const name = key.substring(1); + + let attribute: BAttribute | null = null; + if (key.startsWith("#")) { + attribute = new BAttribute({ + noteId: note.noteId, + attributeId, + type: "label", + name, + value, + position, + isInheritable: false + }); + } + + if (key.startsWith("~")) { + attribute = new BAttribute({ + noteId: note.noteId, + attributeId, + type: "relation", + name, + value, + position, + isInheritable: false + }); + } + + if (!attribute) { + continue; + } + + position++; + } + return note; +}