Files
bitvid/tests/video-card-zap-totals.test.mjs
thePR0M3TH3ANandClaude Fable 5 8d45f25962 zaps: make the card + modal zap counter always visible (0 sats when none)
Diagnosis (live, verified): the zap-totals pipeline is correct end to end —
a seeded receipt queries and sums to the right sats — but of 310 kind-9735
receipts reachable on the relays, ZERO reference any bitvid video, so every
badge was legitimately hidden. That reads as "broken" and profile zaps to a
Lightning address never attach to a video, so hidden-when-zero left the
feature invisible in practice.

Show the counter always, like the view counter it sits beside: "0 sats"
until a receipt lands, then the summed total. Card binder and modal render
no longer gate on sats>0; the badge is revealed on bind/open and re-hidden
on modal close (no video open). Live-checked: 158/158 cards render the
badge. Tests spec-corrected with integrity notes.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 14:43:22 -04:00

90 lines
3.5 KiB
JavaScript

// #47 follow-up: video cards show the zap total (sats, orange, right-aligned)
// next to the view count. The badge is ALWAYS visible once bound (mirroring the
// view counter — a zero is a real, informative value), showing "0 sats" until a
// receipt batch lands, then the summed total.
//
// test_integrity_note:
// change_type: ["spec_correction"]
// scenarios:
// - id: SCN-card-zap-badge
// given: "cards bound to pointers with zero then nonzero cached totals"
// when: "bind() runs and a later zapTotals change fires"
// then: "badge visible from bind showing 0 sats; updates to the summed total in place; detached cards pruned"
// observable_outcomes:
// - "badge textContent per state; wrapper visible after bind"
// determinism_controls:
// - "JSDOM; the real zapTotals singleton driven through its store test hook; scripted subscription-manager list double"
// anti_cheat_rationale:
// prevents: ["hard-coded return value", "over-mocking internal logic"]
// relaxation:
// did_relax_any_assertion: false
// if_true_explain_spec_basis: "hidden-when-zero replaced with always-visible (matches the view counter the feature sits beside; the maintainer wants a persistent counter). Equally strict: still asserts exact textContent per state."
import "./test-helpers/setup-localstorage.mjs";
import test from "node:test";
import { strict as assert } from "node:assert";
import { JSDOM } from "jsdom";
import { createZapTotalBinder } from "../js/ui/views/videoCardZapTotals.js";
import zapTotalsStore, {
initZapTotals,
} from "../js/zapTotals.js";
const A1 = "30078:aaaa:video-1";
function makeCard(doc) {
const card = doc.createElement("div");
card.innerHTML =
'<div class="video-card__zaps hidden"><span data-zap-total></span></div>';
doc.body.appendChild(card);
return card;
}
test("binder: zero stays hidden, receipts reveal and format, prune drops detached", async () => {
const dom = new JSDOM("<!DOCTYPE html><body></body>", { pretendToBeVisual: true });
const doc = dom.window.document;
// Drive the REAL singleton store: scripted receipts arrive via its batched
// fetch (subscription-manager list double), so the binder sees the same
// cache + change signal production uses.
let receipts = [];
initZapTotals({
nostrClient: {
relays: ["wss://relay.example"],
getSubscriptionManager: () => ({ list: async () => receipts }),
},
tools: { nip57: { getSatoshisAmountFromBolt11: () => NaN } },
});
const binder = createZapTotalBinder();
const card = makeCard(doc);
const badge = card.querySelector("[data-zap-total]");
const wrapper = card.querySelector(".video-card__zaps");
// Tag-style ARRAY pointer — the shape VideoListView actually passes.
const pointerInfo = { key: `a:${A1}`, pointer: ["a", A1] };
binder.bind(card, pointerInfo);
assert.equal(wrapper.classList.contains("hidden"), false, "visible from bind");
assert.match(badge.textContent, /^0 sats$/, "shows 0 sats until receipts arrive");
// A receipt batch lands: 2100 sats via the zap request's amount tag.
receipts = [
{
id: "r1",
kind: 9735,
tags: [
["a", A1],
["description", JSON.stringify({ tags: [["amount", "2100000"]] })],
],
},
];
await zapTotalsStore.flush();
assert.equal(wrapper.classList.contains("hidden"), false, "stays visible");
assert.match(badge.textContent, /2,?100 sats|2\.1K sats/);
// Detached cards are dropped by prune (no leak, no stale writes).
card.remove();
binder.prune();
binder.destroy();
});