Files
bitvid/tests/video-modal-zap.test.mjs
T
google-labs-jules[bot]andPR0M3TH3AN 0665a3392e refactor(app): Move heavy initialization from constructor to setup()
- Introduces a `setup()` method in `Application` class to handle complex initialization and side effects.
- Updates `constructor` to be lightweight, only storing configuration options.
- Updates `js/bootstrap.js`, `js/embed.js`, and relevant tests to call `app.setup()` explicitly after instantiation.
- Resolves "Heavy Constructor" code health issue.
- Fixes `tests/video-modal-zap.test.mjs` environment issue with `globalThis.navigator`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-08 03:44:38 +00:00

991 lines
26 KiB
JavaScript

import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
const noop = () => {};
class DummyElement {
constructor() {
this.classList = {
add: noop,
remove: noop,
toggle: noop,
contains: () => false,
};
Object.defineProperty(this, "style", {
configurable: true,
enumerable: true,
writable: true,
value: {},
});
this.dataset = {};
this.children = [];
}
appendChild(child) {
this.children.push(child);
return child;
}
removeChild(child) {
const index = this.children.indexOf(child);
if (index >= 0) {
this.children.splice(index, 1);
}
return child;
}
querySelector() {
return null;
}
addEventListener() {}
removeEventListener() {}
setAttribute() {}
removeAttribute() {}
focus() {}
contains() {
return false;
}
}
function createDocumentStub() {
const doc = {
body: new DummyElement(),
documentElement: new DummyElement(),
head: new DummyElement(),
getElementById: () => null,
querySelector: () => null,
createElement: () => new DummyElement(),
addEventListener: noop,
removeEventListener: noop,
};
doc.body.contains = () => false;
doc.defaultView = null;
return doc;
}
function createWindowStub(documentStub) {
const nav = { clipboard: { writeText: async () => {} } };
const win = {
document: documentStub,
location: {
href: "https://example.com/",
origin: "https://example.com",
pathname: "/",
},
history: {
pushState: noop,
replaceState: noop,
},
navigator: nav,
addEventListener: noop,
removeEventListener: noop,
requestAnimationFrame: (fn) => {
if (typeof fn === "function") {
fn();
}
},
setTimeout,
clearTimeout,
NostrTools: {
nip19: {
neventEncode: () => "nevent1example",
npubEncode: () => "npub1example",
decode: (value) => {
if (typeof value !== "string") {
throw new Error("Invalid nip19 input");
}
if (value.startsWith("npub")) {
return { type: "npub", data: "f".repeat(64) };
}
if (value.startsWith("nevent")) {
return { type: "nevent", data: { id: "f".repeat(64) } };
}
return null;
},
},
},
};
win.HTMLElement = DummyElement;
win.HTMLImageElement = DummyElement;
win.setInterval = setInterval;
win.clearInterval = clearInterval;
win.console = console;
return win;
}
const documentStub = createDocumentStub();
const windowStub = createWindowStub(documentStub);
windowStub.document = documentStub;
documentStub.defaultView = windowStub;
windowStub.document.defaultView = windowStub;
globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = true;
if (typeof globalThis.window === "undefined") {
globalThis.window = windowStub;
} else {
Object.assign(globalThis.window, windowStub);
}
globalThis.HTMLElement = DummyElement;
globalThis.HTMLImageElement = DummyElement;
if (typeof globalThis.self === "undefined") {
globalThis.self = globalThis.window;
} else if (globalThis.self !== globalThis.window) {
globalThis.self = globalThis.window;
}
if (typeof globalThis.WebSocket === "undefined") {
globalThis.WebSocket = class {
constructor() {
throw new Error("WebSocket is not available in tests.");
}
close() {}
};
}
globalThis.document = documentStub;
try {
Object.defineProperty(globalThis, "navigator", {
value: windowStub.navigator,
writable: true,
configurable: true,
});
} catch (e) {
// Fallback for environments where navigator is read-only
}
globalThis.location = windowStub.location;
if (typeof globalThis.fetch === "undefined") {
globalThis.fetch = async () => {
throw new Error("Unexpected fetch call in tests.");
};
}
const {
rememberLightningMetadata,
normalizeLightningAddressKey,
setCachedPlatformLightningAddress,
clearZapCaches,
} = await import("../js/payments/zapSharedState.js");
function primeLightningMetadata(address, overrides = {}) {
const key = normalizeLightningAddressKey(address);
rememberLightningMetadata({
key,
address,
resolved: { address, url: `https://lnurl.test/${address}` },
metadata: {
callback: `https://lnurl.test/${address}/callback`,
minSendable: 1_000,
maxSendable: 10_000_000,
commentAllowed: 280,
allowsNostr: true,
...overrides,
},
fetchedAt: Date.now(),
});
}
function createVideoModalStub() {
const listeners = new Map();
return {
setCopyEnabled: noop,
setShareEnabled: noop,
setWalletPromptVisible: noop,
zapVisibilityCalls: [],
setZapVisibility(config) {
const payload =
config && typeof config === "object"
? { ...config }
: { visible: Boolean(config) };
this.zapVisibilityCalls.push(payload);
this.lastZapVisibility = payload;
},
dialogOpen: false,
closeZapDialogCalls: [],
closeZapDialog(options = {}) {
this.closeZapDialogCalls.push(options);
this.dialogOpen = false;
},
resetStats: noop,
updateViewCountLabel: noop,
setViewCountPointer: noop,
completedStates: [],
addEventListener(type, handler) {
listeners.set(type, handler);
},
removeEventListener(type) {
listeners.delete(type);
},
setZapPendingCalls: [],
statusMessages: [],
receipts: [],
summaries: [],
resetForms: [],
currentAmount: "",
currentComment: "",
setZapPending(value) {
this.setZapPendingCalls.push(Boolean(value));
},
setZapCompleted(value) {
this.completedStates.push(Boolean(value));
},
setZapStatus(message, tone) {
this.statusMessages.push({ message, tone });
},
clearZapReceipts() {
this.receipts.push({ cleared: true });
},
renderZapReceipts(list, options) {
this.receipts.push({ list, options });
},
setZapSplitSummary(text) {
this.summaries.push(text);
},
setZapRetryPending(pending, { summary } = {}) {
this.retryState = { pending, summary };
},
resetZapForm({ amount, comment }) {
this.resetForms.push({ amount, comment });
this.currentAmount = amount;
this.currentComment = comment;
},
setZapAmount(value) {
this.currentAmount = value;
},
setZapComment(value) {
this.currentComment = value;
},
getZapCommentValue() {
return this.currentComment || "";
},
};
}
function createServices({ splitAndZap }) {
const nostrService = {
getVideoSubscription: () => null,
getVideosMap: () => new Map(),
on: () => () => {},
};
const feedEngine = {
run: noop,
register: noop,
};
const playbackService = {
createSession: () => ({
getPlaybackConfig: () => ({}),
getMagnetForPlayback: () => "",
}),
};
const authService = {
on: () => () => {},
};
return {
nostrService,
feedEngine,
playbackService,
authService,
payments: {
splitAndZap,
ensureWallet: async ({ settings }) => settings,
sendPayment: async () => ({}),
},
};
}
function createHelpers() {
return {
mediaLoaderFactory: () => ({ observe: noop, unobserve: noop }),
};
}
async function createApp({ splitAndZap }) {
clearZapCaches();
const modalStub = createVideoModalStub();
const { Application } = await import("../js/app.js");
const app = new Application({
services: createServices({ splitAndZap }),
helpers: createHelpers(),
ui: {
videoModal: () => modalStub,
},
});
app.setup();
app.boundVideoModalZapOpenHandler = (event = {}) => {
const requiresLogin = Boolean(event?.detail?.requiresLogin);
app.pendingModalZapOpen = requiresLogin;
const openResult = app.zapController?.open({ requiresLogin });
if (!openResult) {
event?.preventDefault?.();
if (!requiresLogin) {
app.pendingModalZapOpen = false;
}
modalStub.closeZapDialog?.({ silent: true, restoreFocus: false });
return;
}
app.pendingModalZapOpen = false;
};
const creatorAddress = "[email protected]";
const platformAddress = "[email protected]";
setCachedPlatformLightningAddress(platformAddress);
primeLightningMetadata(creatorAddress);
primeLightningMetadata(platformAddress);
app.showError = noop;
app.showSuccess = noop;
app.showStatus = noop;
return { app, modalStub, creatorAddress, platformAddress };
}
await (async () => {
// Test: logged-out zap request closes modal and queues reopen
const { app, modalStub } = await createApp({
splitAndZap: async () => ({ receipts: [] }),
});
let loginNotificationCount = 0;
app.zapController.notifyLoginRequired = () => {
loginNotificationCount += 1;
};
modalStub.dialogOpen = true;
modalStub.closeZapDialogCalls = [];
app.pendingModalZapOpen = false;
app.boundVideoModalZapOpenHandler({ detail: { requiresLogin: true } });
assert.equal(
loginNotificationCount,
1,
"should request login notification when zapping logged out",
);
assert.equal(
app.pendingModalZapOpen,
true,
"zap should remain pending for post-login reopen",
);
assert.equal(
modalStub.closeZapDialogCalls.length,
1,
"zap dialog should close immediately when login required",
);
assert.deepEqual(modalStub.closeZapDialogCalls[0], {
silent: true,
restoreFocus: false,
});
assert.equal(modalStub.dialogOpen, false, "zap dialog should be closed");
app.destroy();
})();
await (async () => {
// Test: preventDefault is called when zap opening is rejected post-dispatch
const { app, modalStub } = await createApp({
splitAndZap: async () => ({ receipts: [] }),
});
let loginNotificationCount = 0;
app.zapController.notifyLoginRequired = () => {
loginNotificationCount += 1;
};
app.zapController.isUserLoggedIn = () => false;
modalStub.closeZapDialogCalls = [];
const preventCalls = [];
const event = {
detail: { requiresLogin: false },
preventDefault: () => {
preventCalls.push(true);
},
};
app.pendingModalZapOpen = true;
app.boundVideoModalZapOpenHandler(event);
assert.equal(
loginNotificationCount,
1,
"should notify login requirement when zap open is rejected",
);
assert.equal(
preventCalls.length,
1,
"should prevent default zap opening when controller rejects it",
);
assert.equal(
app.pendingModalZapOpen,
false,
"pending zap reopen flag should clear when rejection wasn't login-gated",
);
assert.deepEqual(modalStub.closeZapDialogCalls[0], {
silent: true,
restoreFocus: false,
});
app.destroy();
})();
await (async () => {
// Test: session actors should not be treated as logged-in users for zaps
const { app, modalStub } = await createApp({
splitAndZap: async () => ({ receipts: [] }),
});
const { nostrClient } = await import("../js/nostrClientFacade.js");
const originalSessionActor = nostrClient.sessionActor;
const sessionPubkey = "a".repeat(64);
nostrClient.sessionActor = { pubkey: sessionPubkey, privateKey: "priv" };
app.pubkey = sessionPubkey;
app.isUserLoggedIn = () => false;
app.zapController.isUserLoggedIn = () => false;
app.zapController.hasSessionActor = () => true;
modalStub.zapVisibilityCalls = [];
app.zapController.setVisibility(true);
const lastVisibility =
modalStub.zapVisibilityCalls[modalStub.zapVisibilityCalls.length - 1];
assert.deepEqual(
lastVisibility,
{ visible: true, requiresLogin: true },
"zap controller should require login when only a session actor is present",
);
assert.equal(
app.isUserLoggedIn(),
false,
"session actor pubkeys should not count as logged-in users",
);
app.pubkey = null;
nostrClient.sessionActor = originalSessionActor;
app.destroy();
})();
await (async () => {
// Test: wallet required before zapping
const splitCalls = [];
const { app, modalStub, creatorAddress } = await createApp({
splitAndZap: async (...args) => {
splitCalls.push(args);
return { receipts: [] };
},
});
let walletPaneCalls = 0;
app.zapController.requestWalletPane = () => {
walletPaneCalls += 1;
return Promise.resolve();
};
app.currentVideo = {
id: "event123",
pubkey: "a".repeat(64),
tags: [],
content: "",
created_at: 1_700_000_000,
lightningAddress: creatorAddress,
};
await app.zapController.sendZap({ amount: 500, comment: "Hello" });
assert.equal(walletPaneCalls, 1, "should prompt to open wallet pane");
assert.equal(splitCalls.length, 0, "splitAndZap should not be invoked");
assert.deepEqual(modalStub.setZapPendingCalls, [], "zap controls should remain idle");
app.destroy();
})();
await (async () => {
// Test: splitAndZap invoked with video metadata when wallet is present
const splitCalls = [];
const splitAndZap = async (payload, deps) => {
splitCalls.push({ payload, deps });
return {
receipts: [
{
recipientType: "creator",
status: "success",
amount: payload.amountSats,
address: payload.videoEvent.lightningAddress,
payment: { result: { preimage: "ff".repeat(16) } },
validation: {
status: "passed",
event: { kind: 9735, id: "creator-receipt" },
},
},
],
};
};
const { app, modalStub, creatorAddress } = await createApp({
splitAndZap,
});
const pubkeyHex = "b".repeat(64);
app.pubkey = pubkeyHex;
const normalized = app.normalizeHexPubkey(pubkeyHex);
app.nwcSettingsService.cache.set(normalized, {
nwcUri: "nostr+walletconnect://example",
defaultZap: null,
lastChecked: null,
version: "",
});
app.currentVideo = {
id: "event456",
pubkey: pubkeyHex,
tags: [["d", "video123"]],
content: "",
created_at: 1_700_000_001,
lightningAddress: creatorAddress,
};
app.zapController.setAmount(1500);
app.zapController.setComment("Great video");
await app.zapController.sendZap({ amount: 1500, comment: "Great video" });
assert.equal(splitCalls.length, 1, "splitAndZap should be called once");
const call = splitCalls[0];
assert.equal(call.payload.amountSats, 1500);
assert.equal(call.payload.comment, "Great video");
assert.equal(
call.payload.videoEvent.lightningAddress,
creatorAddress,
"video event should include lightning address"
);
assert.equal(call.payload.walletSettings.nwcUri, "nostr+walletconnect://example");
assert.equal(call.payload.videoEvent.pubkey, pubkeyHex);
assert.equal(call.payload.videoEvent.id, "event456");
assert.deepEqual(call.payload.videoEvent.tags, [["d", "video123"]]);
const lastStatus = modalStub.statusMessages[modalStub.statusMessages.length - 1];
assert(lastStatus.message.includes("Sent 1500 sats"), "should report zap summary");
assert.equal(lastStatus.tone, "success");
assert.equal(modalStub.resetForms.length > 0, true, "form should reset after success");
assert.equal(modalStub.currentComment, "");
app.destroy();
})();
await (async () => {
// Test: validation skipped receipts do not trigger warnings
const receipts = [
{
recipientType: "creator",
status: "success",
amount: 600,
address: "[email protected]",
payment: { result: { preimage: "dd".repeat(16) } },
validation: {
status: "skipped",
reason: "Zap request unavailable",
},
},
{
recipientType: "platform",
status: "success",
amount: 100,
address: "[email protected]",
validation: {
status: "skipped",
reason: "Zap request unavailable",
},
},
];
const { app, modalStub, creatorAddress } = await createApp({
splitAndZap: async () => ({ receipts }),
});
const pubkeyHex = "d".repeat(64);
app.pubkey = pubkeyHex;
const normalized = app.normalizeHexPubkey(pubkeyHex);
app.nwcSettingsService.cache.set(normalized, {
nwcUri: "nostr+walletconnect://example",
defaultZap: null,
lastChecked: null,
version: "",
});
app.currentVideo = {
id: "event901",
pubkey: pubkeyHex,
tags: [["d", "video901"]],
content: "",
created_at: 1_700_000_150,
lightningAddress: creatorAddress,
};
const initialWarningCount = modalStub.statusMessages.filter(
(entry) => entry.tone === "warning"
).length;
await app.zapController.sendZap({ amount: 700, comment: "No receipt support" });
const warningsAfter = modalStub.statusMessages.filter(
(entry) => entry.tone === "warning"
).length;
assert.equal(
warningsAfter,
initialWarningCount,
"skipped validation should not emit new warnings",
);
const lastStatus = modalStub.statusMessages[modalStub.statusMessages.length - 1];
assert.equal(lastStatus.tone, "success", "zap should report success when validation skipped");
assert(!/awaiting validated zap receipt/i.test(lastStatus.message));
const lastReceiptEntry = modalStub.receipts[modalStub.receipts.length - 1];
assert.equal(
lastReceiptEntry?.options?.partial,
false,
"receipt rendering should complete when validation is skipped",
);
app.destroy();
})();
await (async () => {
// Test: platform receipt failure keeps retry state active and surfaces warning
const platformError = new Error("Budget exceeded");
const receipts = [
{
recipientType: "creator",
status: "success",
amount: 900,
address: "[email protected]",
payment: { result: { preimage: "cc".repeat(16) } },
validation: {
status: "passed",
event: { kind: 9735, id: "creator-receipt" },
},
},
{
recipientType: "platform",
status: "error",
amount: 100,
address: "[email protected]",
error: platformError,
},
];
const { app, modalStub, creatorAddress, platformAddress } = await createApp({
splitAndZap: async () => ({ receipts }),
});
const pubkeyHex = "c".repeat(64);
app.pubkey = pubkeyHex;
const normalized = app.normalizeHexPubkey(pubkeyHex);
app.nwcSettingsService.cache.set(normalized, {
nwcUri: "nostr+walletconnect://example",
defaultZap: null,
lastChecked: null,
version: "",
});
app.currentVideo = {
id: "event789",
pubkey: pubkeyHex,
tags: [["d", "video789"]],
content: "",
created_at: 1_700_000_100,
lightningAddress: creatorAddress,
};
const initialResetCount = modalStub.resetForms.length;
const initialCompletedCount = modalStub.completedStates.length;
await app.zapController.sendZap({ amount: 1000, comment: "Nice work" });
const lastReceiptEntry = modalStub.receipts[modalStub.receipts.length - 1];
assert.equal(
lastReceiptEntry.options?.partial,
true,
"should render zap receipts in partial mode when failures occur",
);
assert.equal(
app.zapController.modalZapRetryState?.shares?.length,
1,
"controller should queue failing receipt for retry",
);
assert.equal(
modalStub.retryState?.pending,
true,
"modal should display retry pending state for failed share",
);
const lastStatus = modalStub.statusMessages[modalStub.statusMessages.length - 1];
assert.equal(lastStatus.tone, "warning", "warning tone should be emitted for receipt failure");
assert.ok(
/increase your wallet zap limit or reduce the platform fee/i.test(lastStatus.message),
"status message should translate budget exceeded guidance",
);
assert.ok(
/Remaining share\(s\): Platform 100 sats\./.test(lastStatus.message),
"status message should summarize remaining platform share",
);
assert.equal(
modalStub.resetForms.length,
initialResetCount,
"zap form should not reset while a share remains unpaid",
);
assert.equal(
modalStub.completedStates.length,
initialCompletedCount + 1,
"zap completion calls should only reflect the in-flight attempt",
);
assert.equal(
modalStub.completedStates[modalStub.completedStates.length - 1],
false,
"zap modal should not mark zap as completed on partial failure",
);
assert.equal(
app.zapController.modalZapCommentValue,
"Nice work",
"zap controller should retain comment value for retry",
);
// Ensure platform retry summary references the expected address context
assert.equal(
app.zapController.modalZapRetryState?.shares?.[0]?.type,
"platform",
"retry state should track platform share type",
);
assert.equal(
app.zapController.modalZapRetryState?.shares?.[0]?.amount,
100,
"retry state should preserve remaining platform amount",
);
app.destroy();
})();
await (async () => {
// Test: validation failure surfaces warning and resets form
const validationReceipts = [
{
recipientType: "creator",
status: "success",
amount: 750,
address: "[email protected]",
payment: { result: { preimage: "dd".repeat(16) } },
validation: {
status: "failed",
reason: "No compliant receipt published",
},
},
];
const { app, modalStub, creatorAddress } = await createApp({
splitAndZap: async () => ({ receipts: validationReceipts }),
});
const pubkeyHex = "d".repeat(64);
app.pubkey = pubkeyHex;
const normalized = app.normalizeHexPubkey(pubkeyHex);
app.nwcSettingsService.cache.set(normalized, {
nwcUri: "nostr+walletconnect://example",
defaultZap: null,
lastChecked: null,
version: "",
});
app.currentVideo = {
id: "event990",
pubkey: pubkeyHex,
tags: [["d", "video990"]],
content: "",
created_at: 1_700_000_200,
lightningAddress: creatorAddress,
};
const initialResetCount = modalStub.resetForms.length;
const initialCompleted = modalStub.completedStates.length;
await app.zapController.sendZap({ amount: 750, comment: "Pending receipt" });
const lastReceiptEntry = modalStub.receipts[modalStub.receipts.length - 1];
assert.equal(
lastReceiptEntry.options?.partial,
true,
"should render zap receipts in partial mode for validation issues",
);
const lastStatus = modalStub.statusMessages[modalStub.statusMessages.length - 1];
assert.equal(lastStatus.tone, "warning", "validation failure should emit warning status");
assert.match(
lastStatus.message,
/awaiting validated zap receipt/i,
"status message should highlight missing validated receipt",
);
assert.equal(
modalStub.resetForms.length,
initialResetCount + 1,
"zap form should reset after validation warning",
);
assert.equal(
modalStub.completedStates.length,
initialCompleted + 1,
"completion state should record the validation attempt",
);
assert.equal(
modalStub.completedStates[modalStub.completedStates.length - 1],
true,
"validation warning should mark zap attempt as completed",
);
app.destroy();
})();
await (async () => {
// Test: allowance exhaustion surfaces guidance and skips retry queue
const { app, modalStub, creatorAddress, platformAddress } = await createApp({
splitAndZap: async () => ({ receipts: [] }),
});
const shareError = new Error("Budget exceeded");
const tracker = [
{
type: "creator",
status: "success",
amount: 900,
address: creatorAddress,
payment: { preimage: "aa".repeat(16) },
validation: {
status: "passed",
event: { kind: 9735, id: "creator-receipt" },
},
},
{
type: "platform",
status: "error",
amount: 100,
address: platformAddress,
error: shareError,
},
];
const walletError = new Error("Budget exceeded");
walletError.__zapShareTracker = tracker;
modalStub.statusMessages = [];
modalStub.retryState = undefined;
app.zapController.handleZapError({
error: walletError,
amount: 1000,
comment: "",
walletSettings: { nwcUri: "nostr+walletconnect://example" },
});
const lastStatus =
modalStub.statusMessages[modalStub.statusMessages.length - 1];
assert.ok(lastStatus, "status message should be recorded");
assert.equal(
lastStatus.tone,
"warning",
"partial allowance exhaustion should produce a warning tone",
);
assert.ok(
/zap allowance/i.test(lastStatus.message),
"status message should mention zap allowance exhaustion",
);
assert.ok(
/increase your wallet zap limit or reduce the platform split/i.test(
lastStatus.message,
),
"status message should advise adjusting allowance or platform split",
);
assert.equal(
lastStatus.message.includes("Press Send again"),
false,
"allowance exhaustion should not prompt a retry",
);
assert.equal(
app.zapController.modalZapRetryState,
null,
"controller should clear retry state when allowance exhausted",
);
assert.equal(
modalStub.retryState?.pending,
false,
"modal should not mark retry pending when allowance exhausted",
);
app.destroy();
})();
const { VideoModal } = await import("../js/ui/components/VideoModal.js");
(() => {
const modal = new VideoModal({ document: documentStub, setGlobalModalState: noop });
modal.modalZapReceipts = new DummyElement();
const receipts = [
{
recipientType: "creator",
amount: 600,
address: "[email protected]",
payment: { result: { preimage: "aa".repeat(16) } },
validation: {
status: "passed",
event: { kind: 9735, id: "validated" },
},
},
];
modal.renderZapReceipts(receipts, { partial: false });
assert.equal(modal.modalZapReceipts.children.length, 1, "validated receipt should render one entry");
const entry = modal.modalZapReceipts.children[0];
const header = entry.children[0];
assert.equal(header.children[1].textContent, "Validated", "validated receipt should flag status");
})();
(() => {
const modal = new VideoModal({ document: documentStub, setGlobalModalState: noop });
modal.modalZapReceipts = new DummyElement();
const receipts = [
{
recipientType: "creator",
amount: 450,
address: "[email protected]",
payment: { result: { preimage: "bb".repeat(16) } },
validation: {
status: "failed",
reason: "No compliant receipt",
},
},
];
modal.renderZapReceipts(receipts, { partial: true });
assert.equal(
modal.modalZapReceipts.children.length,
1,
"validation failure should render a single warning entry",
);
const warning = modal.modalZapReceipts.children[0];
assert.match(
warning.textContent,
/no validated zap receipts yet/i,
"warning message should note missing validated receipts",
);
})();
process.exit(0);