mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-08 13:52:58 +00:00
Add automated moderation coverage
This commit is contained in:
1 parent
84c2b647e7
commit
157fa86a8f
4 files changed
+582
-2
No files matched your search
@@ -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>
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user