mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
chore(ai): add minimal reproducers for known bugs
Added minimal reproducers for: - Popover flip bug (Playwright) - VideoCard block action bug (Node.js) Reproducers are located in `examples/reproducers/`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
73c705831b
commit
511d664eeb
4 files changed
+321
No files matched your search
@@ -0,0 +1,27 @@
|
||||
# Popover Flip Bug Reproducer
|
||||
|
||||
This reproducer demonstrates a bug where the popover fails to flip to the top when the bottom viewport space is restricted.
|
||||
|
||||
## Running the Reproducer
|
||||
|
||||
1. Ensure dependencies are installed:
|
||||
```bash
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
2. Build the CSS (required for the test page):
|
||||
```bash
|
||||
npm run build:css
|
||||
```
|
||||
|
||||
3. Run the Playwright test using the reproducer config:
|
||||
```bash
|
||||
npx playwright test -c examples/reproducers/playwright.config.ts examples/reproducers/popover-flip-bug/repro.spec.ts
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
The test should FAIL with an assertion error similar to:
|
||||
`Error: expect(received).toBeLessThanOrEqual(expected)`
|
||||
indicating that the popover's bottom coordinate is outside the viewport.
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { Locator } from "@playwright/test";
|
||||
import {
|
||||
applyReducedMotion,
|
||||
failOnConsoleErrors,
|
||||
} from "../../../tests/e2e/helpers/uiTestUtils";
|
||||
|
||||
async function getPanelMetrics(locator: Locator) {
|
||||
return locator.evaluate((node) => {
|
||||
const rect = node.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,
|
||||
},
|
||||
viewport: { width: viewportWidth, height: viewportHeight },
|
||||
placement: node.dataset.popoverPlacement || "",
|
||||
state: node.dataset.popoverState || "",
|
||||
popoverMaxWidth,
|
||||
tokenMaxWidth: tokenValue,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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("keeps the bottom-right grid menu inside the viewport", async ({ page }) => {
|
||||
await applyReducedMotion(page);
|
||||
failOnConsoleErrors(page);
|
||||
// Ensure we are testing against the correct page served by the root http-server
|
||||
await page.goto("/docs/popover-scenarios.html", { waitUntil: "networkidle" });
|
||||
|
||||
// Force a tall body to ensure there is space for the popover to flip to the top
|
||||
await page.addStyleTag({ content: "body { min-height: 2000px !important; }" });
|
||||
// Reduce viewport height to force the menu to flip to the top
|
||||
await page.setViewportSize({ width: 1280, height: 250 });
|
||||
|
||||
const trigger = page.locator('[data-test-trigger="grid-bottom-right"]');
|
||||
// Scroll to bottom to ensure the element is near the viewport edge
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
|
||||
await trigger.click();
|
||||
const panel = page.locator('[data-test-panel="grid-bottom-right"]');
|
||||
await expect(panel).toHaveAttribute("data-popover-state", "open");
|
||||
|
||||
const metrics = await getPanelMetrics(panel);
|
||||
|
||||
assertWithinViewport(metrics);
|
||||
expect(metrics.state).toBe("open");
|
||||
expect(metrics.placement.startsWith("top"), `Expected placement to start with 'top' but got '${metrics.placement}'`).toBeTruthy();
|
||||
expect(metrics.popoverMaxWidth).toBe(metrics.tokenMaxWidth);
|
||||
expect(metrics.tokenMaxWidth.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
# VideoCard Block Action Bug Reproducer
|
||||
|
||||
This reproducer demonstrates a bug where the VideoCard fails to restore the trusted mute hide state after a block action is performed on an overridden video.
|
||||
|
||||
## Running the Reproducer
|
||||
|
||||
1. Ensure dependencies are installed:
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
2. Run the reproducer script:
|
||||
```bash
|
||||
node examples/reproducers/video-card-bug/repro.mjs
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
The test should FAIL with an assertion error similar to:
|
||||
`AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: false !== true`
|
||||
indicating that `contextAfterHide.activeHidden` is false instead of true.
|
||||
@@ -0,0 +1,196 @@
|
||||
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 } from "../../../js/state/cache.js";
|
||||
import {
|
||||
withMockedNostrTools,
|
||||
createModerationAppHarness,
|
||||
} from "../../../tests/helpers/moderation-test-helpers.mjs";
|
||||
import {
|
||||
normalizeVideoModerationContext,
|
||||
} from "../../../js/ui/moderationUiHelpers.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 };
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user