Files
bitvid/tests/modal-coordinator-zap-badge.test.mjs
T
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

116 lines
4.6 KiB
JavaScript

// Regression for "clicking a video no longer opens the player": coordinator
// methods run with `this` bound to the APPLICATION (bindCoordinator), so a
// cross-method `this.helper()` call inside the coordinator only works when
// app.js also adds an explicit delegate — the zap-badge helpers were plain
// methods, `this.subscribeModalZapTotal` didn't exist on the app, and EVERY
// modal open threw. This test drives subscribeModalViewCount through the REAL
// bindCoordinator with ONLY the delegates app.js actually defines, so any
// future cross-method `this.` call without a delegate fails here first.
//
// test_integrity_note:
// change_type: ["new_tests"]
// scenarios:
// - id: SCN-modal-open-no-missing-delegate
// given: "a coordinator bound to an app stub exposing only app.js's real modal delegates"
// when: "subscribeModalViewCount runs (the modal-open path) and teardown runs (close path)"
// then: "no TypeError; the zap badge is visible from open (0 sats) and updates to the summed total; hidden on teardown"
// observable_outcomes:
// - "subscribeModalViewCount does not throw (the user-facing 'video will not open' bug)"
// - "badge visible at open showing 0 sats, updates to the summed total, hidden on teardown (no video open)"
// determinism_controls:
// - "JSDOM; scripted subscription-manager double; store flushed manually"
// anti_cheat_rationale:
// prevents: ["over-mocking internal logic", "hard-coded return value"]
// relaxation:
// did_relax_any_assertion: false
import "./test-helpers/setup-localstorage.mjs";
import test from "node:test";
import { strict as assert } from "node:assert";
import { JSDOM } from "jsdom";
import { createModalCoordinator } from "../js/app/modalCoordinator.js";
import bindCoordinator from "../js/app/bindCoordinator.js";
import zapTotalsStore, {
initZapTotals,
requestVideoZapTotal,
getVideoZapTotalSnapshot,
onZapTotalsChanged,
} from "../js/zapTotals.js";
import { formatViewCount } from "../js/viewCounter.js";
const A1 = "30078:" + "c".repeat(64) + ":video-1";
test("modal open path survives binding and drives the zap badge", async () => {
const dom = new JSDOM(
'<!DOCTYPE html><body><span id="videoZapTotal" class="hidden"><span data-zap-total-text></span></span></body>',
{ pretendToBeVisual: true },
);
global.document = dom.window.document;
let receipts = [];
initZapTotals({
nostrClient: {
relays: ["wss://relay.example"],
getSubscriptionManager: () => ({ list: async () => receipts }),
},
tools: { nip57: { getSatoshisAmountFromBolt11: () => NaN } },
});
const coordinator = createModalCoordinator({
devLogger: { warn: () => {}, log: () => {}, error: () => {} },
subscribeToVideoViewCount: () => "token-1",
unsubscribeFromVideoViewCount: () => {},
formatViewCount,
requestVideoZapTotal,
getVideoZapTotalSnapshot,
onZapTotalsChanged,
});
// App stub with ONLY the delegates app.js really defines for this path.
const app = {
videoModal: {
getViewCountElement: () => dom.window.document.createElement("span"),
updateViewCountLabel: () => {},
setViewCountPointer: () => {},
},
formatViewCountLabel: (n) => `${n} views`,
};
app._modal = bindCoordinator(app, coordinator);
app.teardownModalViewCountSubscription = (...args) =>
app._modal.teardownModalViewCountSubscription(...args);
app.subscribeModalViewCount = (...args) =>
app._modal.subscribeModalViewCount(...args);
const pointer = ["a", A1];
// The bug: this line threw "this.subscribeModalZapTotal is not a function".
app.subscribeModalViewCount(pointer, `a:${A1}`);
const badge = dom.window.document.getElementById("videoZapTotal");
// Always visible while a video is open (mirrors the view counter); 0 sats
// until a receipt lands.
assert.equal(badge.classList.contains("hidden"), false, "visible while open");
assert.match(
badge.querySelector("[data-zap-total-text]").textContent,
/^0 sats$/,
);
receipts = [
{
id: "r1",
kind: 9735,
tags: [["a", A1], ["description", JSON.stringify({ tags: [["amount", "21000"]] })]],
},
];
await zapTotalsStore.flush();
assert.equal(badge.classList.contains("hidden"), false, "stays visible");
assert.match(
badge.querySelector("[data-zap-total-text]").textContent,
/21 sats/,
);
// Modal close path re-hides the badge (no video open) — also used to throw
// via the missing this.teardownModalZapTotal delegate.
app.teardownModalViewCountSubscription();
assert.equal(badge.classList.contains("hidden"), true, "teardown hides the badge");
});