You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
9.4 KiB
320 lines
9.4 KiB
import { beforeEach, describe, expect, it, vi } from "vitest"; |
|
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
|
import type NDK from "@nostr-dev-kit/ndk"; |
|
import { fetchHighlightsForPublication } from "../../src/lib/utils/fetch_publication_highlights.ts"; |
|
|
|
// Mock NDKEvent class |
|
class MockNDKEvent { |
|
kind: number; |
|
pubkey: string; |
|
content: string; |
|
tags: string[][]; |
|
created_at: number; |
|
id: string; |
|
sig: string; |
|
|
|
constructor(event: { |
|
kind: number; |
|
pubkey: string; |
|
content: string; |
|
tags: string[][]; |
|
created_at?: number; |
|
id?: string; |
|
sig?: string; |
|
}) { |
|
this.kind = event.kind; |
|
this.pubkey = event.pubkey; |
|
this.content = event.content; |
|
this.tags = event.tags; |
|
this.created_at = event.created_at || Date.now() / 1000; |
|
this.id = event.id || "mock-id"; |
|
this.sig = event.sig || "mock-sig"; |
|
} |
|
|
|
getMatchingTags(tagName: string): string[][] { |
|
return this.tags.filter((tag) => tag[0] === tagName); |
|
} |
|
|
|
tagValue(tagName: string): string | undefined { |
|
const tag = this.tags.find((tag) => tag[0] === tagName); |
|
return tag ? tag[1] : undefined; |
|
} |
|
} |
|
|
|
describe("fetchHighlightsForPublication", () => { |
|
let mockNDK: NDK; |
|
let publicationEvent: NDKEvent; |
|
let mockHighlights: MockNDKEvent[]; |
|
|
|
beforeEach(() => { |
|
// Create the sample 30040 publication event from the user's example |
|
publicationEvent = new MockNDKEvent({ |
|
kind: 30040, |
|
pubkey: |
|
"fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", |
|
content: "", |
|
tags: [ |
|
["d", "document-test"], |
|
["title", "Document Test"], |
|
["author", "unknown"], |
|
["version", "1"], |
|
["m", "application/json"], |
|
["M", "meta-data/index/replaceable"], |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
], |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:another-first-level-heading", |
|
], |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:a-third-first-level-heading", |
|
], |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:asciimath-test-document", |
|
], |
|
["t", "a-tags"], |
|
["t", "testfile"], |
|
["t", "asciimath"], |
|
["t", "latexmath"], |
|
["image", "https://i.nostr.build/5kWwbDR04joIASVx.png"], |
|
], |
|
created_at: 1744910311, |
|
id: "4585ed74a0be37655aa887340d239f0bbb9df5476165d912f098c55a71196fef", |
|
sig: |
|
"e6a832dcfc919c913acee62cb598211544bc8e03a3f61c016eb3bf6c8cb4fb333eff8fecc601517604c7a8029dfa73591f3218465071a532f4abfe8c0bf3662d", |
|
}) as unknown as NDKEvent; |
|
|
|
// Create mock highlight events for different sections |
|
mockHighlights = [ |
|
new MockNDKEvent({ |
|
kind: 9802, |
|
pubkey: "user-pubkey-1", |
|
content: "This is an interesting point", |
|
tags: [ |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
], |
|
["context", "surrounding text here"], |
|
[ |
|
"p", |
|
"fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", |
|
"", |
|
"author", |
|
], |
|
], |
|
id: "highlight-1", |
|
}), |
|
new MockNDKEvent({ |
|
kind: 9802, |
|
pubkey: "user-pubkey-2", |
|
content: "Another highlight on same section", |
|
tags: [ |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
], |
|
["context", "more surrounding text"], |
|
[ |
|
"p", |
|
"fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", |
|
"", |
|
"author", |
|
], |
|
], |
|
id: "highlight-2", |
|
}), |
|
new MockNDKEvent({ |
|
kind: 9802, |
|
pubkey: "user-pubkey-3", |
|
content: "Highlight on different section", |
|
tags: [ |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:another-first-level-heading", |
|
], |
|
["context", "different section text"], |
|
[ |
|
"p", |
|
"fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", |
|
"", |
|
"author", |
|
], |
|
], |
|
id: "highlight-3", |
|
}), |
|
]; |
|
|
|
// Mock NDK instance |
|
mockNDK = { |
|
fetchEvents: vi.fn(async (filter) => { |
|
// Return highlights that match the filter |
|
const aTagFilter = filter["#a"]; |
|
if (aTagFilter) { |
|
return new Set( |
|
mockHighlights.filter((highlight) => |
|
aTagFilter.includes(highlight.tagValue("a") || "") |
|
), |
|
); |
|
} |
|
return new Set(); |
|
}), |
|
} as unknown as NDK; |
|
}); |
|
|
|
it("should extract section references from 30040 publication event", async () => { |
|
const result = await fetchHighlightsForPublication( |
|
publicationEvent, |
|
mockNDK, |
|
); |
|
|
|
// Should have results for the sections that have highlights |
|
expect(result.size).toBeGreaterThan(0); |
|
expect( |
|
result.has( |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
), |
|
).toBe(true); |
|
}); |
|
|
|
it("should fetch highlights for each section reference", async () => { |
|
const result = await fetchHighlightsForPublication( |
|
publicationEvent, |
|
mockNDK, |
|
); |
|
|
|
// First section should have 2 highlights |
|
const firstSectionHighlights = result.get( |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
); |
|
expect(firstSectionHighlights?.length).toBe(2); |
|
|
|
// Second section should have 1 highlight |
|
const secondSectionHighlights = result.get( |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:another-first-level-heading", |
|
); |
|
expect(secondSectionHighlights?.length).toBe(1); |
|
}); |
|
|
|
it("should group highlights by section address", async () => { |
|
const result = await fetchHighlightsForPublication( |
|
publicationEvent, |
|
mockNDK, |
|
); |
|
|
|
const firstSectionHighlights = result.get( |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
); |
|
|
|
// Verify the highlights are correctly grouped |
|
expect(firstSectionHighlights?.[0].content).toBe( |
|
"This is an interesting point", |
|
); |
|
expect(firstSectionHighlights?.[1].content).toBe( |
|
"Another highlight on same section", |
|
); |
|
}); |
|
|
|
it("should not include sections without highlights", async () => { |
|
const result = await fetchHighlightsForPublication( |
|
publicationEvent, |
|
mockNDK, |
|
); |
|
|
|
// Sections without highlights should not be in the result |
|
expect( |
|
result.has( |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:a-third-first-level-heading", |
|
), |
|
).toBe(false); |
|
expect( |
|
result.has( |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:asciimath-test-document", |
|
), |
|
).toBe(false); |
|
}); |
|
|
|
it("should handle publication with no section references", async () => { |
|
const emptyPublication = new MockNDKEvent({ |
|
kind: 30040, |
|
pubkey: "test-pubkey", |
|
content: "", |
|
tags: [ |
|
["d", "empty-doc"], |
|
["title", "Empty Document"], |
|
], |
|
}) as unknown as NDKEvent; |
|
|
|
const result = await fetchHighlightsForPublication( |
|
emptyPublication, |
|
mockNDK, |
|
); |
|
|
|
expect(result.size).toBe(0); |
|
}); |
|
|
|
it("should only process 30041 kind references, ignoring other a-tags", async () => { |
|
const mixedPublication = new MockNDKEvent({ |
|
kind: 30040, |
|
pubkey: "test-pubkey", |
|
content: "", |
|
tags: [ |
|
["d", "mixed-doc"], |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
], |
|
["a", "30023:some-pubkey:blog-post"], // Different kind, should be ignored |
|
["a", "1:some-pubkey"], // Different kind, should be ignored |
|
], |
|
}) as unknown as NDKEvent; |
|
|
|
const result = await fetchHighlightsForPublication( |
|
mixedPublication, |
|
mockNDK, |
|
); |
|
|
|
// Should call fetchEvents with only the 30041 reference |
|
expect(mockNDK.fetchEvents).toHaveBeenCalledWith( |
|
expect.objectContaining({ |
|
kinds: [9802], |
|
"#a": [ |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:first-level-heading", |
|
], |
|
}), |
|
); |
|
}); |
|
|
|
it("should handle d-tags with colons correctly", async () => { |
|
const colonPublication = new MockNDKEvent({ |
|
kind: 30040, |
|
pubkey: "test-pubkey", |
|
content: "", |
|
tags: [ |
|
["d", "colon-doc"], |
|
[ |
|
"a", |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:section:with:colons", |
|
], |
|
], |
|
}) as unknown as NDKEvent; |
|
|
|
const result = await fetchHighlightsForPublication( |
|
colonPublication, |
|
mockNDK, |
|
); |
|
|
|
// Should correctly parse the section address with colons |
|
expect(mockNDK.fetchEvents).toHaveBeenCalledWith( |
|
expect.objectContaining({ |
|
"#a": [ |
|
"30041:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:section:with:colons", |
|
], |
|
}), |
|
); |
|
}); |
|
});
|
|
|