Files
bitvid/tests/discussion-count-service.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 6f5da76d30 [comments] align count with displayed kinds + preserve multi-line content
- discussionCountService now counts getAllowedCommentKinds() (NIP-22 kind 1111
  + legacy kind 1) instead of 1111-only, so the discussion badge no longer
  undercounts threads that contain legacy comments. The count filters are
  derived from the same source the thread display uses, so they can't drift.
- Add white-space: pre-wrap to .comment-thread__body so comments with line
  breaks/paragraphs preserve them instead of collapsing into one run-on line
  (long lines still wrap via word-break: break-word).

Test: spec-corrected discussion-count-service to assert the count filters use
getAllowedCommentKinds() (reverting to 1111-only fails the deepEqual).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 12:15:46 -04:00

185 lines
5.2 KiB
JavaScript

// Run with: node tests/discussion-count-service.test.mjs
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import DiscussionCountService, {
COUNT_UNSUPPORTED_TITLE,
} from "../js/services/discussionCountService.js";
import { getAllowedCommentKinds } from "../js/nostr/commentTargetNormalizer.js";
// SPEC CHANGE: the discussion-count badge now counts the same kinds the thread
// displays (NIP-22 kind 1111 + legacy kind 1) instead of only 1111, so it no
// longer undercounts threads that contain legacy comments.
const COMMENT_KINDS = getAllowedCommentKinds();
function createDomWithCountElement(videoId) {
const dom = new JSDOM(`
<div id="root">
<div data-discussion-count="${videoId}" data-count-state="idle">
<span data-discussion-count-value>0</span>
</div>
</div>
`);
const document = dom.window.document;
const container = document.getElementById("root");
const target = container.querySelector(
`[data-discussion-count="${videoId}"]`
);
return { dom, document, container, target };
}
async function testRefreshCountsCachesResults() {
const video = {
id: "count-video-1",
tags: [["d", "identifier-1"]],
pubkey: "pubkey-1",
kind: 30078,
};
const { container, target } = createDomWithCountElement(video.id);
const service = new DiscussionCountService();
const nostrClient = {
pool: {},
countEventsAcrossRelays: async (filters) => {
assert.equal(filters.length, 3, "should request id, uppercase, and address filters");
const [eventFilter, uppercaseFilter, addressFilter] = filters;
assert.deepEqual(eventFilter, {
kinds: COMMENT_KINDS,
"#E": ["count-video-1"],
});
assert.deepEqual(uppercaseFilter, {
kinds: COMMENT_KINDS,
"#A": ["30078:pubkey-1:identifier-1"],
"#K": ["30078"],
"#P": ["pubkey-1"],
});
assert.deepEqual(addressFilter, {
kinds: COMMENT_KINDS,
"#A": ["30078:pubkey-1:identifier-1"],
});
return {
total: 5,
perRelay: [{ ok: true }],
};
},
};
service.refreshCounts([video], {
videoListRoot: container,
nostrClient,
});
const inFlight = service.inFlightDiscussionCounts.get(video.id);
assert.ok(inFlight, "refresh should record an in-flight request");
await inFlight;
assert.equal(
service.getCachedCount(video.id),
5,
"successful responses should be cached"
);
assert.equal(
target.dataset.countState,
"ready",
"DOM should transition to ready after a successful fetch"
);
assert.equal(
target.querySelector("[data-discussion-count-value]").textContent,
"5",
"count value should reflect the fetched total"
);
let callCount = 0;
nostrClient.countEventsAcrossRelays = async () => {
callCount += 1;
return { total: 99, perRelay: [{ ok: true }] };
};
service.refreshCounts([video], {
videoListRoot: container,
nostrClient,
});
assert.equal(
callCount,
0,
"cached videos should not trigger additional COUNT queries"
);
assert.equal(
target.querySelector("[data-discussion-count-value]").textContent,
"5",
"cached values should remain visible without pending states"
);
}
async function testUnsupportedRelaysUpdateDomState() {
const video = {
id: "unsupported-video",
tags: [["d", "identifier-2"]],
pubkey: "pubkey-2",
kind: 30078,
};
const { container, target } = createDomWithCountElement(video.id);
const service = new DiscussionCountService();
const nostrClient = {
pool: {},
countEventsAcrossRelays: async (filters) => {
assert.equal(filters.length, 3, "unsupported relays should still receive all filters");
const [eventFilter, uppercaseFilter, addressFilter] = filters;
assert.deepEqual(eventFilter, {
kinds: COMMENT_KINDS,
"#E": ["unsupported-video"],
});
assert.deepEqual(uppercaseFilter, {
kinds: COMMENT_KINDS,
"#A": ["30078:pubkey-2:identifier-2"],
"#K": ["30078"],
"#P": ["pubkey-2"],
});
assert.deepEqual(addressFilter, {
kinds: COMMENT_KINDS,
"#A": ["30078:pubkey-2:identifier-2"],
});
return {
total: 7,
perRelay: [{ ok: false }],
};
},
};
service.refreshCounts([video], {
videoListRoot: container,
nostrClient,
});
const inFlight = service.inFlightDiscussionCounts.get(video.id);
assert.ok(inFlight, "unsupported relays should still trigger in-flight tracking");
await inFlight;
assert.equal(
service.getCachedCount(video.id),
undefined,
"unsupported relay responses should not populate the cache"
);
assert.equal(
target.dataset.countState,
"unsupported",
"DOM should flag unsupported relays for COUNT queries"
);
assert.equal(
target.querySelector("[data-discussion-count-value]").textContent,
"—",
"unsupported states should render an em dash for the count value"
);
assert.equal(
target.getAttribute("title"),
COUNT_UNSUPPORTED_TITLE,
"unsupported states should expose a descriptive tooltip"
);
}
await testRefreshCountsCachesResults();
await testUnsupportedRelaysUpdateDomState();
console.log("discussion-count-service tests passed");