Add automated moderation coverage

This commit is contained in:
thePR0M3TH3AN committed 2025-10-17 18:07:08 -04:00
1 parent 84c2b647e7
commit 157fa86a8f
4 files changed
+582 -2

No files matched your search

+317
View File
@@ -0,0 +1,317 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Moderation Fixtures • bitvid</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/tailwind.generated.css" />
<style>
body {
background-color: #0f172a;
color: #f8fafc;
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main {
max-width: 960px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1.5rem;
}
section {
margin-bottom: 2.5rem;
}
section h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
section p {
color: #cbd5f5;
margin-bottom: 1rem;
}
</style>
</head>
<body data-ds="new">
<main id="moderationFixtureRoot">
<h1>Moderation Test Fixtures</h1>
<p>
These scenarios mirror the QA vectors in <code>docs/moderation/testing.md</code> so
automated checks can assert blur thresholds, autoplay blocking, and local
override persistence.
</p>
</main>
<script type="module">
import { VideoCard } from "/js/ui/components/VideoCard.js";
const STORAGE_KEY = "bitvid:moderation:fixture-overrides";
const fixtures = [
{
key: "blur-threshold",
heading: "Blur threshold (≥3 trusted nudity reports)",
description:
"Card should be blurred and autoplay disabled until the viewer overrides it.",
video: {
id: "1".repeat(64),
title: "Fixture: Blurred Preview",
pubkey: "a".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "nudity",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 },
],
},
{
key: "autoplay-threshold",
heading: "Autoplay threshold (≥2 trusted nudity reports)",
description:
"Card should block autoplay but stay unblurred so users can inspect the thumbnail.",
video: {
id: "2".repeat(64),
title: "Fixture: Autoplay Blocked",
pubkey: "e".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: false,
trustedCount: 2,
reportType: "nudity",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
],
},
{
key: "show-anyway",
heading: "\"Show anyway\" override (persists locally)",
description:
"Card starts blurred and blocked; activating \"Show anyway\" should persist across reloads.",
enableOverride: true,
video: {
id: "3".repeat(64),
title: "Fixture: Override Persistence",
pubkey: "f".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "nudity",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 },
],
},
];
window.__MODERATION_FIXTURE_STORAGE_KEY__ = STORAGE_KEY;
function loadOverrides() {
try {
const raw = window.localStorage?.getItem(STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
function persistOverrides(next) {
try {
window.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
} catch {
/* ignore persistence issues in test fixtures */
}
}
function buildSummary(fixture) {
if (!fixture?.moderation?.reportType) {
return null;
}
const latest = Array.isArray(fixture.reporters)
? fixture.reporters.reduce(
(max, reporter) =>
Number.isFinite(reporter?.latest)
? Math.max(max, Math.floor(reporter.latest))
: max,
0,
)
: 0;
return {
eventId: fixture.video.id,
totalTrusted: fixture.moderation.trustedCount,
types: {
[fixture.moderation.reportType]: {
trusted: fixture.moderation.trustedCount,
total: fixture.moderation.trustedCount,
latest,
},
},
updatedAt: Date.now(),
};
}
function buildModerationState(fixture, overrides, videoId) {
const overrideEntry = overrides?.[videoId];
const base = {
reportType: fixture.moderation.reportType,
trustedCount: fixture.moderation.trustedCount,
reporterDisplayNames: Array.isArray(fixture.reporters)
? fixture.reporters.map((reporter) => reporter.name)
: [],
reporterPubkeys: Array.isArray(fixture.reporters)
? fixture.reporters.map((reporter) => reporter.pubkey)
: [],
trustedReporters: Array.isArray(fixture.reporters)
? fixture.reporters.map((reporter) => ({
pubkey: reporter.pubkey,
latest: reporter.latest,
}))
: [],
original: {
blockAutoplay: fixture.moderation.blockAutoplay === true,
blurThumbnail: fixture.moderation.blurThumbnail === true,
},
};
const summary = buildSummary(fixture);
if (summary) {
base.summary = summary;
}
if (overrideEntry?.showAnyway) {
base.viewerOverride = {
showAnyway: true,
updatedAt: Number.isFinite(overrideEntry.updatedAt)
? Math.floor(overrideEntry.updatedAt)
: Date.now(),
};
base.blockAutoplay = false;
base.blurThumbnail = false;
} else {
base.blockAutoplay = fixture.moderation.blockAutoplay === true;
base.blurThumbnail = fixture.moderation.blurThumbnail === true;
}
return base;
}
function setOverride(overrides, videoId, entry) {
const next = { ...overrides };
next[videoId] = {
...entry,
updatedAt: Number.isFinite(entry?.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now(),
};
persistOverrides(next);
return next;
}
function createCard(fixture, overrides) {
const video = {
...fixture.video,
};
video.moderation = buildModerationState(fixture, overrides, video.id);
const card = new VideoCard({
document,
video,
shareUrl: "#",
formatters: {
formatTimeAgo: () => "just now",
formatNumber: (value) => {
if (typeof value === "number") {
return value.toLocaleString("en-US");
}
return String(value ?? "");
},
},
helpers: {
isMagnetSupported: () => true,
escapeHtml: (value) => String(value ?? ""),
toLocaleString: (value, locales, options) => {
if (value && typeof value.toLocaleString === "function") {
return value.toLocaleString(locales || "en-US", options);
}
return String(value ?? "");
},
},
assets: {
fallbackThumbnailSrc: "/assets/jpg/video-thumbnail-fallback.jpg",
},
designSystem: {
getMode: () => "new",
isNew: () => true,
},
});
if (fixture.enableOverride) {
card.onModerationOverride = () => {
overrides = setOverride(overrides, video.id, { showAnyway: true });
card.video.moderation = buildModerationState(
fixture,
overrides,
video.id,
);
card.refreshModerationUi();
return true;
};
}
const root = card.getRoot();
root.dataset.testId = fixture.key;
return { card, root };
}
function renderFixtures() {
const root = document.getElementById("moderationFixtureRoot");
if (!root) {
return;
}
let overrides = loadOverrides();
fixtures.forEach((fixture) => {
const section = document.createElement("section");
const heading = document.createElement("h2");
heading.textContent = fixture.heading;
section.appendChild(heading);
if (fixture.description) {
const description = document.createElement("p");
description.textContent = fixture.description;
section.appendChild(description);
}
const { root: cardRoot } = createCard(fixture, overrides);
section.appendChild(cardRoot);
root.appendChild(section);
});
}
renderFixtures();
</script>
</body>
</html>
+11 -2
View File
@@ -2,15 +2,23 @@
## Quick checklist
- [ ] F1 set cached and used for “trusted reports”.
- Automated coverage: `tests/moderation/trusted-report-count.test.mjs` (run via `npm run test:unit`).
- [ ] Blur at ≥3 F1 reports of `nudity`; autoplay blocked at ≥2.
- Automated coverage: `tests/visual/moderation.spec.ts` blur and autoplay scenarios (run via `npm run test:visual`).
- [ ] “Show anyway” works and records only local pref.
- Automated coverage: `tests/visual/moderation.spec.ts` show-anyway persistence scenario (run via `npm run test:visual`).
- [ ] Mute list (10000) downranks/hides consistently.
- Manual: subscribe to fixture B and verify muted author disappears from feeds after refresh.
- [ ] Admin lists (30000) take effect only when subscribed.
- Manual: load fixture C, toggle subscription, and confirm the hard-hide only applies while subscribed.
- [ ] COUNT fallback shows placeholders and doesn’t crash UI.
- Manual: point the client at a relay without COUNT support and confirm placeholders render instead of errors.
- [ ] Comment threads render with NIP-10; moderation badges show in context.
- Manual: open a reported thread and confirm badges + reply ordering remain intact.
- [ ] Reports from muted reporters are ignored.
- Automated coverage: `tests/moderation/trusted-report-count.test.mjs` (muted reporter case).
## Fixtures (create on test relays)
## Fixtures (create on test relays / served in `/docs/moderation/fixtures/`)
### A) NIP-56 report events
Create three distinct F1 reporter keys; each sends:
@@ -66,6 +74,7 @@ Expected: when viewer subscribes to this list, author is hard-hidden.
## Automated checks (suggested)
* Unit test `trustedReportCount`.
* `npm run test:unit` — covers `tests/moderation/trusted-report-count.test.mjs` for deduping and mute handling.
* `npm run test:visual` — runs Playwright fixtures in `tests/visual/moderation.spec.ts` against `/docs/moderation/fixtures/index.html`.
* Integration test: emit events over a local relay; assert UI state.
* Regression test: ensure Home feed ignores reputation gating.