mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
524 lines
17 KiB
JavaScript
524 lines
17 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { JSDOM } from "jsdom";
|
|
|
|
import "../test-helpers/setup-localstorage.mjs";
|
|
|
|
import {
|
|
renderChannelVideosFromList,
|
|
applyChannelVisualBlur,
|
|
__setChannelProfileTestState,
|
|
__ensureChannelModerationEventsForTests,
|
|
} from "../../js/channelProfile.js";
|
|
import { nostrClient } from "../../js/nostrClientFacade.js";
|
|
import moderationService from "../../js/services/moderationService.js";
|
|
import { setApplication } from "../../js/applicationContext.js";
|
|
import { normalizeVideoModerationContext } from "../../js/ui/moderationUiHelpers.js";
|
|
import { buildModerationBadgeText } from "../../js/ui/moderationCopy.js";
|
|
import { withMockedNostrTools, createModerationAppHarness } from "../helpers/moderation-test-helpers.mjs";
|
|
import { waitFor } from "../test-helpers/wait-for.mjs";
|
|
|
|
function setupDom(t) {
|
|
const dom = new JSDOM(
|
|
"<!DOCTYPE html><html><body>" +
|
|
'<div id="channelModerationBadge"></div>' +
|
|
'<div id="channelBanner"></div>' +
|
|
'<img id="channelAvatar" />' +
|
|
'<div id="channelVideoList"></div>' +
|
|
"</body></html>",
|
|
{ url: "https://example.invalid/channel" },
|
|
);
|
|
|
|
const { window } = dom;
|
|
const { document } = window;
|
|
|
|
const previousGlobals = {
|
|
window: globalThis.window,
|
|
document: globalThis.document,
|
|
HTMLElement: globalThis.HTMLElement,
|
|
CustomEvent: globalThis.CustomEvent,
|
|
Event: globalThis.Event,
|
|
Node: globalThis.Node,
|
|
location: globalThis.location,
|
|
MutationObserver: globalThis.MutationObserver,
|
|
IntersectionObserver: globalThis.IntersectionObserver,
|
|
};
|
|
|
|
globalThis.window = window;
|
|
globalThis.document = document;
|
|
globalThis.HTMLElement = window.HTMLElement;
|
|
globalThis.CustomEvent = window.CustomEvent;
|
|
globalThis.Event = window.Event;
|
|
globalThis.Node = window.Node;
|
|
Object.defineProperty(globalThis, "navigator", {
|
|
value: window.navigator,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
globalThis.location = window.location;
|
|
globalThis.MutationObserver = window.MutationObserver;
|
|
globalThis.IntersectionObserver =
|
|
window.IntersectionObserver ||
|
|
class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
t.after(() => {
|
|
Object.entries(previousGlobals).forEach(([key, value]) => {
|
|
if (typeof value === "undefined") {
|
|
delete globalThis[key];
|
|
} else {
|
|
globalThis[key] = value;
|
|
}
|
|
});
|
|
});
|
|
|
|
return { document, window };
|
|
}
|
|
|
|
function createVideoFixture(overrides = {}) {
|
|
return {
|
|
id: overrides.id || "v".repeat(64),
|
|
title: overrides.title || "Fixture Video",
|
|
pubkey: overrides.pubkey || "a".repeat(64),
|
|
created_at: overrides.created_at || Math.floor(Date.now() / 1000),
|
|
url: overrides.url || "https://example.invalid/video.mp4",
|
|
moderation: overrides.moderation || {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("renderChannelVideosFromList decorates videos before storing", async (t) => {
|
|
const { document } = setupDom(t);
|
|
withMockedNostrTools(t);
|
|
|
|
nostrClient.allEvents = new Map();
|
|
nostrClient.rawEvents = new Set();
|
|
|
|
const app = await createModerationAppHarness();
|
|
app.loadedThumbnails = new Map();
|
|
app.videosMap = new Map();
|
|
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
|
app.formatTimeAgo = () => "just now";
|
|
app.ensureGlobalMoreMenuHandlers = () => {};
|
|
app.closeAllMoreMenus = () => {};
|
|
app.normalizeHexPubkey = (value) =>
|
|
typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
app.hasOlderVersion = () => false;
|
|
app.deriveVideoPointerInfo = () => null;
|
|
app.persistWatchHistoryMetadataForVideo = () => {};
|
|
app.canCurrentUserManageBlacklist = () => false;
|
|
app.isMagnetUriSupported = () => false;
|
|
app.mountVideoListView = () => {};
|
|
app.mediaLoader = { observe() {} };
|
|
app.attachMoreMenuHandlers = () => {};
|
|
app.decorateVideoModeration = (video) => video;
|
|
|
|
const originalDecorate = app.decorateVideoModeration;
|
|
let decoratedCount = 0;
|
|
app.decorateVideoModeration = function decorated(video) {
|
|
decoratedCount += 1;
|
|
if (!video.moderation || typeof video.moderation !== "object") {
|
|
video.moderation = {};
|
|
}
|
|
video.moderation.decoratedByTest = true;
|
|
return originalDecorate.call(this, video);
|
|
};
|
|
|
|
const container = document.getElementById("channelVideoList");
|
|
const video = createVideoFixture();
|
|
|
|
const rendered = await renderChannelVideosFromList({
|
|
videos: [video],
|
|
container,
|
|
app,
|
|
loadToken: 0,
|
|
});
|
|
|
|
assert.equal(rendered, true);
|
|
assert.equal(decoratedCount, 1);
|
|
const stored = app.videosMap.get(video.id);
|
|
assert.ok(stored, "decorated video stored in videosMap");
|
|
assert.equal(stored.moderation.decoratedByTest, true);
|
|
});
|
|
|
|
test("renderChannelVideosFromList applies moderation blur without existing metadata", async (t) => {
|
|
const { document } = setupDom(t);
|
|
withMockedNostrTools(t);
|
|
|
|
nostrClient.allEvents = new Map();
|
|
nostrClient.rawEvents = new Set();
|
|
|
|
const app = await createModerationAppHarness();
|
|
app.loadedThumbnails = new Map();
|
|
app.videosMap = new Map();
|
|
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
|
app.formatTimeAgo = () => "just now";
|
|
app.ensureGlobalMoreMenuHandlers = () => {};
|
|
app.closeAllMoreMenus = () => {};
|
|
app.normalizeHexPubkey = (value) =>
|
|
typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
app.hasOlderVersion = () => false;
|
|
app.deriveVideoPointerInfo = () => null;
|
|
app.persistWatchHistoryMetadataForVideo = () => {};
|
|
app.canCurrentUserManageBlacklist = () => false;
|
|
app.isMagnetUriSupported = () => false;
|
|
app.mountVideoListView = () => {};
|
|
app.mediaLoader = { observe() {} };
|
|
app.attachMoreMenuHandlers = () => {};
|
|
app.getActiveModerationThresholds = () => ({
|
|
blurThreshold: 1,
|
|
autoplayBlockThreshold: Number.POSITIVE_INFINITY,
|
|
trustedMuteHideThreshold: Number.POSITIVE_INFINITY,
|
|
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
|
|
});
|
|
|
|
const stubModerationService = {
|
|
async refreshViewerFromClient() {},
|
|
async setActiveEventIds() {},
|
|
getAdminListSnapshot() {
|
|
return null;
|
|
},
|
|
getAccessControlStatus() {
|
|
return { hex: "", whitelisted: false, blacklisted: false };
|
|
},
|
|
getTrustedReportSummary() {
|
|
return { types: { nudity: { trusted: 2 } } };
|
|
},
|
|
trustedReportCount() {
|
|
return 2;
|
|
},
|
|
getTrustedReporters() {
|
|
return [];
|
|
},
|
|
isAuthorMutedByTrusted() {
|
|
return false;
|
|
},
|
|
};
|
|
|
|
app.nostrService = {
|
|
getModerationService() {
|
|
return stubModerationService;
|
|
},
|
|
};
|
|
|
|
const container = document.getElementById("channelVideoList");
|
|
const channelPubkey = "e".repeat(64);
|
|
__setChannelProfileTestState({ pubkey: channelPubkey });
|
|
|
|
const video = createVideoFixture({ pubkey: channelPubkey });
|
|
delete video.moderation;
|
|
|
|
const rendered = await renderChannelVideosFromList({
|
|
videos: [video],
|
|
container,
|
|
app,
|
|
loadToken: 0,
|
|
});
|
|
|
|
assert.equal(rendered, true, "channel render succeeded with moderation stage");
|
|
const stored = app.videosMap.get(video.id);
|
|
assert.ok(stored, "video stored in app map after moderation");
|
|
assert.equal(stored.moderation?.blurThumbnail, true, "moderation blur applied");
|
|
|
|
const thumbnail = container.querySelector('[data-video-thumbnail="true"]');
|
|
assert.ok(thumbnail, "video card thumbnail rendered");
|
|
assert.equal(thumbnail.dataset.thumbnailState, "blurred");
|
|
|
|
const bannerEl = document.getElementById("channelBanner");
|
|
assert.equal(bannerEl.dataset.visualState, "blurred");
|
|
});
|
|
|
|
test("applyChannelVisualBlur blurs banner and avatar when viewer mutes author", async (t) => {
|
|
const { document } = setupDom(t);
|
|
withMockedNostrTools(t);
|
|
|
|
const bannerEl = document.getElementById("channelBanner");
|
|
const avatarEl = document.getElementById("channelAvatar");
|
|
const pubkey = "b".repeat(64);
|
|
|
|
const originalMute = moderationService.isAuthorMutedByViewer;
|
|
moderationService.isAuthorMutedByViewer = () => true;
|
|
t.after(() => {
|
|
moderationService.isAuthorMutedByViewer = originalMute;
|
|
});
|
|
|
|
applyChannelVisualBlur({
|
|
bannerEl,
|
|
avatarEl,
|
|
pubkey,
|
|
app: { videosMap: new Map() },
|
|
});
|
|
|
|
assert.equal(bannerEl.dataset.visualState, "blurred");
|
|
assert.equal(avatarEl.dataset.visualState, "blurred");
|
|
});
|
|
|
|
test("moderation override clears channel blur via event wiring", async (t) => {
|
|
const { document } = setupDom(t);
|
|
withMockedNostrTools(t);
|
|
|
|
nostrClient.allEvents = new Map();
|
|
nostrClient.rawEvents = new Set();
|
|
|
|
const app = await createModerationAppHarness();
|
|
app.loadedThumbnails = new Map();
|
|
app.videosMap = new Map();
|
|
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
|
app.formatTimeAgo = () => "just now";
|
|
app.ensureGlobalMoreMenuHandlers = () => {};
|
|
app.closeAllMoreMenus = () => {};
|
|
app.normalizeHexPubkey = (value) =>
|
|
typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
app.hasOlderVersion = () => false;
|
|
app.deriveVideoPointerInfo = () => null;
|
|
app.persistWatchHistoryMetadataForVideo = () => {};
|
|
app.canCurrentUserManageBlacklist = () => false;
|
|
app.isMagnetUriSupported = () => false;
|
|
app.mountVideoListView = () => {};
|
|
app.mediaLoader = { observe() {} };
|
|
app.attachMoreMenuHandlers = () => {};
|
|
app.getActiveModerationThresholds = () => ({
|
|
blurThreshold: 1,
|
|
autoplayBlockThreshold: 1,
|
|
trustedMuteHideThreshold: 1,
|
|
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
|
|
});
|
|
|
|
const channelPubkey = "c".repeat(64);
|
|
__setChannelProfileTestState({ pubkey: channelPubkey });
|
|
__ensureChannelModerationEventsForTests();
|
|
|
|
const container = document.getElementById("channelVideoList");
|
|
const moderationState = {
|
|
// BitVid's current default blur threshold is three trusted reports.
|
|
trustedCount: 3,
|
|
summary: {
|
|
types: { nudity: { trusted: 3 } },
|
|
},
|
|
};
|
|
const video = createVideoFixture({
|
|
id: "a".repeat(64),
|
|
pubkey: channelPubkey,
|
|
moderation: moderationState,
|
|
});
|
|
|
|
await renderChannelVideosFromList({
|
|
videos: [video],
|
|
container,
|
|
app,
|
|
loadToken: 0,
|
|
});
|
|
|
|
const bannerEl = document.getElementById("channelBanner");
|
|
const avatarEl = document.getElementById("channelAvatar");
|
|
assert.equal(bannerEl.dataset.visualState, "blurred");
|
|
assert.equal(avatarEl.dataset.visualState, "blurred");
|
|
|
|
const cardStub = { refreshModerationUi() {} };
|
|
app.handleModerationOverride({ video, card: cardStub });
|
|
|
|
assert.equal(bannerEl.dataset.visualState, undefined);
|
|
assert.equal(avatarEl.dataset.visualState, undefined);
|
|
});
|
|
|
|
test("channel header moderation badge reflects blur state and override actions", async (t) => {
|
|
const { document } = setupDom(t);
|
|
withMockedNostrTools(t);
|
|
|
|
nostrClient.allEvents = new Map();
|
|
nostrClient.rawEvents = new Set();
|
|
|
|
const app = await createModerationAppHarness();
|
|
app.loadedThumbnails = new Map();
|
|
app.videosMap = new Map();
|
|
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
|
app.formatTimeAgo = () => "just now";
|
|
app.ensureGlobalMoreMenuHandlers = () => {};
|
|
app.closeAllMoreMenus = () => {};
|
|
app.normalizeHexPubkey = (value) =>
|
|
(typeof value === "string" ? value.trim().toLowerCase() : "");
|
|
app.hasOlderVersion = () => false;
|
|
app.deriveVideoPointerInfo = () => null;
|
|
app.persistWatchHistoryMetadataForVideo = () => {};
|
|
app.canCurrentUserManageBlacklist = () => false;
|
|
app.isMagnetUriSupported = () => false;
|
|
app.mountVideoListView = () => {};
|
|
app.mediaLoader = { observe() {} };
|
|
app.attachMoreMenuHandlers = () => {};
|
|
app.getActiveModerationThresholds = () => ({
|
|
blurThreshold: 1,
|
|
autoplayBlockThreshold: 1,
|
|
trustedMuteHideThreshold: 1,
|
|
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
|
|
});
|
|
let overrideCalls = 0;
|
|
app.handleModerationOverride = () => {
|
|
overrideCalls += 1;
|
|
return true;
|
|
};
|
|
|
|
setApplication(app);
|
|
t.after(() => {
|
|
setApplication(null);
|
|
});
|
|
|
|
const channelPubkey = "d".repeat(64);
|
|
__setChannelProfileTestState({ pubkey: channelPubkey });
|
|
__ensureChannelModerationEventsForTests();
|
|
|
|
const container = document.getElementById("channelVideoList");
|
|
const moderationState = {
|
|
blurThumbnail: true,
|
|
trustedCount: 2,
|
|
reportType: "nudity",
|
|
summary: { types: { nudity: { trusted: 2 } } },
|
|
original: { blurThumbnail: true, blockAutoplay: false },
|
|
};
|
|
const video = createVideoFixture({ pubkey: channelPubkey, moderation: moderationState });
|
|
|
|
await renderChannelVideosFromList({
|
|
videos: [video],
|
|
container,
|
|
app,
|
|
loadToken: 0,
|
|
});
|
|
|
|
applyChannelVisualBlur({ app, pubkey: channelPubkey });
|
|
const storedVideo = app.videosMap.get(video.id);
|
|
|
|
const bannerEl = document.getElementById("channelBanner");
|
|
const avatarEl = document.getElementById("channelAvatar");
|
|
assert.equal(bannerEl.dataset.visualState, "blurred");
|
|
assert.equal(avatarEl.dataset.visualState, "blurred");
|
|
|
|
const badgeContainer = document.getElementById("channelModerationBadge");
|
|
const badge = badgeContainer.querySelector('[data-moderation-badge="true"]');
|
|
assert.ok(badge, "badge renders in channel header");
|
|
|
|
const textEl = badge.querySelector(".moderation-badge__text");
|
|
const context = normalizeVideoModerationContext(storedVideo.moderation);
|
|
const expectedText = buildModerationBadgeText(context, { variant: "card" });
|
|
assert.equal(textEl.textContent, expectedText);
|
|
|
|
const overrideButton = badge.querySelector('[data-moderation-action="override"]');
|
|
assert.ok(overrideButton, "override control is available");
|
|
|
|
overrideButton.click();
|
|
await waitFor(() => {
|
|
assert.equal(overrideCalls, 1);
|
|
});
|
|
const updatedVideo = app.videosMap.get(video.id);
|
|
if (!updatedVideo.moderation || typeof updatedVideo.moderation !== "object") {
|
|
updatedVideo.moderation = {};
|
|
}
|
|
updatedVideo.moderation.viewerOverride = { showAnyway: true };
|
|
updatedVideo.moderation.blurThumbnail = false;
|
|
updatedVideo.moderation.blockAutoplay = false;
|
|
const originalState = updatedVideo.moderation.original && typeof updatedVideo.moderation.original === "object"
|
|
? { ...updatedVideo.moderation.original }
|
|
: {};
|
|
if (typeof originalState.blurThumbnail === "undefined") {
|
|
originalState.blurThumbnail = true;
|
|
}
|
|
if (typeof originalState.blockAutoplay === "undefined") {
|
|
originalState.blockAutoplay = true;
|
|
}
|
|
updatedVideo.moderation.original = originalState;
|
|
app.videosMap.set(updatedVideo.id, updatedVideo);
|
|
const overrideContext = normalizeVideoModerationContext(updatedVideo.moderation);
|
|
const overrideText = buildModerationBadgeText(overrideContext, { variant: "card" });
|
|
|
|
assert.equal(overrideContext.overrideActive, true);
|
|
assert.ok(overrideText.startsWith("Showing despite"));
|
|
});
|
|
|
|
test("channel video cards update moderation state in place when summary changes", async (t) => {
|
|
const { document } = setupDom(t);
|
|
withMockedNostrTools(t);
|
|
|
|
nostrClient.allEvents = new Map();
|
|
nostrClient.rawEvents = new Set();
|
|
|
|
const app = await createModerationAppHarness();
|
|
app.loadedThumbnails = new Map();
|
|
app.videosMap = new Map();
|
|
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
|
app.formatTimeAgo = () => "just now";
|
|
app.ensureGlobalMoreMenuHandlers = () => {};
|
|
app.closeAllMoreMenus = () => {};
|
|
app.normalizeHexPubkey = (value) =>
|
|
typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
app.hasOlderVersion = () => false;
|
|
app.deriveVideoPointerInfo = () => null;
|
|
app.persistWatchHistoryMetadataForVideo = () => {};
|
|
app.canCurrentUserManageBlacklist = () => false;
|
|
app.isMagnetUriSupported = () => false;
|
|
app.mountVideoListView = () => {};
|
|
app.mediaLoader = { observe() {} };
|
|
app.attachMoreMenuHandlers = () => {};
|
|
app.getActiveModerationThresholds = () => ({
|
|
blurThreshold: 1,
|
|
autoplayBlockThreshold: Number.POSITIVE_INFINITY,
|
|
trustedMuteHideThreshold: Number.POSITIVE_INFINITY,
|
|
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
|
|
});
|
|
|
|
setApplication(app);
|
|
t.after(() => setApplication(null));
|
|
|
|
const video = createVideoFixture();
|
|
__setChannelProfileTestState({ pubkey: video.pubkey });
|
|
__ensureChannelModerationEventsForTests();
|
|
|
|
const container = document.getElementById("channelVideoList");
|
|
await renderChannelVideosFromList({
|
|
videos: [video],
|
|
container,
|
|
app,
|
|
loadToken: 0,
|
|
});
|
|
|
|
const cardEl = container.querySelector('[data-component="video-card"]');
|
|
assert.ok(cardEl, "video card rendered for channel");
|
|
const thumbnailEl = cardEl.querySelector('[data-video-thumbnail="true"]');
|
|
assert.ok(thumbnailEl, "card thumbnail present");
|
|
assert.equal(
|
|
thumbnailEl.dataset.thumbnailState || "",
|
|
"",
|
|
"thumbnail should not be blurred initially",
|
|
);
|
|
|
|
const existing = app.videosMap.get(video.id) || video;
|
|
const updatedVideo = {
|
|
...existing,
|
|
moderation: {
|
|
...(existing?.moderation || {}),
|
|
summary: {
|
|
types: {
|
|
nudity: { trusted: 3 },
|
|
},
|
|
},
|
|
trustedCount: 3,
|
|
blurThumbnail: true,
|
|
reportType: "nudity",
|
|
},
|
|
};
|
|
app.videosMap.set(video.id, updatedVideo);
|
|
|
|
const priorCard = container.querySelector('[data-component="video-card"]');
|
|
|
|
moderationService.emit("summary", { eventId: video.id });
|
|
|
|
const currentCard = container.querySelector('[data-component="video-card"]');
|
|
assert.strictEqual(currentCard, priorCard, "card should not rerender");
|
|
assert.equal(thumbnailEl.dataset.thumbnailState, "blurred");
|
|
assert.equal(cardEl.dataset.moderationReportType, "nudity");
|
|
assert.equal(cardEl.dataset.moderationReportCount, "3");
|
|
});
|