mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add minimal reproducers for known bugs
- Added `examples/reproducers/issue-001-video-card-logic/` to reproduce VideoCard logic failure. - Added `examples/reproducers/issue-002-login-modal-nsec/` to reproduce Login Modal missing error. - Added `examples/reproducers/issue-003-popover-ref-error/` to reproduce Popover ReferenceError. - Each reproducer includes a README.md with instructions.
This commit is contained in:
1 parent
0eacc08200
commit
6df9f97da5
6 files changed
+460
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
# Issue 001: VideoCard Logic Failure
|
||||||
|
|
||||||
|
This reproducer demonstrates the bug where `VideoCard` block action fails to restore the trusted mute hide state after an override.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
```bash
|
||||||
|
node --import ./tests/test-helpers/setup-localstorage.mjs examples/reproducers/issue-001-video-card-logic/repro.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
The test should FAIL with an `AssertionError: false !== true`.
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
|
import "../../../tests/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 "../../../tests/helpers/moderation-test-helpers.mjs";
|
||||||
|
import {
|
||||||
|
applyModerationContextDatasets,
|
||||||
|
normalizeVideoModerationContext,
|
||||||
|
} from "../../../js/ui/moderationUiHelpers.js";
|
||||||
|
import { buildModerationBadgeText } from "../../../js/ui/moderationCopy.js";
|
||||||
|
import { userBlocks } from "../../../js/userBlocks.js";
|
||||||
|
|
||||||
|
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",
|
||||||
|
];
|
||||||
|
|
||||||
|
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];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock navigator safely
|
||||||
|
const originalNavigator = globalThis.navigator;
|
||||||
|
Object.defineProperty(globalThis, "navigator", {
|
||||||
|
value: window.navigator,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Restore navigator
|
||||||
|
Object.defineProperty(globalThis, "navigator", {
|
||||||
|
value: originalNavigator,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { window, document: window.document };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproducer for skipped test: "VideoCard block action restores trusted mute hide state after override"
|
||||||
|
test("VideoCard block action restores trusted mute hide state after override", async (t) => {
|
||||||
|
const { document } = setupDom(t);
|
||||||
|
withMockedNostrTools(t);
|
||||||
|
|
||||||
|
const app = await createModerationAppHarness();
|
||||||
|
app.getActiveModerationThresholds = () => ({
|
||||||
|
autoplayBlockThreshold: Number.POSITIVE_INFINITY,
|
||||||
|
blurThreshold: Number.POSITIVE_INFINITY,
|
||||||
|
trustedMuteHideThreshold: 1,
|
||||||
|
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
|
||||||
|
});
|
||||||
|
app.pubkey = "f".repeat(64);
|
||||||
|
app.isUserLoggedIn = () => true;
|
||||||
|
app.showStatus = () => {};
|
||||||
|
app.showError = () => {};
|
||||||
|
app.onVideosShouldRefresh = async () => {};
|
||||||
|
|
||||||
|
const originalEnsureLoaded = userBlocks.ensureLoaded;
|
||||||
|
const originalAddBlock = userBlocks.addBlock;
|
||||||
|
const originalIsBlocked = userBlocks.isBlocked;
|
||||||
|
|
||||||
|
userBlocks.ensureLoaded = async () => {};
|
||||||
|
userBlocks.addBlock = async () => ({ ok: true });
|
||||||
|
userBlocks.isBlocked = () => false;
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
userBlocks.ensureLoaded = originalEnsureLoaded;
|
||||||
|
userBlocks.addBlock = originalAddBlock;
|
||||||
|
userBlocks.isBlocked = originalIsBlocked;
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoId = "d".repeat(64);
|
||||||
|
const video = {
|
||||||
|
id: videoId,
|
||||||
|
title: "Muted Hide Clip",
|
||||||
|
pubkey: "e".repeat(64),
|
||||||
|
moderation: {
|
||||||
|
trustedMuted: true,
|
||||||
|
trustedMuteCount: 1,
|
||||||
|
trustedMuters: ["f".repeat(64)],
|
||||||
|
trustedCount: 0,
|
||||||
|
reportType: "nudity",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
normalizeVideoModerationContext(card.video?.moderation).activeHidden,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(card.getRoot().dataset.moderationHidden, "true");
|
||||||
|
|
||||||
|
card.onModerationOverride = ({ video: overrideVideo, card: overrideCard }) =>
|
||||||
|
app.handleModerationOverride({ video: overrideVideo, card: overrideCard });
|
||||||
|
|
||||||
|
card.moderationActionButton.click();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const contextAfterOverride = normalizeVideoModerationContext(
|
||||||
|
card.video?.moderation,
|
||||||
|
);
|
||||||
|
assert.equal(contextAfterOverride.overrideActive, true);
|
||||||
|
assert.equal(contextAfterOverride.activeHidden, false);
|
||||||
|
assert.ok(card.moderationBlockButton);
|
||||||
|
|
||||||
|
card.onModerationBlock = ({ video: targetVideo, card: targetCard }) =>
|
||||||
|
app.handleModerationBlock({ video: targetVideo, card: targetCard });
|
||||||
|
|
||||||
|
card.moderationBlockButton.click();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const contextAfterHide = normalizeVideoModerationContext(card.video?.moderation);
|
||||||
|
assert.equal(contextAfterHide.overrideActive, false);
|
||||||
|
assert.equal(contextAfterHide.activeHidden, true);
|
||||||
|
assert.equal(card.getRoot().dataset.moderationHidden, "true");
|
||||||
|
assert.equal(card.moderationBlockButton, null);
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Issue 002: Login Modal NSEC Error
|
||||||
|
|
||||||
|
This reproducer attempts to reproduce the issue where `login-modal` fails to find the `[data-nsec-error]` element.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
```bash
|
||||||
|
npx playwright test examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
If the bug is present, the test will FAIL waiting for `[data-nsec-error]` to be visible.
|
||||||
|
Note: In some environments, this test might pass if the bug is flaky or fixed.
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
applyReducedMotion,
|
||||||
|
failOnConsoleErrors,
|
||||||
|
} from "../../../tests/e2e/helpers/uiTestUtils";
|
||||||
|
|
||||||
|
test.describe("login modal flows (reproducer)", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await applyReducedMotion(page);
|
||||||
|
failOnConsoleErrors(page);
|
||||||
|
await page.goto("/components/login-modal.html", { waitUntil: "networkidle" });
|
||||||
|
|
||||||
|
await page.addScriptTag({
|
||||||
|
type: "module",
|
||||||
|
content: `
|
||||||
|
import LoginModalController from "/js/ui/loginModalController.js";
|
||||||
|
import {
|
||||||
|
prepareStaticModal,
|
||||||
|
openStaticModal,
|
||||||
|
closeStaticModal,
|
||||||
|
} from "/js/ui/components/staticModalAccessibility.js";
|
||||||
|
|
||||||
|
const modalElement = document.getElementById("loginModal");
|
||||||
|
window.__testAuth = {
|
||||||
|
lastRequest: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authService = {
|
||||||
|
requestLogin: async (options) => {
|
||||||
|
window.__testAuth.lastRequest = options;
|
||||||
|
if (options.providerId === "nip46") {
|
||||||
|
if (typeof options.onHandshakePrepared === "function") {
|
||||||
|
options.onHandshakePrepared({ uri: "nostr:test-uri" });
|
||||||
|
}
|
||||||
|
await new Promise(() => {});
|
||||||
|
}
|
||||||
|
return { pubkey: "npub-test" };
|
||||||
|
},
|
||||||
|
hydrateFromStorage: async () => null,
|
||||||
|
on: () => () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
{
|
||||||
|
id: "nsec",
|
||||||
|
label: "Secret key",
|
||||||
|
description: "Use a raw key",
|
||||||
|
login: async () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nip46",
|
||||||
|
label: "Remote signer",
|
||||||
|
description: "Pair a signer",
|
||||||
|
login: async () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nip07",
|
||||||
|
label: "Extension",
|
||||||
|
description: "Browser extension",
|
||||||
|
login: async () => {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const prepareModal = (modal) =>
|
||||||
|
prepareStaticModal({ root: modal }) || modal;
|
||||||
|
|
||||||
|
const openModal = ({ modal, triggerElement } = {}) =>
|
||||||
|
openStaticModal(modal, { triggerElement });
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
if (modalElement) {
|
||||||
|
closeStaticModal(modalElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__loginController = new LoginModalController({
|
||||||
|
modalElement,
|
||||||
|
providers,
|
||||||
|
services: {
|
||||||
|
authService,
|
||||||
|
nostrClient: {
|
||||||
|
getRemoteSignerStatus: () => null,
|
||||||
|
onRemoteSignerChange: () => () => {},
|
||||||
|
getStoredNip46Metadata: () => null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
helpers: {
|
||||||
|
prepareModal,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
setModalState: (name, isOpen) => {
|
||||||
|
window.__testModalState = { name, isOpen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeButton = document.getElementById("closeLoginModal");
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener("click", () => {
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__loginReady = true;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForFunction(() => window.__loginReady === true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles nsec login flow (reproduces missing error element)", async ({ page }) => {
|
||||||
|
const modal = page.locator("#loginModal");
|
||||||
|
|
||||||
|
await page.evaluate(() => window.__loginController.openModal());
|
||||||
|
await expect(modal).toHaveAttribute("data-open", "true");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('[data-provider-button][data-provider-id="nsec"]')
|
||||||
|
.click();
|
||||||
|
await expect(page.locator("[data-nsec-secret]")).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator("[data-nsec-submit]").click();
|
||||||
|
const errorMessage = page.locator("[data-nsec-error]");
|
||||||
|
|
||||||
|
// This is expected to fail if the bug exists
|
||||||
|
await expect(errorMessage).toBeVisible();
|
||||||
|
await expect(errorMessage).toHaveText(
|
||||||
|
"Paste an nsec, hex key, or mnemonic to continue.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Issue 003: Popover ReferenceError
|
||||||
|
|
||||||
|
This reproducer attempts to reproduce the `ReferenceError` in popover placement tests.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
```bash
|
||||||
|
npx playwright test examples/reproducers/issue-003-popover-ref-error/repro.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
If the bug is present, the test will FAIL with `ReferenceError`.
|
||||||
|
Note: In some environments, this test might pass if the bug is flaky or fixed.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import type { Locator } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
applyReducedMotion,
|
||||||
|
failOnConsoleErrors,
|
||||||
|
} from "../../../tests/e2e/helpers/uiTestUtils";
|
||||||
|
|
||||||
|
// Function that might be causing reference error
|
||||||
|
async function getPanelWithTriggerMetrics(panel: Locator, trigger: Locator) {
|
||||||
|
const triggerHandle = await trigger.elementHandle();
|
||||||
|
if (!triggerHandle) {
|
||||||
|
throw new Error("Trigger element is not attached");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await panel.evaluate((node, triggerElement) => {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
const triggerRect = triggerElement.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const tokenValue = window
|
||||||
|
.getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--popover-inline-safe-max")
|
||||||
|
.trim();
|
||||||
|
const popoverMaxWidth = window
|
||||||
|
.getComputedStyle(node)
|
||||||
|
.getPropertyValue("--popover-max-width")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
rect: {
|
||||||
|
top: rect.top,
|
||||||
|
left: rect.left,
|
||||||
|
right: rect.right,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
},
|
||||||
|
triggerRect: {
|
||||||
|
top: triggerRect.top,
|
||||||
|
left: triggerRect.left,
|
||||||
|
right: triggerRect.right,
|
||||||
|
bottom: triggerRect.bottom,
|
||||||
|
width: triggerRect.width,
|
||||||
|
height: triggerRect.height,
|
||||||
|
},
|
||||||
|
viewport: { width: viewportWidth, height: viewportHeight },
|
||||||
|
placement: node.dataset.popoverPlacement || "",
|
||||||
|
state: node.dataset.popoverState || "",
|
||||||
|
popoverMaxWidth,
|
||||||
|
tokenMaxWidth: tokenValue,
|
||||||
|
};
|
||||||
|
}, triggerHandle);
|
||||||
|
} finally {
|
||||||
|
await triggerHandle.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertWithinViewport(metrics: {
|
||||||
|
rect: { top: number; left: number; right: number; bottom: number };
|
||||||
|
viewport: { width: number; height: number };
|
||||||
|
}) {
|
||||||
|
const tolerance = 0.5;
|
||||||
|
expect(metrics.rect.left).toBeGreaterThanOrEqual(-tolerance);
|
||||||
|
expect(metrics.rect.top).toBeGreaterThanOrEqual(-tolerance);
|
||||||
|
expect(metrics.rect.right).toBeLessThanOrEqual(metrics.viewport.width + tolerance);
|
||||||
|
expect(metrics.rect.bottom).toBeLessThanOrEqual(metrics.viewport.height + tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("popover demo alignments (reproducer)", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await applyReducedMotion(page);
|
||||||
|
failOnConsoleErrors(page);
|
||||||
|
await page.goto("/views/dev/popover-demo.html", { waitUntil: "networkidle" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("aligns the video card menu with the trigger's right edge", async ({ page }) => {
|
||||||
|
const trigger = page.locator("[data-demo-card-more]");
|
||||||
|
await trigger.click();
|
||||||
|
|
||||||
|
const panel = page.locator('[data-component="overlay-root"] [data-popover-state="open"]');
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
|
const metrics = await getPanelWithTriggerMetrics(panel, trigger);
|
||||||
|
|
||||||
|
assertWithinViewport(metrics);
|
||||||
|
expect(metrics.state).toBe("open");
|
||||||
|
expect(metrics.placement).toBe("bottom-end");
|
||||||
|
expect(metrics.popoverMaxWidth).toBe(metrics.tokenMaxWidth);
|
||||||
|
expect(metrics).toHaveProperty("popoverMaxWidth");
|
||||||
|
|
||||||
|
const tolerance = 1.5;
|
||||||
|
expect(Math.abs(metrics.rect.right - metrics.triggerRect.right)).toBeLessThanOrEqual(tolerance);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user