diff --git a/examples/reproducers/login-modal-bug-repro.md b/examples/reproducers/login-modal-bug-repro.md new file mode 100644 index 00000000..47041478 --- /dev/null +++ b/examples/reproducers/login-modal-bug-repro.md @@ -0,0 +1,17 @@ +# Login Modal Bug + +## Bug Description +The login modal fails to show error messages when invalid input (or no input) is submitted. Specifically, in the nsec/key login flow, submitting without entering a key should display an error message "Paste an nsec, hex key, or mnemonic to continue.", but the error element remains hidden. This is observed in `tests/e2e/ui-modals.spec.ts`. + +## Steps to Reproduce +1. Install dependencies: `npm install` +2. Run the reproducer script: `npx playwright test examples/reproducers/login-modal-bug-repro.spec.ts` + +## Logs +``` +Error: expect(locator).toBeVisible() failed +Locator: locator('[data-nsec-error]') +Expected: visible +Received: hidden +Timeout: 5000ms +``` diff --git a/examples/reproducers/login-modal-bug-repro.spec.ts b/examples/reproducers/login-modal-bug-repro.spec.ts new file mode 100644 index 00000000..0d5dc990 --- /dev/null +++ b/examples/reproducers/login-modal-bug-repro.spec.ts @@ -0,0 +1,127 @@ +import { expect, test } from "@playwright/test"; +import type { Page } from "@playwright/test"; + +// Helpers extracted from tests/e2e/helpers/uiTestUtils.ts to avoid relative import issues +export async function applyReducedMotion(page: Page) { + await page.evaluate(() => { + const style = document.createElement("style"); + style.innerHTML = ` + *, *::before, *::after { + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + } + `; + document.head.appendChild(style); + }); +} + +export function failOnConsoleErrors(page: Page) { + page.on("console", (message) => { + if (message.type() === "error") { + // Ignore known false positives or non-blocking errors if necessary + // For now, fail on any error + throw new Error(`Console error: ${message.text()}`); + } + }); +} + +test.describe("login modal bug reproduction", () => { + 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; + return { pubkey: "npub-test" }; + }, + hydrateFromStorage: async () => null, + on: () => () => {}, + }; + + const providers = [ + { + id: "nsec", + label: "Secret key", + description: "Use a raw key", + 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, + }, + helpers: { + prepareModal, + openModal, + closeModal, + setModalState: (name, isOpen) => { + window.__testModalState = { name, isOpen }; + }, + }, + }); + + window.__loginReady = true; + `, + }); + + await page.waitForFunction(() => window.__loginReady === true); + }); + + test("login modal error message should be visible", 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(); + + // Trigger the empty input error + await page.locator("[data-nsec-submit]").click(); + + const errorMessage = page.locator("[data-nsec-error]"); + + // This expectation fails in the current codebase + // The error message remains hidden + await expect(errorMessage).toBeVisible(); + await expect(errorMessage).toHaveText( + "Paste an nsec, hex key, or mnemonic to continue.", + ); + }); +}); diff --git a/examples/reproducers/moderation-bug-repro.md b/examples/reproducers/moderation-bug-repro.md new file mode 100644 index 00000000..ec107ad6 --- /dev/null +++ b/examples/reproducers/moderation-bug-repro.md @@ -0,0 +1,17 @@ +# Moderation UI Bug + +## Bug Description +The "Show anyway" button in the moderation UI fails to render in the test environment, causing `toBeVisible()` assertions to fail. This is observed in `tests/visual/moderation.spec.ts`. The logic for creating the button appears correct (`allowOverride` is true), but the button element is not found in the DOM during the test execution. This might be due to a race condition in the test fixture's state initialization or an environment-specific rendering issue. + +## Steps to Reproduce +1. Install dependencies: `npm install` +2. Run the reproducer script: `npx playwright test examples/reproducers/moderation-bug-repro.spec.ts` + +## Logs +``` +Error: expect(locator).toBeVisible() failed +Locator: locator('[data-test-id="show-anyway"]').getByRole('button', { name: 'Show anyway' }) +Expected: visible +Timeout: 5000ms +Error: element(s) not found +``` diff --git a/examples/reproducers/moderation-bug-repro.spec.ts b/examples/reproducers/moderation-bug-repro.spec.ts new file mode 100644 index 00000000..47745458 --- /dev/null +++ b/examples/reproducers/moderation-bug-repro.spec.ts @@ -0,0 +1,37 @@ +import { expect, test } from "@playwright/test"; + +const FIXTURE_URL = "/docs/moderation/fixtures/index.html"; +const STORAGE_KEY = "bitvid:moderation:fixture-overrides"; +const SENTINEL_KEY = "__moderationFixturesInit__"; + +function setupStorageReset(page) { + return page.addInitScript(({ storageKey, sentinelKey }) => { + try { + const session = window.sessionStorage; + const local = window.localStorage; + if (session && !session.getItem(sentinelKey)) { + local?.removeItem(storageKey); + session.setItem(sentinelKey, "1"); + } + } catch { + /* ignore reset failures in tests */ + } + }, { storageKey: STORAGE_KEY, sentinelKey: SENTINEL_KEY }); +} + +test.beforeEach(async ({ page }) => { + await setupStorageReset(page); +}); + +test.describe("moderation bug reproduction", () => { + test("show anyway override persists across reloads", async ({ page }) => { + await page.goto(FIXTURE_URL, { waitUntil: "networkidle" }); + + const overrideCard = page.locator('[data-test-id="show-anyway"]'); + const showAnywayButton = overrideCard.getByRole("button", { name: "Show anyway" }); + + // This expectation fails in the current codebase + // The "Show anyway" button should be visible but it's not found in the DOM + await expect(showAnywayButton).toBeVisible(); + }); +}); diff --git a/examples/reproducers/popover-bug-repro.md b/examples/reproducers/popover-bug-repro.md new file mode 100644 index 00000000..80e3da26 --- /dev/null +++ b/examples/reproducers/popover-bug-repro.md @@ -0,0 +1,16 @@ +# Popover ReferenceError Bug + +## Bug Description +The `tests/e2e/popover.spec.ts` test suite fails with `ReferenceError: getPanelWithTriggerMetrics is not defined`. This function appears to be defined in the test file, but for some reason, it is not accessible in the test scope where it is called. + +## Steps to Reproduce +1. Install dependencies: `npm install` +2. Run the reproducer script: `npx playwright test examples/reproducers/popover-bug-repro.spec.ts` + +Note: The provided reproducer script `examples/reproducers/popover-bug-repro.spec.ts` *fixes* the issue by correctly defining the function within the test file scope (or rather, copying the logic to where it works), effectively demonstrating what the code *should* do if the reference error didn't exist. To see the original error, run the original test suite: `npx playwright test tests/e2e/popover.spec.ts`. + +## Logs +``` +ReferenceError: getPanelWithTriggerMetrics is not defined + at /app/tests/e2e/popover.spec.ts:196:21 +``` diff --git a/examples/reproducers/popover-bug-repro.spec.ts b/examples/reproducers/popover-bug-repro.spec.ts new file mode 100644 index 00000000..4d00a191 --- /dev/null +++ b/examples/reproducers/popover-bug-repro.spec.ts @@ -0,0 +1,88 @@ +import { expect, test } from "@playwright/test"; +import type { Locator } from "@playwright/test"; + +// Simplified helper functions extracted from tests/e2e/popover.spec.ts to reproduce the ReferenceError +// The error "ReferenceError: getPanelWithTriggerMetrics is not defined" occurs in the test context +// even though the function appears to be defined in the file. + +test.describe("popover bug reproduction", () => { + test.beforeEach(async ({ page }) => { + // Navigate to the demo page + await page.goto("/views/dev/popover-demo.html", { waitUntil: "networkidle" }); + }); + + // Re-declaration of the function, similar to how it is in the failing test file. + // Note: The original test seems to define this function in the outer scope, + // but something about how Playwright handles scope or compilation might be causing the issue. + // However, the error 'ReferenceError: getPanelWithTriggerMetrics is not defined' + // suggests it is not available when called. + + 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; + + 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 || "", + }; + }, 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("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(); + + // This is where the error happens in the original test + const metrics = await getPanelWithTriggerMetrics(panel, trigger); + + assertWithinViewport(metrics); + expect(metrics.state).toBe("open"); + expect(metrics.placement).toBe("bottom-end"); + + const tolerance = 1.5; + expect(Math.abs(metrics.rect.right - metrics.triggerRect.right)).toBeLessThanOrEqual(tolerance); + }); +}); diff --git a/examples/reproducers/sidebar-bug-repro.md b/examples/reproducers/sidebar-bug-repro.md new file mode 100644 index 00000000..1cfc3579 --- /dev/null +++ b/examples/reproducers/sidebar-bug-repro.md @@ -0,0 +1,16 @@ +# Mobile Sidebar Layout Bug + +## Bug Description +The `tests/visual/overlay-layers.spec.ts` test suite fails in the "mobile sidebar shares desktop rail behavior" test. There is a mismatch between the expected margin of the app container and the computed margin in the test environment (diff of ~45px or ~12.8px depending on environment). This indicates a layout issue where the mobile sidebar or app container margins are not being calculated or applied correctly according to the CSS variables. + +## Steps to Reproduce +1. Install dependencies: `npm install` +2. Run the reproducer script: `npx playwright test examples/reproducers/sidebar-bug-repro.spec.ts` + +## Logs +``` +Error: expect(received).toBeLessThan(expected) + +Expected: < 0.5 +Received: 12.799999999999997 +``` diff --git a/examples/reproducers/sidebar-bug-repro.spec.ts b/examples/reproducers/sidebar-bug-repro.spec.ts new file mode 100644 index 00000000..32abd0b1 --- /dev/null +++ b/examples/reproducers/sidebar-bug-repro.spec.ts @@ -0,0 +1,94 @@ +import { expect, test } from "@playwright/test"; +import type { Page } from "@playwright/test"; + +test.describe("sidebar bug reproduction", () => { + async function dismissDisclaimerModal(page: Page) { + const modal = page.locator("#disclaimerModal"); + if ((await modal.count()) === 0) { + return; + } + + await page.evaluate(() => { + try { + window.localStorage?.setItem("hasSeenDisclaimer", "true"); + } catch (error) { + console.warn("Failed to persist disclaimer state", error); + } + document + .querySelectorAll("#disclaimerModal") + .forEach((node) => { + node.classList.add("hidden"); + node.setAttribute("data-open", "false"); + }); + document.documentElement?.classList.remove("modal-open"); + document.body?.classList.remove("modal-open"); + }); + } + + test("mobile sidebar margin mismatch", async ({ page }) => { + // Set viewport to mobile size + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/index.html", { waitUntil: "networkidle" }); + + await dismissDisclaimerModal(page); + + const initialLayout = await page.evaluate(() => { + const sidebar = document.getElementById("sidebar"); + const app = document.getElementById("app"); + if (!sidebar || !app) { + throw new Error("Missing sidebar layout nodes"); + } + + // Helper to compute expected margins from CSS variables + const ensureProbeStyles = () => { + if (document.getElementById("sidebar-width-probe-style")) { + return; + } + const style = document.createElement("style"); + style.id = "sidebar-width-probe-style"; + style.textContent = ` + .sidebar-width-probe { + position: absolute; + visibility: hidden; + } + + .sidebar-width-probe--collapsed { + width: calc(var(--sidebar-width-collapsed) + var(--sidebar-content-gap)); + } + `; + document.head.appendChild(style); + }; + + ensureProbeStyles(); + + const computeMargin = () => { + const probe = document.createElement("div"); + probe.classList.add("sidebar-width-probe", "sidebar-width-probe--collapsed"); + document.body.appendChild(probe); + const width = parseFloat(window.getComputedStyle(probe).width); + probe.remove(); + return width; + }; + + const marginLeft = parseFloat(window.getComputedStyle(app).marginLeft); + const state = sidebar.classList.contains("sidebar-expanded") + ? "expanded" + : sidebar.classList.contains("sidebar-collapsed") + ? "collapsed" + : "unknown"; + + return { + state, + marginLeft, + collapsedMargin: computeMargin(), + }; + }); + + // Verify initial state + expect(initialLayout.state).toBe("collapsed"); + + // This expectation fails in the current codebase + // There is a mismatch between the actual margin and the calculated margin from CSS variables + expect(Math.abs(initialLayout.marginLeft - initialLayout.collapsedMargin)).toBeLessThan(0.5); + }); +});