From 6df9f97da5ce4559e0104ee2e5b1bc4e9aaf8d6f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:17:50 +0000 Subject: [PATCH] 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. --- .../issue-001-video-card-logic/README.md | 11 + .../issue-001-video-card-logic/repro.mjs | 199 ++++++++++++++++++ .../issue-002-login-modal-nsec/README.md | 12 ++ .../issue-002-login-modal-nsec/repro.spec.ts | 131 ++++++++++++ .../issue-003-popover-ref-error/README.md | 12 ++ .../issue-003-popover-ref-error/repro.spec.ts | 95 +++++++++ 6 files changed, 460 insertions(+) create mode 100644 examples/reproducers/issue-001-video-card-logic/README.md create mode 100644 examples/reproducers/issue-001-video-card-logic/repro.mjs create mode 100644 examples/reproducers/issue-002-login-modal-nsec/README.md create mode 100644 examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts create mode 100644 examples/reproducers/issue-003-popover-ref-error/README.md create mode 100644 examples/reproducers/issue-003-popover-ref-error/repro.spec.ts diff --git a/examples/reproducers/issue-001-video-card-logic/README.md b/examples/reproducers/issue-001-video-card-logic/README.md new file mode 100644 index 00000000..3fe46741 --- /dev/null +++ b/examples/reproducers/issue-001-video-card-logic/README.md @@ -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`. diff --git a/examples/reproducers/issue-001-video-card-logic/repro.mjs b/examples/reproducers/issue-001-video-card-logic/repro.mjs new file mode 100644 index 00000000..c4f3d8fa --- /dev/null +++ b/examples/reproducers/issue-001-video-card-logic/repro.mjs @@ -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("", { + 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); +}); diff --git a/examples/reproducers/issue-002-login-modal-nsec/README.md b/examples/reproducers/issue-002-login-modal-nsec/README.md new file mode 100644 index 00000000..8d216fb4 --- /dev/null +++ b/examples/reproducers/issue-002-login-modal-nsec/README.md @@ -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. diff --git a/examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts b/examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts new file mode 100644 index 00000000..4bdb7951 --- /dev/null +++ b/examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts @@ -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.", + ); + }); +}); diff --git a/examples/reproducers/issue-003-popover-ref-error/README.md b/examples/reproducers/issue-003-popover-ref-error/README.md new file mode 100644 index 00000000..dd5ed579 --- /dev/null +++ b/examples/reproducers/issue-003-popover-ref-error/README.md @@ -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. diff --git a/examples/reproducers/issue-003-popover-ref-error/repro.spec.ts b/examples/reproducers/issue-003-popover-ref-error/repro.spec.ts new file mode 100644 index 00000000..245a7a60 --- /dev/null +++ b/examples/reproducers/issue-003-popover-ref-error/repro.spec.ts @@ -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); + }); +});