mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #706 from PR0M3TH3AN/codex/expand-tests-for-moderation-and-reporting
Expand moderation coverage and add video card override test
This commit is contained in:
4 files changed
+580
-89
No files matched your search
@@ -0,0 +1,205 @@
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import { ModerationService } from "../../js/services/moderationService.js";
|
||||
|
||||
const DEFAULT_CONTACT_OWNER = "f".repeat(64);
|
||||
|
||||
export function withMockedNostrTools(t) {
|
||||
const hadWindow = typeof globalThis.window !== "undefined";
|
||||
const previousWindow = hadWindow ? globalThis.window : undefined;
|
||||
if (!hadWindow) {
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
const previousGlobalTools = globalThis.NostrTools;
|
||||
const previousWindowTools = globalThis.window?.NostrTools;
|
||||
|
||||
const nip19 = {
|
||||
npubEncode(hex) {
|
||||
if (typeof hex !== "string" || !hex) {
|
||||
throw new Error("invalid hex");
|
||||
}
|
||||
return `npub${hex}`;
|
||||
},
|
||||
decode(value) {
|
||||
if (typeof value !== "string" || !value.startsWith("npub")) {
|
||||
throw new Error("invalid npub");
|
||||
}
|
||||
return { type: "npub", data: value.slice(4) };
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.NostrTools = { nip19 };
|
||||
if (globalThis.window) {
|
||||
globalThis.window.NostrTools = globalThis.NostrTools;
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
if (typeof previousGlobalTools === "undefined") {
|
||||
delete globalThis.NostrTools;
|
||||
} else {
|
||||
globalThis.NostrTools = previousGlobalTools;
|
||||
}
|
||||
|
||||
if (globalThis.window) {
|
||||
if (typeof previousWindowTools === "undefined") {
|
||||
delete globalThis.window.NostrTools;
|
||||
} else {
|
||||
globalThis.window.NostrTools = previousWindowTools;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hadWindow) {
|
||||
delete globalThis.window;
|
||||
} else {
|
||||
globalThis.window = previousWindow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createModerationServiceHarness(t, { userBlocks, accessControl } = {}) {
|
||||
const service = new ModerationService({
|
||||
logger: () => {},
|
||||
userBlocks:
|
||||
userBlocks || {
|
||||
async ensureLoaded() {},
|
||||
isBlocked() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
accessControl: accessControl || null,
|
||||
});
|
||||
|
||||
const tracked = new Map();
|
||||
|
||||
function capture(eventName) {
|
||||
if (!tracked.has(eventName)) {
|
||||
tracked.set(eventName, []);
|
||||
}
|
||||
const records = tracked.get(eventName);
|
||||
const unsubscribe = service.on(eventName, (detail) => {
|
||||
records.push(detail);
|
||||
});
|
||||
if (typeof unsubscribe === "function") {
|
||||
t.after(() => {
|
||||
try {
|
||||
unsubscribe();
|
||||
} catch {
|
||||
// noop for tests
|
||||
}
|
||||
});
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
return { service, capture, trackedEvents: tracked };
|
||||
}
|
||||
|
||||
export function createReportEvent({
|
||||
id,
|
||||
reporter,
|
||||
eventId,
|
||||
createdAt = Math.floor(Date.now() / 1000),
|
||||
type = "nudity",
|
||||
} = {}) {
|
||||
const reportId =
|
||||
typeof id === "string" && id.length
|
||||
? id
|
||||
: `${Math.random().toString(16).slice(2)}${"0".repeat(64)}`.slice(0, 64);
|
||||
const reporterPubkey = typeof reporter === "string" && reporter ? reporter : "";
|
||||
const targetEventId = typeof eventId === "string" && eventId ? eventId : "";
|
||||
const normalizedType = typeof type === "string" && type ? type : "nudity";
|
||||
|
||||
return {
|
||||
kind: 1984,
|
||||
id: reportId,
|
||||
pubkey: reporterPubkey,
|
||||
created_at: Number.isFinite(createdAt) ? Math.floor(createdAt) : Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["e", targetEventId],
|
||||
["report", normalizedType],
|
||||
],
|
||||
content: "fixture report",
|
||||
};
|
||||
}
|
||||
|
||||
export function createContactEvent({
|
||||
owner = DEFAULT_CONTACT_OWNER,
|
||||
contacts = [],
|
||||
createdAt = Math.floor(Date.now() / 1000),
|
||||
id,
|
||||
} = {}) {
|
||||
const eventId =
|
||||
typeof id === "string" && id.length
|
||||
? id
|
||||
: `${Math.random().toString(16).slice(2)}${"1".repeat(64)}`.slice(0, 64);
|
||||
const ownerHex = typeof owner === "string" && owner ? owner : DEFAULT_CONTACT_OWNER;
|
||||
const tags = contacts
|
||||
.filter((value) => typeof value === "string" && value)
|
||||
.map((value) => ["p", value]);
|
||||
|
||||
return {
|
||||
kind: 3,
|
||||
id: eventId,
|
||||
pubkey: ownerHex,
|
||||
created_at: Number.isFinite(createdAt) ? Math.floor(createdAt) : Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function applyTrustedContacts(service, contacts = []) {
|
||||
const event = createContactEvent({ contacts });
|
||||
service.applyContactEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
let cachedApplicationClass = null;
|
||||
|
||||
async function getApplicationClass() {
|
||||
if (!cachedApplicationClass) {
|
||||
if (typeof globalThis.self === "undefined") {
|
||||
globalThis.self = globalThis;
|
||||
}
|
||||
if (typeof globalThis.WebSocket === "undefined") {
|
||||
globalThis.WebSocket = class {
|
||||
constructor() {}
|
||||
close() {}
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
send() {}
|
||||
};
|
||||
}
|
||||
const module = await import("../../js/app.js");
|
||||
cachedApplicationClass = module.Application || module.default;
|
||||
}
|
||||
return cachedApplicationClass;
|
||||
}
|
||||
|
||||
export async function createModerationAppHarness() {
|
||||
const Application = await getApplicationClass();
|
||||
const app = Object.create(Application.prototype);
|
||||
|
||||
Object.defineProperty(app, "__profiles", {
|
||||
value: new Map(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
app.getProfileCacheEntry = (pubkey) => {
|
||||
if (!pubkey || typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
return app.__profiles.get(pubkey) || null;
|
||||
};
|
||||
|
||||
app.decorateVideoModeration = Application.prototype.decorateVideoModeration;
|
||||
app.deriveModerationReportType = Application.prototype.deriveModerationReportType;
|
||||
app.deriveModerationTrustedCount = Application.prototype.deriveModerationTrustedCount;
|
||||
app.getReporterDisplayName = Application.prototype.getReporterDisplayName;
|
||||
app.handleModerationOverride = Application.prototype.handleModerationOverride;
|
||||
|
||||
app.videosMap = new Map();
|
||||
app.currentVideo = null;
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -166,3 +166,132 @@ test("moderation stage annotates trusted mute metadata", async () => {
|
||||
assert.ok(trustedMuteReason);
|
||||
assert.equal(trustedMuteReason.videoId, "muted");
|
||||
});
|
||||
|
||||
test("moderation stage propagates whitelist, muters, and threshold updates", async () => {
|
||||
const whitelistedHex = "a".repeat(64);
|
||||
const mutedHex = "b".repeat(64);
|
||||
const muterHex = "c".repeat(64);
|
||||
|
||||
const state = {
|
||||
whitelistHex: new Set([whitelistedHex]),
|
||||
summaryById: new Map([
|
||||
[
|
||||
"muted-video",
|
||||
{
|
||||
eventId: "muted-video",
|
||||
totalTrusted: 3,
|
||||
types: { nudity: { trusted: 3, total: 3, latest: 1_700_000_000 } },
|
||||
updatedAt: 1_700_000_100,
|
||||
},
|
||||
],
|
||||
]),
|
||||
trustedCounts: new Map([
|
||||
["whitelisted-video", 4],
|
||||
["muted-video", 3],
|
||||
]),
|
||||
reportersById: new Map([
|
||||
["muted-video", [{ pubkey: muterHex, latest: 1_700_000_050 }]],
|
||||
]),
|
||||
mutedAuthors: new Set([mutedHex]),
|
||||
mutersByAuthor: new Map([[mutedHex, [muterHex]]]),
|
||||
};
|
||||
|
||||
const service = {
|
||||
async refreshViewerFromClient() {},
|
||||
async setActiveEventIds() {},
|
||||
getAdminListSnapshot() {
|
||||
return {
|
||||
whitelist: new Set([`npub${whitelistedHex}`]),
|
||||
whitelistHex: new Set(state.whitelistHex),
|
||||
blacklist: new Set(),
|
||||
blacklistHex: new Set(),
|
||||
};
|
||||
},
|
||||
getAccessControlStatus(identifier) {
|
||||
const normalized = typeof identifier === "string" ? identifier.trim().toLowerCase() : "";
|
||||
return {
|
||||
hex: normalized,
|
||||
whitelisted: state.whitelistHex.has(normalized),
|
||||
blacklisted: false,
|
||||
};
|
||||
},
|
||||
getTrustedReportSummary(videoId) {
|
||||
return state.summaryById.get(videoId) || null;
|
||||
},
|
||||
trustedReportCount(videoId) {
|
||||
return state.trustedCounts.get(videoId) || 0;
|
||||
},
|
||||
getTrustedReporters(videoId) {
|
||||
return state.reportersById.get(videoId) || [];
|
||||
},
|
||||
isAuthorMutedByTrusted(pubkey) {
|
||||
return state.mutedAuthors.has(pubkey);
|
||||
},
|
||||
getTrustedMutersForAuthor(pubkey) {
|
||||
return state.mutersByAuthor.get(pubkey) || [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({
|
||||
service,
|
||||
autoplayThreshold: 2,
|
||||
blurThreshold: 3,
|
||||
reportType: "nudity",
|
||||
});
|
||||
|
||||
const items = [
|
||||
{ video: { id: "whitelisted-video", pubkey: whitelistedHex }, metadata: {} },
|
||||
{ video: { id: "muted-video", pubkey: mutedHex }, metadata: {} },
|
||||
];
|
||||
|
||||
const reasons = [];
|
||||
const context = {
|
||||
feedName: "discovery",
|
||||
addWhy(detail) {
|
||||
reasons.push(detail);
|
||||
return detail;
|
||||
},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const firstPass = await stage(items, context);
|
||||
const whitelistedItem = firstPass.find((entry) => entry.video.id === "whitelisted-video");
|
||||
const mutedItem = firstPass.find((entry) => entry.video.id === "muted-video");
|
||||
|
||||
assert.ok(whitelistedItem);
|
||||
assert.ok(mutedItem);
|
||||
|
||||
assert.equal(whitelistedItem.metadata.moderation.adminWhitelist, true);
|
||||
assert.equal(whitelistedItem.metadata.moderation.blockAutoplay, false);
|
||||
assert.equal(whitelistedItem.metadata.moderation.blurThumbnail, false);
|
||||
assert.equal(whitelistedItem.video.moderation.adminWhitelistBypass, true);
|
||||
|
||||
assert.equal(mutedItem.metadata.moderation.trustedMuted, true);
|
||||
assert.deepEqual(mutedItem.metadata.moderation.trustedMuters, [muterHex]);
|
||||
assert.equal(mutedItem.metadata.moderation.blockAutoplay, true);
|
||||
assert.equal(mutedItem.metadata.moderation.blurThumbnail, true);
|
||||
assert.equal(mutedItem.video.moderation.trustedMuted, true);
|
||||
assert.deepEqual(mutedItem.video.moderation.trustedMuters, [muterHex]);
|
||||
|
||||
state.summaryById.set("muted-video", {
|
||||
eventId: "muted-video",
|
||||
totalTrusted: 1,
|
||||
types: { nudity: { trusted: 1, total: 1, latest: 1_700_000_500 } },
|
||||
updatedAt: 1_700_000_600,
|
||||
});
|
||||
state.trustedCounts.set("muted-video", 1);
|
||||
state.mutersByAuthor.set(mutedHex, []);
|
||||
state.mutedAuthors.delete(mutedHex);
|
||||
|
||||
const secondPass = await stage(firstPass, context);
|
||||
const updatedMuted = secondPass.find((entry) => entry.video.id === "muted-video");
|
||||
|
||||
assert.ok(updatedMuted);
|
||||
assert.equal(updatedMuted.metadata.moderation.trustedMuted, false);
|
||||
assert.deepEqual(updatedMuted.metadata.moderation.trustedMuters, []);
|
||||
assert.equal(updatedMuted.metadata.moderation.blockAutoplay, false);
|
||||
assert.equal(updatedMuted.metadata.moderation.blurThumbnail, false);
|
||||
assert.equal(updatedMuted.video.moderation.trustedMuted, false);
|
||||
assert.equal(updatedMuted.video.moderation.blockAutoplay, false);
|
||||
assert.equal(updatedMuted.video.moderation.blurThumbnail, false);
|
||||
});
|
||||
@@ -1,71 +1,14 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ModerationService } from "../../js/services/moderationService.js";
|
||||
import {
|
||||
withMockedNostrTools,
|
||||
createModerationServiceHarness,
|
||||
createReportEvent,
|
||||
applyTrustedContacts,
|
||||
} from "../helpers/moderation-test-helpers.mjs";
|
||||
|
||||
const nip19 = {
|
||||
npubEncode(hex) {
|
||||
if (typeof hex !== "string" || !hex) {
|
||||
throw new Error("invalid hex");
|
||||
}
|
||||
return `npub${hex}`;
|
||||
},
|
||||
decode(value) {
|
||||
if (typeof value !== "string" || !value.startsWith("npub")) {
|
||||
throw new Error("invalid npub");
|
||||
}
|
||||
return { type: "npub", data: value.slice(4) };
|
||||
},
|
||||
};
|
||||
|
||||
function withMockedNostrTools(t) {
|
||||
const hadWindow = typeof globalThis.window !== "undefined";
|
||||
const previousWindow = hadWindow ? globalThis.window : undefined;
|
||||
if (!hadWindow) {
|
||||
globalThis.window = {};
|
||||
}
|
||||
const previousGlobalTools = globalThis.NostrTools;
|
||||
const previousWindowTools = globalThis.window?.NostrTools;
|
||||
|
||||
globalThis.NostrTools = { nip19 };
|
||||
if (globalThis.window) {
|
||||
globalThis.window.NostrTools = globalThis.NostrTools;
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
if (typeof previousGlobalTools === "undefined") {
|
||||
delete globalThis.NostrTools;
|
||||
} else {
|
||||
globalThis.NostrTools = previousGlobalTools;
|
||||
}
|
||||
|
||||
if (globalThis.window) {
|
||||
if (typeof previousWindowTools === "undefined") {
|
||||
delete globalThis.window.NostrTools;
|
||||
} else {
|
||||
globalThis.window.NostrTools = previousWindowTools;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hadWindow) {
|
||||
delete globalThis.window;
|
||||
} else {
|
||||
globalThis.window = previousWindow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createReport({ id, reporter, eventId, createdAt, type = "nudity" }) {
|
||||
return {
|
||||
kind: 1984,
|
||||
id,
|
||||
pubkey: reporter,
|
||||
created_at: createdAt,
|
||||
tags: [
|
||||
["e", eventId],
|
||||
["report", type],
|
||||
],
|
||||
content: "fixture report",
|
||||
};
|
||||
function createReport(options) {
|
||||
return createReportEvent(options);
|
||||
}
|
||||
|
||||
test("trustedReportCount dedupes multiple reports from the same trusted reporter", (t) => {
|
||||
@@ -74,17 +17,9 @@ test("trustedReportCount dedupes multiple reports from the same trusted reporter
|
||||
const eventId = "f".repeat(64);
|
||||
const reporterHex = "a".repeat(64);
|
||||
|
||||
const service = new ModerationService({
|
||||
logger: () => {},
|
||||
userBlocks: {
|
||||
async ensureLoaded() {},
|
||||
isBlocked() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
const { service } = createModerationServiceHarness(t);
|
||||
|
||||
service.trustedContacts = new Set([reporterHex]);
|
||||
applyTrustedContacts(service, [reporterHex]);
|
||||
|
||||
const firstReport = createReport({
|
||||
id: "1".repeat(64),
|
||||
@@ -116,17 +51,16 @@ test("trustedReportCount ignores reports from muted or blocked reporters", (t) =
|
||||
const mutedReporter = "b".repeat(64);
|
||||
const trustedReporter = "c".repeat(64);
|
||||
|
||||
const service = new ModerationService({
|
||||
logger: () => {},
|
||||
userBlocks: {
|
||||
async ensureLoaded() {},
|
||||
isBlocked(pubkey) {
|
||||
return pubkey === mutedReporter;
|
||||
},
|
||||
const userBlocks = {
|
||||
async ensureLoaded() {},
|
||||
isBlocked(pubkey) {
|
||||
return pubkey === mutedReporter;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
service.trustedContacts = new Set([mutedReporter, trustedReporter]);
|
||||
const { service } = createModerationServiceHarness(t, { userBlocks });
|
||||
|
||||
applyTrustedContacts(service, [mutedReporter, trustedReporter]);
|
||||
|
||||
const mutedReport = createReport({
|
||||
id: "3".repeat(64),
|
||||
@@ -182,12 +116,9 @@ test("blocking and unblocking reporters recomputes trusted summaries", async (t)
|
||||
},
|
||||
};
|
||||
|
||||
const service = new ModerationService({
|
||||
logger: () => {},
|
||||
userBlocks: userBlocksMock,
|
||||
});
|
||||
const { service } = createModerationServiceHarness(t, { userBlocks: userBlocksMock });
|
||||
|
||||
service.trustedContacts = new Set([reporterHex]);
|
||||
applyTrustedContacts(service, [reporterHex]);
|
||||
|
||||
const report = createReport({
|
||||
id: "b".repeat(64),
|
||||
@@ -235,3 +166,65 @@ test("blocking and unblocking reporters recomputes trusted summaries", async (t)
|
||||
assert.equal(service.trustedReportCount(eventId, "nudity"), 1);
|
||||
assert.deepEqual(setActiveCalls, [[eventId], [eventId]]);
|
||||
});
|
||||
|
||||
test("trustedReportCount only counts eligible F1 reporters and admin whitelist", (t) => {
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const eventId = "c".repeat(64);
|
||||
const viewerBlocked = "1".repeat(64);
|
||||
const f1Reporter = "2".repeat(64);
|
||||
const f2Reporter = "3".repeat(64);
|
||||
const adminWhitelisted = "4".repeat(64);
|
||||
const adminBlacklisted = "5".repeat(64);
|
||||
|
||||
const userBlocks = {
|
||||
async ensureLoaded() {},
|
||||
isBlocked(pubkey) {
|
||||
return pubkey === viewerBlocked;
|
||||
},
|
||||
};
|
||||
|
||||
const accessControl = {
|
||||
getWhitelist() {
|
||||
return [`npub${adminWhitelisted}`];
|
||||
},
|
||||
getBlacklist() {
|
||||
return [`npub${adminBlacklisted}`];
|
||||
},
|
||||
};
|
||||
|
||||
const { service } = createModerationServiceHarness(t, { userBlocks, accessControl });
|
||||
|
||||
applyTrustedContacts(service, [f1Reporter, viewerBlocked]);
|
||||
|
||||
const reports = [
|
||||
{ id: "r1".repeat(32), reporter: f1Reporter, createdAt: 1_700_000_000 },
|
||||
{ id: "r2".repeat(32), reporter: viewerBlocked, createdAt: 1_700_000_050 },
|
||||
{ id: "r3".repeat(32), reporter: f2Reporter, createdAt: 1_700_000_100 },
|
||||
{ id: "r4".repeat(32), reporter: adminWhitelisted, createdAt: 1_700_000_200 },
|
||||
{ id: "r5".repeat(32), reporter: adminBlacklisted, createdAt: 1_700_000_300 },
|
||||
];
|
||||
|
||||
for (const entry of reports) {
|
||||
service.ingestReportEvent(
|
||||
createReport({
|
||||
id: entry.id.padEnd(64, entry.id[0]),
|
||||
reporter: entry.reporter,
|
||||
eventId,
|
||||
createdAt: entry.createdAt,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const summary = service.getTrustedReportSummary(eventId);
|
||||
assert.equal(summary.totalTrusted, 2);
|
||||
assert.equal(summary.types.nudity.total, 3);
|
||||
assert.equal(summary.types.nudity.trusted, 2);
|
||||
assert.equal(service.trustedReportCount(eventId, "nudity"), 2);
|
||||
|
||||
const reporters = service.getTrustedReporters(eventId, "nudity");
|
||||
assert.deepEqual(
|
||||
reporters.map((entry) => entry.pubkey).sort(),
|
||||
[adminWhitelisted, f1Reporter].sort(),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import { VideoCard } from "../../js/ui/components/VideoCard.js";
|
||||
import { clearModerationOverride, setModerationOverride } from "../../js/state/cache.js";
|
||||
import {
|
||||
withMockedNostrTools,
|
||||
createModerationAppHarness,
|
||||
} from "../helpers/moderation-test-helpers.mjs";
|
||||
|
||||
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",
|
||||
"requestAnimationFrame",
|
||||
"cancelAnimationFrame",
|
||||
"navigator",
|
||||
];
|
||||
|
||||
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("VideoCard renders moderation badges and respects viewer override", async (t) => {
|
||||
const { document } = setupDom(t);
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const app = await createModerationAppHarness();
|
||||
const videoId = "a".repeat(64);
|
||||
|
||||
const video = {
|
||||
id: videoId,
|
||||
title: "Blurred Clip",
|
||||
pubkey: "d".repeat(64),
|
||||
moderation: {
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
reportType: "nudity",
|
||||
trustedCount: 3,
|
||||
summary: {
|
||||
eventId: videoId,
|
||||
totalTrusted: 3,
|
||||
types: { nudity: { trusted: 3, total: 3, latest: 1_700_000_000 } },
|
||||
updatedAt: 1_700_000_100,
|
||||
},
|
||||
trustedReporters: [{ pubkey: "e".repeat(64), latest: 1_700_000_050 }],
|
||||
adminWhitelist: false,
|
||||
trustedMuted: false,
|
||||
},
|
||||
};
|
||||
|
||||
t.after(() => {
|
||||
clearModerationOverride(video.id, { persist: false });
|
||||
});
|
||||
|
||||
app.videosMap.set(video.id, video);
|
||||
app.currentVideo = video;
|
||||
app.decorateVideoModeration(video);
|
||||
|
||||
const card = new VideoCard({
|
||||
document,
|
||||
video,
|
||||
formatters: {
|
||||
formatTimeAgo: () => "moments ago",
|
||||
},
|
||||
helpers: {
|
||||
isMagnetSupported: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
document.body.appendChild(card.getRoot());
|
||||
|
||||
assert.equal(card.getRoot().dataset.autoplayPolicy, "blocked");
|
||||
assert.equal(card.thumbnailEl.dataset.thumbnailState, "blurred");
|
||||
assert.equal(card.getRoot().dataset.moderationReportCount, "3");
|
||||
assert.ok(card.moderationBadgeEl);
|
||||
assert.equal(card.moderationBadgeEl.dataset.moderationState, "blocked");
|
||||
assert.ok(card.moderationActionButton);
|
||||
|
||||
card.onModerationOverride = ({ video: overrideVideo }) => {
|
||||
app.videosMap.set(overrideVideo.id, overrideVideo);
|
||||
app.currentVideo = overrideVideo;
|
||||
setModerationOverride(overrideVideo.id, { showAnyway: true, updatedAt: Date.now() }, { persist: false });
|
||||
app.decorateVideoModeration(overrideVideo);
|
||||
card.refreshModerationUi();
|
||||
return true;
|
||||
};
|
||||
|
||||
card.moderationActionButton.click();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const contextAfterOverride = card.getModerationContext();
|
||||
assert.equal(contextAfterOverride.overrideActive, true);
|
||||
assert.equal(contextAfterOverride.activeBlockAutoplay, false);
|
||||
|
||||
assert.equal(card.getRoot().dataset.autoplayPolicy, undefined);
|
||||
assert.equal(card.getRoot().dataset.moderationOverride, "show-anyway");
|
||||
assert.equal(card.thumbnailEl.dataset.thumbnailState, undefined);
|
||||
assert.equal(card.moderationBadgeEl.dataset.moderationState, "override");
|
||||
assert.ok(card.moderationBadgeEl.textContent.includes("Showing despite"));
|
||||
});
|
||||
Reference in new issue
Block a user