Files
bitvid/tests/video-card-source-visibility.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 2e510b2330 fix(liveness): don't bury WebTorrent-only content under hide-until-verified
A WebTorrent-only card's sole liveness signal is the swarm probe (20s timeout,
5min cache), and P2P liveness is slow + intermittent — a live-but-slow video is
indistinguishable from a dead one until that probe finishes. Hiding such a card
until the swarm verifies could bury perfectly good P2P content for minutes.

hide-until-verified now only suppresses a card while the FAST, reliable CDN probe
is still running. Once the CDN resolves dead (or is absent, i.e. P2P-only), it
falls back to show-pending for the WebTorrent part: shown while the swarm is
checked, hidden only once it too is confirmed dead. Burying live content is worse
than a brief flash of a dead one, and only the slow P2P probe is at issue.
cardNeedsEagerLivenessProbe likewise skips P2P-only cards (they start visible).

Tests: hide-foreign does-not-bury-WebTorrent-only coverage in
tests/video-card-source-visibility.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 09:48:55 -04:00

410 lines
14 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import {
updateVideoCardSourceVisibility,
cardNeedsEagerLivenessProbe,
} from "../js/utils/cardSourceVisibility.js";
import { setCardLivenessPolicy } from "../js/constants.js";
import { VideoCard } from "../js/ui/components/VideoCard.js";
function setupDom(t) {
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
url: "https://example.com",
pretendToBeVisual: true,
});
const { window } = dom;
const previous = new Map();
const keys = [
"window",
"document",
"HTMLElement",
"Element",
"Node",
"MouseEvent",
"CustomEvent",
"ResizeObserver",
"IntersectionObserver",
"CSSStyleSheet",
"requestAnimationFrame",
"cancelAnimationFrame",
];
keys.forEach((key) => {
const hadValue = Object.prototype.hasOwnProperty.call(globalThis, key);
previous.set(key, hadValue ? globalThis[key] : Symbol.for("__undefined__"));
if (key === "requestAnimationFrame" && typeof window[key] !== "function") {
window.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
} else if (key === "cancelAnimationFrame" && typeof window[key] !== "function") {
window.cancelAnimationFrame = (id) => clearTimeout(id);
} else if (key === "ResizeObserver" && typeof window[key] !== "function") {
window.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
} else if (
key === "IntersectionObserver" &&
typeof window[key] !== "function"
) {
// JSDOM doesn't implement IntersectionObserver; VideoCard.observeViewport
// constructs one at build time. A no-op stub is enough for these tests.
window.IntersectionObserver = class {
constructor() {}
observe() {}
unobserve() {}
disconnect() {}
takeRecords() {
return [];
}
};
}
globalThis[key] = window[key];
});
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
globalThis.requestAnimationFrame = window.requestAnimationFrame;
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => clearTimeout(id);
globalThis.cancelAnimationFrame = window.cancelAnimationFrame;
}
if (!window.ResizeObserver) {
class ResizeObserverStub {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserverStub;
globalThis.ResizeObserver = ResizeObserverStub;
}
t.after(() => {
dom.window.close();
keys.forEach((key) => {
const previousValue = previous.get(key);
if (previousValue === Symbol.for("__undefined__")) {
delete globalThis[key];
} else {
globalThis[key] = previousValue;
}
});
});
return { window, document: window.document };
}
test("updateVideoCardSourceVisibility hides non-owner cards without healthy sources and restores visibility", (t) => {
const { document } = setupDom(t);
const createCard = ({ owner = "false", urlState = "offline", streamState = "unhealthy" } = {}) => {
const card = document.createElement("article");
card.classList.add("card");
card.dataset.ownerIsViewer = owner;
card.dataset.urlHealthState = urlState;
card.dataset.streamHealthState = streamState;
document.body.appendChild(card);
return card;
};
const viewerCard = createCard();
updateVideoCardSourceVisibility(viewerCard);
assert.equal(viewerCard.hidden, true, "non-owners should be hidden when all sources fail");
assert.equal(viewerCard.dataset.sourceVisibility, "hidden", "hidden cards should record a hidden state");
const ownerCard = createCard({ owner: "true" });
ownerCard.hidden = true;
ownerCard.dataset.sourceVisibility = "hidden";
updateVideoCardSourceVisibility(ownerCard);
assert.equal(ownerCard.hidden, false, "owners should remain visible despite failing sources");
assert.equal(ownerCard.dataset.sourceVisibility, "visible", "owner visibility should be marked explicitly");
viewerCard.dataset.streamHealthState = "healthy";
updateVideoCardSourceVisibility(viewerCard);
assert.equal(viewerCard.hidden, false, "cards should unhide once a source becomes healthy");
assert.equal(viewerCard.dataset.sourceVisibility, "visible", "recovering cards should record visible state");
});
test("VideoCard hides cards without playable sources until a healthy CDN update arrives", (t) => {
const { document } = setupDom(t);
const card = new VideoCard({
document,
video: {
id: "event-123",
title: "Unplayable Clip",
},
formatters: {
formatTimeAgo: () => "just now",
},
helpers: {
isMagnetSupported: () => false,
},
});
const root = card.getRoot();
document.body.appendChild(root);
assert.equal(root.hidden, true, "cards without playable sources should start hidden");
assert.equal(root.dataset.sourceVisibility, "hidden", "initial visibility state should be hidden");
root.dataset.urlHealthState = "healthy";
updateVideoCardSourceVisibility(root);
assert.equal(root.hidden, false, "a healthy CDN update should unhide the card");
assert.equal(root.dataset.sourceVisibility, "visible", "visibility dataset should reflect the restored state");
});
function makeCard(
document,
{
foreign = "false",
owner = "false",
url = "checking",
stream = "checking",
cdnUrl = "https://cdn.example/v.mp4", // pass "" for a WebTorrent-only card
} = {}
) {
const card = document.createElement("article");
card.classList.add("card");
card.dataset.ownerIsViewer = owner;
card.dataset.foreign = foreign;
card.dataset.urlHealthState = url;
card.dataset.streamHealthState = stream;
if (cdnUrl) {
card.dataset.urlHealthUrl = encodeURIComponent(cdnUrl);
}
document.body.appendChild(card);
return card;
}
test("CARD_LIVENESS_POLICY=show-pending keeps a pending foreign card visible (current behavior)", (t) => {
const { document } = setupDom(t);
setCardLivenessPolicy("show-pending");
t.after(() => setCardLivenessPolicy("show-pending"));
// Probes still running (checking) → the card shows; it only hides once every
// source is confirmed dead.
const card = makeCard(document, { foreign: "true", url: "checking", stream: "checking" });
updateVideoCardSourceVisibility(card);
assert.equal(card.hidden, false, "pending foreign card is visible under show-pending");
card.dataset.urlHealthState = "offline";
card.dataset.streamHealthState = "unhealthy";
updateVideoCardSourceVisibility(card);
assert.equal(card.hidden, true, "confirmed-dead card hides");
});
test("CARD_LIVENESS_POLICY=hide-foreign hides pending FOREIGN cards but shows pending NATIVE cards", (t) => {
const { document } = setupDom(t);
setCardLivenessPolicy("hide-foreign");
t.after(() => setCardLivenessPolicy("show-pending"));
const foreignCard = makeCard(document, { foreign: "true", url: "checking", stream: "checking" });
updateVideoCardSourceVisibility(foreignCard);
assert.equal(foreignCard.hidden, true, "foreign card stays hidden until a source verifies");
const nativeCard = makeCard(document, { foreign: "false", url: "checking", stream: "checking" });
updateVideoCardSourceVisibility(nativeCard);
assert.equal(nativeCard.hidden, false, "native card keeps show-pending behavior");
// A foreign card becomes visible the moment any source proves playable.
foreignCard.dataset.streamHealthState = "healthy";
updateVideoCardSourceVisibility(foreignCard);
assert.equal(foreignCard.hidden, false, "verified foreign card appears");
});
test("CARD_LIVENESS_POLICY=hide-all hides every pending non-owner card but never the owner's", (t) => {
const { document } = setupDom(t);
setCardLivenessPolicy("hide-all");
t.after(() => setCardLivenessPolicy("show-pending"));
const nativeCard = makeCard(document, { foreign: "false", url: "checking", stream: "checking" });
updateVideoCardSourceVisibility(nativeCard);
assert.equal(nativeCard.hidden, true, "even a native card hides until verified under hide-all");
const ownerCard = makeCard(document, { owner: "true", url: "checking", stream: "checking" });
updateVideoCardSourceVisibility(ownerCard);
assert.equal(ownerCard.hidden, false, "the owner's own card is always visible");
nativeCard.dataset.urlHealthState = "healthy";
updateVideoCardSourceVisibility(nativeCard);
assert.equal(nativeCard.hidden, false, "card appears once a source verifies");
});
// WebTorrent-only foreign content must NOT get buried under hide-foreign: its
// only signal is the slow/unreliable swarm probe (20s + 5min cache), so a
// live-but-slow P2P video would otherwise stay hidden. It falls back to
// show-pending instead (shown while checked, hidden only once confirmed dead).
test("hide-foreign does not bury WebTorrent-only foreign cards (no CDN to verify fast)", (t) => {
const { document } = setupDom(t);
setCardLivenessPolicy("hide-foreign");
t.after(() => setCardLivenessPolicy("show-pending"));
// No CDN url → urlHealthState resolves to "offline"; stream probe still running.
const p2pCard = makeCard(document, {
foreign: "true",
cdnUrl: "",
url: "offline",
stream: "checking",
});
updateVideoCardSourceVisibility(p2pCard);
assert.equal(p2pCard.hidden, false, "P2P-only foreign card is shown while its swarm is checked, not buried");
assert.equal(
cardNeedsEagerLivenessProbe(p2pCard),
false,
"P2P-only card is show-pending, so it is not eager-probed",
);
// Only once the swarm probe also confirms it dead does it hide.
p2pCard.dataset.streamHealthState = "unhealthy";
updateVideoCardSourceVisibility(p2pCard);
assert.equal(p2pCard.hidden, true, "confirmed-dead P2P card finally hides");
// A CDN-backed foreign card, by contrast, IS hidden while its fast probe runs.
const cdnCard = makeCard(document, { foreign: "true", url: "checking", stream: "checking" });
updateVideoCardSourceVisibility(cdnCard);
assert.equal(cdnCard.hidden, true, "CDN-backed foreign card stays hidden during its fast probe");
});
// Hide-until-verified cards start display:none, so the viewport observer can't
// fire for them — they must be eager-probed. This predicate gates that, and it
// must mirror the visibility policy exactly so the right cards get probed.
test("cardNeedsEagerLivenessProbe gates eager probing to hidden-until-verified cards", (t) => {
const { document } = setupDom(t);
t.after(() => setCardLivenessPolicy("show-pending"));
const foreign = makeCard(document, { foreign: "true" });
const native = makeCard(document, { foreign: "false" });
const owner = makeCard(document, { foreign: "true", owner: "true" });
setCardLivenessPolicy("show-pending");
assert.equal(cardNeedsEagerLivenessProbe(foreign), false, "show-pending never eager-probes");
assert.equal(cardNeedsEagerLivenessProbe(native), false);
setCardLivenessPolicy("hide-foreign");
assert.equal(cardNeedsEagerLivenessProbe(foreign), true, "foreign card is eager-probed");
assert.equal(cardNeedsEagerLivenessProbe(native), false, "native card stays viewport-gated");
assert.equal(cardNeedsEagerLivenessProbe(owner), false, "owner card is never gated");
setCardLivenessPolicy("hide-all");
assert.equal(cardNeedsEagerLivenessProbe(native), true, "hide-all eager-probes native too");
assert.equal(cardNeedsEagerLivenessProbe(owner), false, "owner card is never gated");
});
test("VideoCard.closeMoreMenu only restores focus when the trigger was expanded", (t) => {
const { document } = setupDom(t);
const card = new VideoCard({
document,
video: {
id: "event-focus",
title: "Focusable clip",
},
formatters: {
formatTimeAgo: () => "just now",
},
helpers: {
isMagnetSupported: () => true,
},
});
const root = card.getRoot();
document.body.appendChild(root);
const trigger = card.moreMenuButton;
assert.ok(trigger, "the more menu trigger should exist");
let focusCalls = 0;
trigger.focus = () => {
focusCalls += 1;
};
card.onCloseMoreMenu = () => false;
card.closeMoreMenu();
assert.equal(
focusCalls,
0,
"collapsed menus should not restore focus when closing",
);
trigger.setAttribute("aria-expanded", "true");
card.closeMoreMenu();
assert.equal(
focusCalls,
1,
"expanded menus should restore focus back to the trigger",
);
trigger.setAttribute("aria-expanded", "true");
card.closeMoreMenu({ restoreFocus: false });
assert.equal(
focusCalls,
1,
"restoreFocus=false should suppress focus restoration",
);
});
test("VideoCard.closeSettingsMenu only restores focus when the trigger was expanded", (t) => {
const { document } = setupDom(t);
const card = new VideoCard({
document,
video: {
id: "event-settings",
title: "Editable clip",
},
capabilities: {
canEdit: true,
},
formatters: {
formatTimeAgo: () => "just now",
},
helpers: {
isMagnetSupported: () => true,
},
});
const root = card.getRoot();
document.body.appendChild(root);
const trigger = card.settingsButton;
assert.ok(trigger, "the settings trigger should exist when canEdit is true");
let focusCalls = 0;
trigger.focus = () => {
focusCalls += 1;
};
card.onCloseSettingsMenu = () => false;
card.closeSettingsMenu();
assert.equal(
focusCalls,
0,
"collapsed settings menus should not restore focus when closing",
);
trigger.setAttribute("aria-expanded", "true");
card.closeSettingsMenu();
assert.equal(
focusCalls,
1,
"expanded settings menus should restore focus back to the trigger",
);
trigger.setAttribute("aria-expanded", "true");
card.closeSettingsMenu({ restoreFocus: false });
assert.equal(
focusCalls,
1,
"restoreFocus=false should suppress settings focus restoration",
);
});