diff --git a/tests/services/discussionCountService.test.mjs b/tests/services/discussionCountService.test.mjs new file mode 100644 index 00000000..278ad97d --- /dev/null +++ b/tests/services/discussionCountService.test.mjs @@ -0,0 +1,192 @@ +import { describe, test, mock, before, after, beforeEach } from "node:test"; +import assert from "node:assert/strict"; +import { createUiDom } from "../ui/helpers/jsdom-test-helpers.mjs"; +import DiscussionCountService from "../../js/services/discussionCountService.js"; + +describe("DiscussionCountService", () => { + let dom; + let document; + let window; + + before(() => { + dom = createUiDom(); + window = dom.window; + document = dom.document; + }); + + after(() => { + dom.cleanup(); + }); + + test("initialization sets defaults", () => { + const service = new DiscussionCountService(); + assert.equal(service.maxVideos, 24); + assert.ok(service.logger); + assert.equal(service.videoDiscussionCountCache.size, 0); + assert.equal(service.inFlightDiscussionCounts.size, 0); + }); + + test("refreshCounts returns early if videos array is empty or invalid", () => { + const service = new DiscussionCountService(); + // mock logger to ensure no warnings + const logger = { warn: mock.fn() }; + service.logger = logger; + + service.refreshCounts([], {}); + service.refreshCounts(null, {}); + + assert.equal(service.inFlightDiscussionCounts.size, 0); + assert.equal(logger.warn.mock.callCount(), 0); + }); + + test("refreshCounts returns early if dependencies are missing", () => { + const service = new DiscussionCountService(); + const video = { id: "video1" }; + + // Missing nostrClient + service.refreshCounts([video], { videoListRoot: document.createElement("div") }); + assert.equal(service.inFlightDiscussionCounts.size, 0); + + // Missing videoListRoot + service.refreshCounts([video], { nostrClient: {} }); + assert.equal(service.inFlightDiscussionCounts.size, 0); + }); + + test("refreshCounts handles happy path (fetches and updates DOM)", async () => { + const service = new DiscussionCountService(); + const video = { id: "video1", kind: 30078, pubkey: "pubkey1" }; + + const root = document.createElement("div"); + const container = document.createElement("div"); + container.dataset.discussionCount = "video1"; + const valueEl = document.createElement("span"); + valueEl.dataset.discussionCountValue = "true"; + container.appendChild(valueEl); + root.appendChild(container); + + const nostrClient = { + pool: {}, + countEventsAcrossRelays: mock.fn(async () => ({ + total: 42, + ok: true, + perRelay: [{ ok: true }] + })) + }; + + service.refreshCounts([video], { videoListRoot: root, nostrClient }); + + // Wait for promise resolution (microtasks) + await new Promise(resolve => process.nextTick(resolve)); + await new Promise(resolve => process.nextTick(resolve)); + + assert.equal(nostrClient.countEventsAcrossRelays.mock.callCount(), 1); + assert.equal(valueEl.textContent, "42"); + assert.equal(container.dataset.countState, "ready"); + assert.equal(service.videoDiscussionCountCache.get("video1"), 42); + }); + + test("refreshCounts uses cached count if available", async () => { + const service = new DiscussionCountService(); + const video = { id: "video1" }; + service.videoDiscussionCountCache.set("video1", 100); + + const root = document.createElement("div"); + const container = document.createElement("div"); + container.dataset.discussionCount = "video1"; + const valueEl = document.createElement("span"); + valueEl.dataset.discussionCountValue = "true"; + container.appendChild(valueEl); + root.appendChild(container); + + const nostrClient = { + pool: {}, + countEventsAcrossRelays: mock.fn() + }; + + service.refreshCounts([video], { videoListRoot: root, nostrClient }); + + assert.equal(nostrClient.countEventsAcrossRelays.mock.callCount(), 0); + assert.equal(valueEl.textContent, "100"); + }); + + test("refreshCounts handles API errors gracefully", async () => { + const logger = { warn: mock.fn() }; + const service = new DiscussionCountService({ logger }); + const video = { id: "video1" }; + + const root = document.createElement("div"); + const container = document.createElement("div"); + container.dataset.discussionCount = "video1"; + const valueEl = document.createElement("span"); + valueEl.dataset.discussionCountValue = "true"; + container.appendChild(valueEl); + root.appendChild(container); + + const error = new Error("Network error"); + const nostrClient = { + pool: {}, + countEventsAcrossRelays: mock.fn(async () => { throw error; }) + }; + + service.refreshCounts([video], { videoListRoot: root, nostrClient }); + + const requestPromise = service.inFlightDiscussionCounts.get("video1"); + await assert.rejects(requestPromise, error); + + assert.equal(logger.warn.mock.callCount(), 1); + assert.equal(container.dataset.countState, "error"); + assert.equal(valueEl.textContent, "—"); + }); + + test("refreshCounts handles partial results", async () => { + const service = new DiscussionCountService(); + const video = { id: "video1" }; + + const root = document.createElement("div"); + const container = document.createElement("div"); + container.dataset.discussionCount = "video1"; + const valueEl = document.createElement("span"); + valueEl.dataset.discussionCountValue = "true"; + container.appendChild(valueEl); + root.appendChild(container); + + const nostrClient = { + pool: {}, + countEventsAcrossRelays: mock.fn(async () => ({ total: 5, partial: true, ok: true, perRelay: [{ ok: true }] })) + }; + + service.refreshCounts([video], { videoListRoot: root, nostrClient }); + + await new Promise(resolve => process.nextTick(resolve)); + await new Promise(resolve => process.nextTick(resolve)); + + assert.equal(container.dataset.countState, "partial"); + assert.equal(valueEl.textContent, "5"); + }); + + test("refreshCounts handles unsupported relays (empty perRelay)", async () => { + const service = new DiscussionCountService(); + const video = { id: "video1" }; + + const root = document.createElement("div"); + const container = document.createElement("div"); + container.dataset.discussionCount = "video1"; + const valueEl = document.createElement("span"); + valueEl.dataset.discussionCountValue = "true"; + container.appendChild(valueEl); + root.appendChild(container); + + const nostrClient = { + pool: {}, + countEventsAcrossRelays: mock.fn(async () => ({ total: 0, perRelay: [] })) + }; + + service.refreshCounts([video], { videoListRoot: root, nostrClient }); + + await new Promise(resolve => process.nextTick(resolve)); + await new Promise(resolve => process.nextTick(resolve)); + + assert.equal(container.dataset.countState, "unsupported"); + assert.equal(valueEl.textContent, "—"); + }); +});