diff --git a/tests/video-card-source-visibility.test.mjs b/tests/video-card-source-visibility.test.mjs new file mode 100644 index 00000000..93e48610 --- /dev/null +++ b/tests/video-card-source-visibility.test.mjs @@ -0,0 +1,141 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; + +import { updateVideoCardSourceVisibility } from "../js/utils/cardSourceVisibility.js"; +import { VideoCard } from "../js/ui/components/VideoCard.js"; + +function setupDom(t) { + const dom = new JSDOM("", { + url: "https://example.com", + pretendToBeVisual: true, + }); + + const { window } = dom; + const previous = new Map(); + const keys = [ + "window", + "document", + "HTMLElement", + "Element", + "Node", + "MouseEvent", + "CustomEvent", + "ResizeObserver", + "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() {} + }; + } + 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"); +});