From 511d664eeb937e352dac5d0dcc1c1d495eb5bcc3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:36:16 +0000 Subject: [PATCH] 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 <53631862+PR0M3TH3AN@users.noreply.github.com> --- .../reproducers/popover-flip-bug/README.md | 27 +++ .../popover-flip-bug/repro.spec.ts | 77 +++++++ examples/reproducers/video-card-bug/README.md | 21 ++ examples/reproducers/video-card-bug/repro.mjs | 196 ++++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 examples/reproducers/popover-flip-bug/README.md create mode 100644 examples/reproducers/popover-flip-bug/repro.spec.ts create mode 100644 examples/reproducers/video-card-bug/README.md create mode 100644 examples/reproducers/video-card-bug/repro.mjs diff --git a/examples/reproducers/popover-flip-bug/README.md b/examples/reproducers/popover-flip-bug/README.md new file mode 100644 index 00000000..01ff5e52 --- /dev/null +++ b/examples/reproducers/popover-flip-bug/README.md @@ -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. diff --git a/examples/reproducers/popover-flip-bug/repro.spec.ts b/examples/reproducers/popover-flip-bug/repro.spec.ts new file mode 100644 index 00000000..ec64a5fb --- /dev/null +++ b/examples/reproducers/popover-flip-bug/repro.spec.ts @@ -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); +}); diff --git a/examples/reproducers/video-card-bug/README.md b/examples/reproducers/video-card-bug/README.md new file mode 100644 index 00000000..efb15977 --- /dev/null +++ b/examples/reproducers/video-card-bug/README.md @@ -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. diff --git a/examples/reproducers/video-card-bug/repro.mjs b/examples/reproducers/video-card-bug/repro.mjs new file mode 100644 index 00000000..b8e6f602 --- /dev/null +++ b/examples/reproducers/video-card-bug/repro.mjs @@ -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("", { + 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); +});