Expand video modal moderation tests

This commit is contained in:
thePR0M3TH3AN committed 2025-10-25 18:28:13 -04:00
1 parent 13e3963072
commit 5b65d51262
5 files changed
+655 -1

No files matched your search

+44 -1
View File
@@ -59,7 +59,50 @@
class="video-modal__ambient-canvas"
></canvas>
</div>
<div class="video-modal__video">
<div class="video-modal__video" data-video-surface>
<div
class="video-modal__moderation-overlay"
data-moderation-overlay
hidden
>
<div
class="moderation-badge flex flex-wrap items-center gap-sm"
data-moderation-badge="true"
data-variant="warning"
role="status"
aria-live="polite"
aria-atomic="true"
>
<span class="moderation-badge__icon" aria-hidden="true">
<svg
class="moderation-badge__icon-mark"
viewBox="0 0 20 20"
focusable="false"
aria-hidden="true"
>
<path
fill="currentColor"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm-.75-11.75a.75.75 0 011.5 0v4.5a.75.75 0 01-1.5 0v-4.5zm.75 8.5a1 1 0 100-2 1 1 0 000 2z"
fill-rule="evenodd"
clip-rule="evenodd"
/>
</svg>
</span>
<span
class="moderation-badge__text whitespace-nowrap"
data-moderation-text
>
Content reported by trusted contacts
</span>
<button
type="button"
class="moderation-badge__action flex-shrink-0"
data-moderation-action="override"
>
Show anyway
</button>
</div>
</div>
<video
id="modalVideo"
class="video-modal__media"
+30
View File
@@ -1438,6 +1438,36 @@
transform: scale(1.05);
}
#playerModal .video-modal__video {
position: relative;
}
#playerModal .video-modal__video[data-visual-state="blurred"] .video-modal__media {
@apply transition duration-base ease-in-out;
filter: blur(var(--blur-thumbnail-blur));
transform: scale(1.05);
}
#playerModal .video-modal__moderation-overlay {
position: absolute;
inset: 0;
z-index: var(--z-modal-content);
display: flex;
align-items: center;
justify-content: center;
padding: clamp(var(--space-sm), 3vw, var(--space-xl));
background: color-mix(in srgb, var(--color-overlay-strong) 62%, transparent);
text-align: center;
}
#playerModal .video-modal__moderation-overlay[hidden] {
display: none;
}
#playerModal .video-modal__moderation-overlay .moderation-badge {
pointer-events: auto;
}
.card[data-owner-is-viewer="true"][data-url-health-state="offline"][data-stream-health-state="unhealthy"] {
@apply border border-critical border-opacity-40 bg-card-critical;
}
+75
View File
@@ -848,6 +848,38 @@ class Application {
"video:share",
this.boundVideoModalShareHandler
);
this.boundVideoModalModerationOverrideHandler = (event) => {
const detail = event?.detail || {};
const targetVideo =
detail && typeof detail.video === "object"
? detail.video
: this.currentVideo || null;
if (!targetVideo) {
const trigger = detail?.trigger;
if (trigger) {
trigger.disabled = false;
trigger.removeAttribute("aria-busy");
}
return;
}
const handled = this.handleModerationOverride({
video: targetVideo,
card: detail?.card || null,
});
if (handled === false) {
const trigger = detail?.trigger;
if (trigger) {
trigger.disabled = false;
trigger.removeAttribute("aria-busy");
}
}
};
this.videoModal.addEventListener(
"video:moderation-override",
this.boundVideoModalModerationOverrideHandler,
);
this.boundVideoModalTagActivateHandler = (event) => {
const detail = event?.detail || {};
const nativeEvent = detail?.nativeEvent || null;
@@ -7473,6 +7505,24 @@ class Application {
}
}
const doc =
(this.videoModal && this.videoModal.document) ||
(typeof document !== "undefined" ? document : null);
if (doc && typeof doc.dispatchEvent === "function") {
try {
doc.dispatchEvent(
new CustomEvent("video:moderation-override", {
detail: { video: target },
}),
);
} catch (eventError) {
devLogger.warn(
"[Application] Failed to dispatch moderation override event:",
eventError,
);
}
}
return true;
}
@@ -7528,6 +7578,24 @@ class Application {
}
}
const doc =
(this.videoModal && this.videoModal.document) ||
(typeof document !== "undefined" ? document : null);
if (doc && typeof doc.dispatchEvent === "function") {
try {
doc.dispatchEvent(
new CustomEvent("video:moderation-hide", {
detail: { video: target },
}),
);
} catch (eventError) {
devLogger.warn(
"[Application] Failed to dispatch moderation hide event:",
eventError,
);
}
}
return true;
}
@@ -10625,6 +10693,13 @@ class Application {
);
this.boundVideoModalShareHandler = null;
}
if (this.boundVideoModalModerationOverrideHandler) {
this.videoModal.removeEventListener(
"video:moderation-override",
this.boundVideoModalModerationOverrideHandler,
);
this.boundVideoModalModerationOverrideHandler = null;
}
if (this.boundVideoModalTagActivateHandler) {
this.videoModal.removeEventListener(
"tag:activate",
+374
View File
@@ -14,6 +14,7 @@ import {
unsubscribeFromVideoViewCount,
formatViewCount,
} from "../../viewCounter.js";
import { normalizeVideoModerationContext } from "../moderationUiHelpers.js";
import {
formatAbsoluteTimestamp,
formatTimeAgo,
@@ -99,6 +100,7 @@ export class VideoModal {
this.modalBackdrop = null;
this.scrollRegion = null;
this.modalVideo = null;
this.videoStage = null;
this.modalStatus = null;
this.modalProgress = null;
this.modalProgressStatus = null;
@@ -234,19 +236,43 @@ export class VideoModal {
this.detachAmbientBackground = null;
this.activeVideo = null;
this.activeModerationContext = null;
this.tagPreferenceStateResolver = null;
this.moderationOverlay = null;
this.moderationBadge = null;
this.moderationBadgeText = null;
this.moderationOverrideButton = null;
this.moderationBadgeId = "";
this.handleCopyRequest = this.handleCopyRequest.bind(this);
this.handleShareRequest = this.handleShareRequest.bind(this);
this.handleCreatorNavigation = this.handleCreatorNavigation.bind(this);
this.handleModalMoreButtonClick = this.handleModalMoreButtonClick.bind(this);
this.handleReactionClick = this.handleReactionClick.bind(this);
this.handleVideoTagActivate = this.handleVideoTagActivate.bind(this);
this.handleModerationOverrideClick =
this.handleModerationOverrideClick.bind(this);
this.handleGlobalModerationOverride =
this.handleGlobalModerationOverride.bind(this);
this.handleGlobalModerationHide =
this.handleGlobalModerationHide.bind(this);
this.MODAL_LOADING_POSTER = "assets/gif/please-stand-by.gif";
this.DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
this.setMediaLoader(mediaLoader);
if (this.document) {
this.document.addEventListener(
"video:moderation-override",
this.handleGlobalModerationOverride,
);
this.document.addEventListener(
"video:moderation-hide",
this.handleGlobalModerationHide,
);
}
}
log(message, ...args) {
@@ -401,6 +427,17 @@ export class VideoModal {
this.teardownAmbientGlow({ clear: false });
if (this.moderationOverrideButton) {
this.moderationOverrideButton.removeEventListener(
"click",
this.handleModerationOverrideClick,
);
}
this.moderationOverlay = null;
this.moderationBadge = null;
this.moderationBadgeText = null;
this.moderationOverrideButton = null;
const previousScrollRegion = this.scrollRegion;
if (previousScrollRegion && this.modalNavScrollHandler) {
previousScrollRegion.removeEventListener(
@@ -483,6 +520,20 @@ export class VideoModal {
: [];
this.videoTagsData = null;
this.videoStage = playerModal.querySelector(".video-modal__video") || null;
this.moderationOverlay =
this.videoStage?.querySelector("[data-moderation-overlay]") || null;
this.moderationBadge =
this.moderationOverlay?.querySelector("[data-moderation-badge='true']") || null;
this.moderationBadgeText =
this.moderationOverlay?.querySelector("[data-moderation-text]") || null;
const overrideButton =
this.moderationOverlay?.querySelector("[data-moderation-action='override']") || null;
if (overrideButton) {
overrideButton.addEventListener("click", this.handleModerationOverrideClick);
this.moderationOverrideButton = overrideButton;
}
this.modalVideo = playerModal.querySelector("#modalVideo") || null;
this.modalStatus = playerModal.querySelector("#modalStatus") || null;
this.modalProgress = playerModal.querySelector("#modalProgress") || null;
@@ -569,6 +620,8 @@ export class VideoModal {
this.setupModalZapPopover();
this.setupModalMorePopover();
this.refreshActiveVideoModeration({ video: this.activeVideo });
this.detachCommentEventHandlers();
const commentsRoot =
@@ -2021,6 +2074,30 @@ export class VideoModal {
this.removeCommentRetryButton();
this.invokeCommentTeardown();
if (this.document) {
this.document.removeEventListener(
"video:moderation-override",
this.handleGlobalModerationOverride,
);
this.document.removeEventListener(
"video:moderation-hide",
this.handleGlobalModerationHide,
);
}
if (this.moderationOverrideButton) {
this.moderationOverrideButton.removeEventListener(
"click",
this.handleModerationOverrideClick,
);
}
this.moderationOverlay = null;
this.moderationBadge = null;
this.moderationBadgeText = null;
this.moderationOverrideButton = null;
this.moderationBadgeId = "";
this.activeModerationContext = null;
this.commentProfiles = new Map();
this.commentNodes = new Map();
this.commentChildLists = new Map();
@@ -2638,6 +2715,7 @@ export class VideoModal {
open(video, options = {}) {
this.activeVideo = video || null;
this.moderationBadgeId = "";
if (!this.playerModal) {
return;
}
@@ -2656,10 +2734,12 @@ export class VideoModal {
}
this.setGlobalModalState("player", true);
this.applyLoadingPoster();
this.refreshActiveVideoModeration({ video: this.activeVideo });
}
close() {
this.activeVideo = null;
this.applyModerationOverlay(null);
if (this.playerModal) {
this.playerModal.classList.add("hidden");
this.playerModal.setAttribute("hidden", "");
@@ -3794,6 +3874,300 @@ export class VideoModal {
if (creator !== undefined) {
this.updateCreator(creator);
}
this.refreshActiveVideoModeration({ video: this.activeVideo });
}
getModerationBadgeId() {
if (this.moderationBadgeId) {
return this.moderationBadgeId;
}
const baseId =
typeof this.activeVideo?.id === "string" && this.activeVideo.id
? this.activeVideo.id
: "";
if (!baseId) {
this.moderationBadgeId = "";
return this.moderationBadgeId;
}
const sanitized = baseId.replace(/[^a-zA-Z0-9_-]/g, "");
if (!sanitized) {
this.moderationBadgeId = "";
return this.moderationBadgeId;
}
this.moderationBadgeId = `video-modal-${sanitized}-moderation`;
return this.moderationBadgeId;
}
buildModerationReasonText(context) {
if (!context) {
return "";
}
const reasons = [];
if (context.trustedMuted) {
const muteCount = Math.max(1, Number(context.trustedMuteCount) || 0);
const muteLabel = muteCount === 1 ? "trusted contact" : "trusted contacts";
reasons.push(
`muted by ${muteCount === 1 ? "a" : muteCount} ${muteLabel}`,
);
}
const typeLabel = context.friendlyType || "this video";
const reportCount = Math.max(0, Number(context.trustedCount) || 0);
if (reportCount > 0) {
const friendLabel = reportCount === 1 ? "friend" : "friends";
reasons.push(`${reportCount} ${friendLabel} reported ${typeLabel}`);
} else if (!context.trustedMuted) {
reasons.push(
context.friendlyType ? `reports of ${typeLabel}` : "reports",
);
}
if (!reasons.length) {
return "";
}
const combined = reasons.join(" · ");
return combined.charAt(0).toUpperCase() + combined.slice(1);
}
buildModerationBadgeText(context) {
if (!context) {
return "";
}
const blurSource = (context.originalBlurReason || context.blurReason || "")
.toLowerCase()
.trim();
const suppressBlurLabel =
blurSource === "trusted-mute" || blurSource === "trusted-mute-hide";
const parts = [];
if (!suppressBlurLabel) {
parts.push("Blurred");
}
const reason = this.buildModerationReasonText(context);
if (reason) {
parts.push(reason);
}
if (!parts.length) {
return "Blurred due to reports";
}
return parts.join(" · ");
}
refreshActiveVideoModeration({ video = this.activeVideo } = {}) {
if (!video) {
this.activeModerationContext = null;
this.applyModerationOverlay(null);
return;
}
const context = normalizeVideoModerationContext(video.moderation);
this.activeModerationContext = context;
this.applyModerationOverlay(context);
}
applyModerationOverlay(context) {
const stage = this.videoStage;
const overlay = this.moderationOverlay;
const badge = this.moderationBadge;
const textEl = this.moderationBadgeText;
const button = this.moderationOverrideButton;
const shouldBlur = Boolean(context?.activeBlur && !context?.overrideActive);
if (!shouldBlur) {
if (stage && stage.dataset.visualState === "blurred") {
delete stage.dataset.visualState;
}
if (overlay) {
overlay.setAttribute("hidden", "");
}
if (badge) {
badge.dataset.variant = context?.overrideActive ? "neutral" : "warning";
if (badge.dataset.moderationState) {
delete badge.dataset.moderationState;
}
badge.removeAttribute("title");
if (badge.id && !this.activeVideo) {
badge.removeAttribute("id");
}
if (badge.hasAttribute("aria-label")) {
badge.removeAttribute("aria-label");
}
}
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
}
if (stage) {
stage.dataset.visualState = "blurred";
}
if (overlay) {
overlay.removeAttribute("hidden");
}
if (this.modalVideo && typeof this.modalVideo.pause === "function") {
try {
this.modalVideo.pause();
} catch (error) {
// ignore pause failures (e.g., in unsupported environments)
}
}
if (badge) {
const state = context?.trustedMuted ? "trusted-mute" : "blocked";
badge.dataset.variant = "warning";
badge.dataset.moderationState = state;
const badgeId = this.getModerationBadgeId();
if (badgeId) {
badge.id = badgeId;
}
const textContent = this.buildModerationBadgeText(context);
if (textEl) {
textEl.textContent = textContent;
}
const muteNames = Array.isArray(context.trustedMuteDisplayNames)
? context.trustedMuteDisplayNames
.map((name) => (typeof name === "string" ? name.trim() : ""))
.filter(Boolean)
: [];
const reporterNames = Array.isArray(context.reporterDisplayNames)
? context.reporterDisplayNames
.map((name) => (typeof name === "string" ? name.trim() : ""))
.filter(Boolean)
: [];
const allNames = [...muteNames, ...reporterNames];
const uniqueNames = [];
const seenNameKeys = new Set();
for (const name of allNames) {
if (!name) {
continue;
}
const key = name.toLowerCase();
if (seenNameKeys.has(key)) {
continue;
}
seenNameKeys.add(key);
uniqueNames.push(name);
}
if (uniqueNames.length) {
const joined = uniqueNames.join(", ");
const hasMuted = muteNames.length > 0;
const hasReporters = reporterNames.length > 0;
const prefix = hasMuted && hasReporters
? "Muted/Reported by"
: hasMuted
? "Muted by"
: "Reported by";
badge.title = `${prefix} ${joined}`;
const baseLabel = textEl?.textContent?.trim() || textContent || "";
if (baseLabel) {
badge.setAttribute("aria-label", `${baseLabel}. ${prefix} ${joined}.`);
} else {
badge.setAttribute("aria-label", `${prefix} ${joined}.`);
}
} else {
badge.removeAttribute("title");
const baseLabel = textEl?.textContent?.trim();
if (baseLabel) {
badge.setAttribute("aria-label", `${baseLabel}.`);
} else {
badge.removeAttribute("aria-label");
}
}
}
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
const badgeId = this.getModerationBadgeId();
if (badgeId) {
button.setAttribute("aria-describedby", badgeId);
} else {
button.removeAttribute("aria-describedby");
}
}
}
handleModerationOverrideClick(event) {
event?.preventDefault?.();
event?.stopPropagation?.();
const button = event?.currentTarget || this.moderationOverrideButton;
if (button) {
button.disabled = true;
button.setAttribute("aria-busy", "true");
}
if (!this.activeVideo) {
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
}
const detail = {
video: this.activeVideo,
event,
trigger: button || null,
context: this.activeModerationContext || null,
};
const handled = this.dispatch("video:moderation-override", detail);
if (!handled && button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
}
handleGlobalModerationOverride(event) {
const detail = event?.detail || {};
const video = detail && typeof detail === "object" ? detail.video : null;
if (!video || !video.id || !this.activeVideo || this.activeVideo.id !== video.id) {
return;
}
if (this.activeVideo !== video) {
this.activeVideo = video;
this.moderationBadgeId = "";
}
this.refreshActiveVideoModeration({ video: this.activeVideo });
}
handleGlobalModerationHide(event) {
const detail = event?.detail || {};
const video = detail && typeof detail === "object" ? detail.video : null;
if (!video || !video.id || !this.activeVideo || this.activeVideo.id !== video.id) {
return;
}
if (this.activeVideo !== video) {
this.activeVideo = video;
this.moderationBadgeId = "";
}
this.refreshActiveVideoModeration({ video: this.activeVideo });
}
renderVideoDescription(description) {
+132
View File
@@ -0,0 +1,132 @@
import test from "node:test";
import assert from "node:assert/strict";
import { setupModal } from "./video-modal-accessibility.test.mjs";
test("VideoModal blurs and restores playback when moderation overlay toggles", async (t) => {
const { modal, cleanup } = await setupModal();
const document = modal.document;
const window = document.defaultView;
try {
await t.test("trusted reports blur the active video", async () => {
const videoElement = modal.getVideoElement();
assert.ok(videoElement, "modal video element should exist");
let pauseCalls = 0;
const originalPause = videoElement.pause;
videoElement.pause = function patchedPause() {
pauseCalls += 1;
if (typeof originalPause === "function") {
return originalPause.call(this);
}
return undefined;
};
const video = {
id: "event-trusted-blur",
title: "Blurred video",
moderation: {
blurThumbnail: true,
trustedCount: 3,
reporterDisplayNames: ["Alice", "Bob"],
summary: {
types: {
nudity: { trusted: 3 },
},
},
},
};
modal.open(video);
const stage = document.querySelector(".video-modal__video");
assert.ok(stage, "video stage should exist");
assert.equal(stage.dataset.visualState, "blurred");
const overlay = document.querySelector("[data-moderation-overlay]");
assert.ok(overlay, "moderation overlay should be rendered");
assert.equal(overlay.hasAttribute("hidden"), false);
assert.ok(pauseCalls > 0, "video pause should be invoked when blur is applied");
const textEl = overlay.querySelector("[data-moderation-text]");
assert.ok(textEl, "moderation text element should exist");
assert.match(
textEl.textContent,
/blurred/i,
"overlay text should describe the blur state",
);
let overrideEvent = null;
modal.addEventListener("video:moderation-override", (event) => {
overrideEvent = event;
});
const showButton = overlay.querySelector(
"[data-moderation-action='override']",
);
assert.ok(showButton, "Show anyway button should be present");
showButton.click();
assert.ok(overrideEvent, "clicking the CTA should emit override event");
assert.equal(overrideEvent.detail.video, video);
video.moderation.blurThumbnail = false;
video.moderation.viewerOverride = { showAnyway: true };
const overrideSignal = new window.CustomEvent("video:moderation-override", {
detail: { video },
});
document.dispatchEvent(overrideSignal);
assert.equal(stage.dataset.visualState, undefined);
assert.equal(overlay.hasAttribute("hidden"), true);
delete video.moderation.viewerOverride;
video.moderation.blurThumbnail = true;
const hideSignal = new window.CustomEvent("video:moderation-hide", {
detail: { video },
});
document.dispatchEvent(hideSignal);
assert.equal(stage.dataset.visualState, "blurred");
assert.equal(overlay.hasAttribute("hidden"), false);
videoElement.pause = originalPause;
modal.close();
});
await t.test("trusted mute blur state matches moderation context", async () => {
const video = {
id: "event-trusted-mute",
title: "Trusted mute video",
moderation: {
blurThumbnail: true,
trustedMuted: true,
trustedMuteCount: 2,
trustedMuteDisplayNames: ["Carol", "Dave"],
},
};
modal.open(video);
const stage = document.querySelector(".video-modal__video");
assert.ok(stage, "video stage should exist");
assert.equal(stage.dataset.visualState, "blurred");
const overlay = document.querySelector("[data-moderation-overlay]");
assert.ok(overlay, "moderation overlay should be rendered");
assert.equal(overlay.hasAttribute("hidden"), false);
const badge = overlay.querySelector("[data-moderation-badge='true']");
assert.ok(badge, "moderation badge should exist");
assert.equal(badge.dataset.moderationState, "trusted-mute");
modal.close();
});
} finally {
await cleanup();
}
});