mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #744 from PR0M3TH3AN/codex/expand-tests-for-video-card-metadata
Add trusted hide coverage and refresh moderation docs
This commit is contained in:
5 files changed
+288
-43
No files matched your search
@@ -22,7 +22,7 @@ bitvid is follow-centric. Your Home feed comes from people you follow (F1). Disc
|
||||
- [`ModerationService`](../../js/services/moderationService.js) orchestrates ingest and scoring. Review the service for the trusted-report math and helper entry points.
|
||||
- [`createModerationStage`](../../js/feedEngine/stages.js) wires the service into the feed engine where moderation summaries decorate timeline items.
|
||||
- [`bitvidApp.decorateVideoModeration`](../../js/app.js) connects stage output to the UI layer alongside feature-flag plumbing.
|
||||
- [`VideoCard.refreshModerationUi`](../../js/ui/components/VideoCard.js) applies badges, blur states, and "show anyway" toggles.
|
||||
- [`VideoCard.refreshModerationUi`](../../js/ui/components/VideoCard.js) applies badges, blur states, hide metadata (`data-moderation-hidden`), and the "show anyway" toggles.
|
||||
|
||||
## Where to extend
|
||||
Thread new moderation behaviors through the same service → stage → app → UI flow above. Extending the existing layers keeps overrides, feature flags, and QA hooks consistent—avoid spinning up parallel moderation modules unless the architecture document explicitly calls for it.
|
||||
@@ -30,14 +30,21 @@ Thread new moderation behaviors through the same service → stage → app → U
|
||||
## Defaults (policy)
|
||||
- Blur video thumbnails if **≥ 3** F1 friends report `nudity`.
|
||||
- Disable autoplay preview if **≥ 2** F1 friends report `nudity`.
|
||||
- Hide videos when **≥ 1** trusted contact mutes the author (`TRUSTED_MUTE_HIDE_THRESHOLD`).
|
||||
- Hide videos when **≥ 3** trusted contacts file spam reports (`TRUSTED_SPAM_HIDE_THRESHOLD`).
|
||||
- Hide videos when **≥ 1** trusted contact mutes the author (`TRUSTED_MUTE_HIDE_THRESHOLD`). Cards render with `data-moderation-hidden="true"`, the badge reads `Hidden · 1 trusted mute`, and a "Show anyway" button becomes available.
|
||||
- Hide videos when **≥ 3** trusted contacts file spam reports (`TRUSTED_SPAM_HIDE_THRESHOLD`). The badge copy escalates to `Hidden · 3 trusted spam reports` and the card stays hidden until the viewer overrides it.
|
||||
- Downrank author when any F1 has them in mute list (10000).
|
||||
- Opt-in admin lists (30000 with `d=bitvid:admin:*`) can hard-hide content.
|
||||
- Trust seeds from `DEFAULT_TRUST_SEED_NPUBS` count as baseline F1 contacts (toggle via `FEATURE_TRUST_SEEDS`).
|
||||
|
||||
> You can override all defaults in **Settings → Safety & Moderation**.
|
||||
|
||||
### Hide thresholds & feature flags
|
||||
|
||||
- `TRUSTED_MUTE_HIDE_THRESHOLD` and `TRUSTED_SPAM_HIDE_THRESHOLD` determine when moderation metadata marks an item as hidden. Both feed the `original.hide*` fields that `VideoCard` inspects before deciding whether to render the hidden summary container.
|
||||
- `FEATURE_TRUSTED_HIDE_CONTROLS` shows the additional threshold inputs in the profile modal so operators can raise/lower the hide triggers client-side.
|
||||
- When any hide threshold fires we persist the active counts (`hideCounts.trustedMuteCount`, `hideCounts.trustedReportCount`) so analytics/debugging tools can show why an item disappeared.
|
||||
- Cards that arrive with `original.hidden === true` automatically expose the "Show anyway" chip so viewers can inspect hidden content. Activating the override clears blur/autoplay restrictions alongside the hidden state.
|
||||
|
||||
## Community blacklist federation
|
||||
|
||||
Operators can delegate hard-hide decisions to trusted curators without giving them full admin powers. The flow is:
|
||||
|
||||
@@ -126,16 +126,43 @@
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "trusted-mute",
|
||||
heading: "Trusted mute (F1 mute list)",
|
||||
key: "trusted-report-hide",
|
||||
heading: "Trusted report hide (≥3 spam reports)",
|
||||
description:
|
||||
"Card should explain that a trusted contact muted this author without enabling overrides.",
|
||||
"Card should render hidden with spam copy until the viewer clicks \"Show anyway\".",
|
||||
enableOverride: true,
|
||||
thresholds: { reportHide: 3 },
|
||||
video: {
|
||||
id: "4".repeat(64),
|
||||
title: "Fixture: Trusted Mute",
|
||||
title: "Fixture: Trusted Report Hide",
|
||||
pubkey: "aa".repeat(32),
|
||||
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
||||
},
|
||||
moderation: {
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 3,
|
||||
reportType: "spam",
|
||||
},
|
||||
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: "trusted-mute-hide",
|
||||
heading: "Trusted mute hide (F1 mute list threshold)",
|
||||
description:
|
||||
"Card should default to hidden when a trusted contact mutes the author and allow an override.",
|
||||
enableOverride: true,
|
||||
thresholds: { muteHide: 1 },
|
||||
video: {
|
||||
id: "5".repeat(64),
|
||||
title: "Fixture: Trusted Mute Hide",
|
||||
pubkey: "bb".repeat(32),
|
||||
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
||||
},
|
||||
moderation: {
|
||||
blockAutoplay: false,
|
||||
blurThumbnail: false,
|
||||
@@ -284,14 +311,20 @@
|
||||
const sanitizedReporters = normalizeFixtureReporters(activeReporters);
|
||||
const thresholds =
|
||||
fixture.thresholds && typeof fixture.thresholds === "object"
|
||||
? { blur: 3, autoplay: 2, ...fixture.thresholds }
|
||||
: { blur: 3, autoplay: 2 };
|
||||
? { blur: 3, autoplay: 2, reportHide: 3, muteHide: 1, ...fixture.thresholds }
|
||||
: { blur: 3, autoplay: 2, reportHide: 3, muteHide: 1 };
|
||||
const autoplayThreshold = Number.isFinite(thresholds.autoplay)
|
||||
? Math.max(0, Math.floor(thresholds.autoplay))
|
||||
: 2;
|
||||
const blurThreshold = Number.isFinite(thresholds.blur)
|
||||
? Math.max(0, Math.floor(thresholds.blur))
|
||||
: 3;
|
||||
const reportHideThreshold = Number.isFinite(thresholds.reportHide)
|
||||
? Math.max(0, Math.floor(thresholds.reportHide))
|
||||
: 3;
|
||||
const muteHideThreshold = Number.isFinite(thresholds.muteHide)
|
||||
? Math.max(0, Math.floor(thresholds.muteHide))
|
||||
: 1;
|
||||
const initialBlock = fixture.moderation.blockAutoplay === true;
|
||||
const initialBlur = fixture.moderation.blurThumbnail === true;
|
||||
const trustedCount = sanitizedReporters.length;
|
||||
@@ -307,16 +340,57 @@
|
||||
latest: reporter.latest,
|
||||
}));
|
||||
|
||||
const muterEntries = Array.isArray(fixture.muters) ? fixture.muters : [];
|
||||
const muterPubkeys = muterEntries
|
||||
.map((muter) => (typeof muter?.pubkey === "string" ? muter.pubkey : ""))
|
||||
.filter(Boolean);
|
||||
const muterDisplayNames = muterEntries
|
||||
.map((muter) => (typeof muter?.name === "string" ? muter.name : ""))
|
||||
.filter(Boolean);
|
||||
const trustedMuted = fixture.moderation.trustedMuted === true;
|
||||
const muteCount = Number.isFinite(fixture.moderation.trustedMuteCount)
|
||||
? Math.max(0, Math.floor(fixture.moderation.trustedMuteCount))
|
||||
: muterPubkeys.length;
|
||||
|
||||
const hideFromMute = trustedMuted && muteCount >= muteHideThreshold;
|
||||
const hideFromReports = trustedCount >= reportHideThreshold;
|
||||
let hidden = fixture.moderation.hidden === true;
|
||||
let hideReason =
|
||||
typeof fixture.moderation.hideReason === "string"
|
||||
? fixture.moderation.hideReason
|
||||
: "";
|
||||
const hideBypass =
|
||||
typeof fixture.moderation.hideBypass === "string"
|
||||
? fixture.moderation.hideBypass
|
||||
: "";
|
||||
|
||||
if (hideFromMute) {
|
||||
hidden = true;
|
||||
hideReason = "trusted-mute-hide";
|
||||
} else if (hideFromReports) {
|
||||
hidden = true;
|
||||
hideReason = "trusted-report-hide";
|
||||
} else if (!hidden) {
|
||||
hideReason = "";
|
||||
}
|
||||
|
||||
const hideCounts = hidden
|
||||
? {
|
||||
trustedMuteCount: muteCount,
|
||||
trustedReportCount: trustedCount,
|
||||
}
|
||||
: null;
|
||||
|
||||
const base = {
|
||||
reportType: fixture.moderation.reportType,
|
||||
trustedCount,
|
||||
reporterDisplayNames,
|
||||
reporterPubkeys,
|
||||
trustedReporters: trustedReporterEntries,
|
||||
original: {
|
||||
blockAutoplay: initialBlock,
|
||||
blurThumbnail: initialBlur,
|
||||
},
|
||||
trustedMuted,
|
||||
trustedMuters: muterPubkeys,
|
||||
trustedMuteCount: muteCount,
|
||||
trustedMuterDisplayNames: muterDisplayNames,
|
||||
};
|
||||
|
||||
const summary = buildSummary(fixture, videoId, sanitizedReporters);
|
||||
@@ -324,24 +398,33 @@
|
||||
base.summary = summary;
|
||||
}
|
||||
|
||||
if (fixture.moderation.trustedMuted) {
|
||||
base.trustedMuted = true;
|
||||
const muterPubkeys = Array.isArray(fixture.muters)
|
||||
? fixture.muters
|
||||
.map((muter) => (typeof muter?.pubkey === "string" ? muter.pubkey : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
base.trustedMuters = muterPubkeys;
|
||||
const muteCount = Number.isFinite(fixture.moderation.trustedMuteCount)
|
||||
? Math.max(0, Math.floor(fixture.moderation.trustedMuteCount))
|
||||
: muterPubkeys.length;
|
||||
base.trustedMuteCount = muteCount;
|
||||
base.trustedMuterDisplayNames = Array.isArray(fixture.muters)
|
||||
? fixture.muters
|
||||
.map((muter) => (typeof muter?.name === "string" ? muter.name : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
const computedBlock = initialBlock && trustedCount >= autoplayThreshold;
|
||||
const computedBlur = initialBlur && trustedCount >= blurThreshold;
|
||||
|
||||
const originalState = {
|
||||
blockAutoplay: computedBlock,
|
||||
blurThumbnail: computedBlur,
|
||||
hidden,
|
||||
hideReason: hidden ? hideReason : "",
|
||||
hideCounts: hideCounts
|
||||
? {
|
||||
trustedMuteCount: Math.max(0, Number(hideCounts.trustedMuteCount) || 0),
|
||||
trustedReportCount: Math.max(0, Number(hideCounts.trustedReportCount) || 0),
|
||||
}
|
||||
: null,
|
||||
hideBypass,
|
||||
hideTriggered: hidden,
|
||||
};
|
||||
|
||||
base.original = {
|
||||
blockAutoplay: originalState.blockAutoplay,
|
||||
blurThumbnail: originalState.blurThumbnail,
|
||||
hidden: originalState.hidden,
|
||||
hideReason: originalState.hideReason,
|
||||
hideCounts: originalState.hideCounts,
|
||||
hideBypass: originalState.hideBypass,
|
||||
hideTriggered: originalState.hideTriggered,
|
||||
};
|
||||
|
||||
if (overrideEntry?.showAnyway) {
|
||||
base.viewerOverride = {
|
||||
@@ -352,9 +435,44 @@
|
||||
};
|
||||
base.blockAutoplay = false;
|
||||
base.blurThumbnail = false;
|
||||
base.hidden = false;
|
||||
if (base.hideReason) {
|
||||
delete base.hideReason;
|
||||
}
|
||||
if (base.hideCounts) {
|
||||
delete base.hideCounts;
|
||||
}
|
||||
if (base.hideBypass) {
|
||||
delete base.hideBypass;
|
||||
}
|
||||
} else {
|
||||
base.blockAutoplay = initialBlock && trustedCount >= autoplayThreshold;
|
||||
base.blurThumbnail = initialBlur && trustedCount >= blurThreshold;
|
||||
base.blockAutoplay = originalState.blockAutoplay;
|
||||
base.blurThumbnail = originalState.blurThumbnail;
|
||||
if (originalState.hidden) {
|
||||
base.hidden = true;
|
||||
if (originalState.hideReason) {
|
||||
base.hideReason = originalState.hideReason;
|
||||
}
|
||||
if (originalState.hideCounts) {
|
||||
base.hideCounts = { ...originalState.hideCounts };
|
||||
}
|
||||
if (originalState.hideBypass) {
|
||||
base.hideBypass = originalState.hideBypass;
|
||||
}
|
||||
} else {
|
||||
if (base.hidden) {
|
||||
delete base.hidden;
|
||||
}
|
||||
if (base.hideReason) {
|
||||
delete base.hideReason;
|
||||
}
|
||||
if (base.hideCounts) {
|
||||
delete base.hideCounts;
|
||||
}
|
||||
if (base.hideBypass) {
|
||||
delete base.hideBypass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
- [ ] 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`).
|
||||
- [ ] Hide videos once trusted mute/spam thresholds are met.
|
||||
- Automated coverage: `tests/visual/moderation.spec.ts` trusted hide scenarios (run via `npm run test:visual`).
|
||||
- Manual: adjust thresholds in the profile modal, simulate trusted mutes/reports, and verify cards disappear until "show anyway" is used.
|
||||
- [ ] “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`).
|
||||
- [ ] “Show anyway” works for blurred and hidden cards and records only local pref.
|
||||
- Automated coverage: `tests/visual/moderation.spec.ts` show-anyway persistence and trusted hide scenarios (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.
|
||||
@@ -40,7 +41,7 @@ Expected:
|
||||
|
||||
* After 1 report → no blur; autoplay allowed.
|
||||
* After 2 reports → autoplay blocked.
|
||||
* After 3 reports → thumbnail blurred; reason chip shown.
|
||||
* After 3 reports → thumbnail blurred; reason chip shown. At the spam threshold (`TRUSTED_SPAM_HIDE_THRESHOLD`) the card hides entirely and the badge copy switches to "Hidden · 3 trusted spam reports" until the viewer overrides it.
|
||||
|
||||
**Exercises:** [`ModerationService.ingestReportEvent()`](../../js/services/moderationService.js) and [`ModerationService.getTrustedReportSummary()`](../../js/services/moderationService.js) wire through [`createModerationStage()`](../../js/feedEngine/stages.js) so [`bitvidApp.decorateVideoModeration()`](../../js/app.js) can hand the summary to [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js).
|
||||
|
||||
@@ -55,7 +56,7 @@ Expected:
|
||||
}
|
||||
```
|
||||
|
||||
Expected: author’s items are downranked/hidden for this viewer.
|
||||
Expected: author’s items are downranked/hidden for this viewer. When the mute threshold (`TRUSTED_MUTE_HIDE_THRESHOLD`) is met, cards render with "Hidden · 1 trusted mute" and require a "Show anyway" override to display content.
|
||||
|
||||
**Exercises:** [`ModerationService.ingestTrustedMuteEvent()`](../../js/services/moderationService.js) updates [`ModerationService.isAuthorMutedByTrusted()`](../../js/services/moderationService.js) / [`getTrustedMutersForAuthor()`](../../js/services/moderationService.js), which [`createModerationStage()`](../../js/feedEngine/stages.js) propagates to the video item before [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js) renders the mute state.
|
||||
|
||||
@@ -85,6 +86,6 @@ Expected: when viewer subscribes to this list, author is hard-hidden.
|
||||
## Automated checks (suggested)
|
||||
|
||||
* `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`.
|
||||
* `npm run test:visual` — runs Playwright fixtures in `tests/visual/moderation.spec.ts` against `/docs/moderation/fixtures/index.html`, covering blur thresholds, autoplay blocks, trusted hide badges, and the override UX.
|
||||
* Integration test: emit events over a local relay; assert UI state.
|
||||
* Regression test: ensure Home feed ignores reputation gating.
|
||||
@@ -175,3 +175,86 @@ test("VideoCard renders moderation badges and respects viewer override", async (
|
||||
assert.ok(!card.hiddenSummaryEl || card.hiddenSummaryEl.parentElement !== card.getRoot());
|
||||
assert.ok(card.moderationBadgeEl.textContent.includes("Showing despite"));
|
||||
});
|
||||
|
||||
test(
|
||||
"VideoCard hides content when hide metadata is present and override unmasks it",
|
||||
async (t) => {
|
||||
const { document } = setupDom(t);
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const app = await createModerationAppHarness();
|
||||
const videoId = "b".repeat(64);
|
||||
|
||||
const video = {
|
||||
id: videoId,
|
||||
title: "Hidden Clip",
|
||||
pubkey: "c".repeat(64),
|
||||
moderation: {
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
reportType: "spam",
|
||||
trustedCount: 3,
|
||||
trustedReporters: [
|
||||
{ pubkey: "d".repeat(64), latest: 1_700_000_001 },
|
||||
{ pubkey: "e".repeat(64), latest: 1_700_000_002 },
|
||||
{ pubkey: "f".repeat(64), latest: 1_700_000_003 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
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: () => true,
|
||||
},
|
||||
});
|
||||
|
||||
document.body.appendChild(card.getRoot());
|
||||
|
||||
const contextBeforeOverride = card.getModerationContext();
|
||||
assert.equal(contextBeforeOverride.activeHidden, true);
|
||||
assert.equal(contextBeforeOverride.effectiveHideReason, "trusted-report-hide");
|
||||
assert.equal(contextBeforeOverride.allowOverride, true);
|
||||
|
||||
assert.equal(card.getRoot().dataset.moderationHidden, "true");
|
||||
assert.equal(card.getRoot().dataset.moderationHideReason, "trusted-report-hide");
|
||||
assert.equal(card.getRoot().dataset.moderationHideTrustedReportCount, "3");
|
||||
assert.ok(card.hiddenSummaryEl);
|
||||
assert.equal(card.anchorEl.hasAttribute("hidden"), true);
|
||||
assert.equal(card.contentEl.hasAttribute("hidden"), true);
|
||||
assert.ok(card.moderationActionButton);
|
||||
assert.ok(card.moderationBadgeEl.textContent.includes("Hidden"));
|
||||
|
||||
card.onModerationOverride = ({ video: overrideVideo, card: overrideCard }) =>
|
||||
app.handleModerationOverride({ video: overrideVideo, card: overrideCard });
|
||||
|
||||
card.moderationActionButton.click();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const contextAfterOverride = card.getModerationContext();
|
||||
assert.equal(contextAfterOverride.overrideActive, true);
|
||||
assert.equal(contextAfterOverride.activeHidden, false);
|
||||
assert.equal(contextAfterOverride.effectiveHideReason, "trusted-report-hide");
|
||||
|
||||
assert.equal(card.getRoot().dataset.moderationHidden, undefined);
|
||||
assert.equal(card.getRoot().dataset.moderationOverride, "show-anyway");
|
||||
assert.equal(card.getRoot().dataset.moderationHideReason, undefined);
|
||||
assert.equal(card.getRoot().dataset.moderationHideTrustedReportCount, undefined);
|
||||
assert.equal(card.anchorEl.hasAttribute("hidden"), false);
|
||||
assert.equal(card.contentEl.hasAttribute("hidden"), false);
|
||||
assert.ok(!card.hiddenSummaryEl || card.hiddenSummaryEl.parentElement !== card.getRoot());
|
||||
assert.ok(card.moderationBadgeEl.textContent.includes("Showing despite"));
|
||||
},
|
||||
);
|
||||
@@ -98,15 +98,51 @@ test.describe("moderation fixtures", () => {
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("trusted mute fixture annotates mute reason", async ({ page }) => {
|
||||
test("trusted report hide fixture supports show anyway override", async ({ page }) => {
|
||||
await page.goto(FIXTURE_URL, { waitUntil: "networkidle" });
|
||||
|
||||
const muteCard = page.locator('[data-test-id="trusted-mute"]');
|
||||
const hideCard = page.locator('[data-test-id="trusted-report-hide"]');
|
||||
const badge = hideCard.locator('[data-moderation-badge="true"]');
|
||||
const showAnywayButton = hideCard.getByRole("button", { name: "Show anyway" });
|
||||
|
||||
await expect(hideCard).toHaveAttribute("data-moderation-hidden", "true");
|
||||
await expect(hideCard).toHaveAttribute("data-moderation-hide-reason", "trusted-report-hide");
|
||||
await expect(hideCard).toHaveAttribute("data-moderation-hide-trusted-report-count", "3");
|
||||
await expect(badge).toContainText("Hidden · 3 trusted spam reports");
|
||||
await expect(showAnywayButton).toBeVisible();
|
||||
|
||||
await showAnywayButton.click();
|
||||
|
||||
await expect(hideCard).not.toHaveAttribute("data-moderation-hidden", "true");
|
||||
await expect(hideCard).toHaveAttribute("data-moderation-override", "show-anyway");
|
||||
await expect(hideCard.locator('[data-moderation-badge="true"]')).toContainText(
|
||||
"Showing despite 3 trusted spam reports",
|
||||
);
|
||||
});
|
||||
|
||||
test("trusted mute hide fixture annotates mute reason and can be overridden", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(FIXTURE_URL, { waitUntil: "networkidle" });
|
||||
|
||||
const muteCard = page.locator('[data-test-id="trusted-mute-hide"]');
|
||||
const badge = muteCard.locator('[data-moderation-badge="true"]');
|
||||
const showAnywayButton = muteCard.getByRole("button", { name: "Show anyway" });
|
||||
|
||||
await expect(muteCard).toHaveAttribute("data-moderation-hidden", "true");
|
||||
await expect(muteCard).toHaveAttribute("data-moderation-hide-reason", "trusted-mute-hide");
|
||||
await expect(muteCard).toHaveAttribute("data-moderation-hide-trusted-mute-count", "1");
|
||||
await expect(muteCard).toHaveAttribute("data-moderation-trusted-mute", "true");
|
||||
await expect(muteCard).toHaveAttribute("data-moderation-trusted-mute-count", "1");
|
||||
await expect(muteCard).not.toHaveAttribute("data-moderation-override", "show-anyway");
|
||||
await expect(badge).toContainText("Hidden · 1 trusted mute");
|
||||
await expect(showAnywayButton).toBeVisible();
|
||||
|
||||
const badge = muteCard.locator('[data-moderation-badge="true"]');
|
||||
await expect(badge).toContainText("Muted by a trusted contact");
|
||||
await showAnywayButton.click();
|
||||
|
||||
await expect(muteCard).not.toHaveAttribute("data-moderation-hidden", "true");
|
||||
await expect(muteCard).toHaveAttribute("data-moderation-override", "show-anyway");
|
||||
await expect(muteCard.locator('[data-moderation-badge="true"]')).toContainText(
|
||||
"Showing despite 1 trusted mute",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user