Add minimal reproducers for known bugs

- Added reproducer for mobile sidebar margin mismatch (tests/visual/overlay-layers.spec.ts).
- Added reproducer for Moderation UI "Show anyway" button visibility (tests/visual/moderation.spec.ts).
- Added reproducer for NIP-07 login unit test ReferenceError (tests/nostr-login-permissions.test.mjs).
- Added examples/reproducers/playwright.config.ts to support running these tests.
- Updated examples/reproducers/README.md with instructions.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-29 12:18:28 +00:00
1 parent b48320fc9a
commit 1b7575eaab
5 files changed
+561 -16

No files matched your search

+31
View File
@@ -21,3 +21,34 @@ Example structure:
- `buildVideoPostEvent` crashes on circular objects if not handled (Fixed).
- `normalizeAndAugmentMagnet` crashes on non-iterable `extraTrackers` (Fixed).
- `build...` functions crash on `null`/`undefined` input (Fixed).
# Bug Reproducers
This directory also contains minimal reproducers for known bugs in the repository.
## Sidebar Layout (Mobile Margin Mismatch)
**Issue**: The sidebar layout on mobile viewports has a margin calculation mismatch.
**Reproducer**: `examples/reproducers/issue-sidebar-layout/repro.spec.ts`
**Run**:
```bash
npx playwright test -c examples/reproducers/playwright.config.ts examples/reproducers/issue-sidebar-layout/repro.spec.ts
```
## Moderation UI ("Show Anyway" Button Visibility)
**Issue**: The "Show anyway" button fails to appear in the DOM during tests, causing visibility checks to fail.
**Reproducer**: `examples/reproducers/issue-moderation-ui/repro.spec.ts`
**Run**:
```bash
npx playwright test -c examples/reproducers/playwright.config.ts examples/reproducers/issue-moderation-ui/repro.spec.ts
```
## NIP-07 Login Test (ReferenceError)
**Issue**: The unit test for NIP-07 login fallback fails with `ReferenceError: plainCall is not defined`.
**Reproducer**: `examples/reproducers/issue-nip07-login/repro.mjs`
**Run**:
```bash
node examples/reproducers/issue-nip07-login/repro.mjs
```
@@ -1,43 +1,118 @@
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__";
const RESTORE_BUTTON_LABEL = "Restore default moderation";
async function waitForFixtureReady(page) {
await page.waitForSelector('body[data-ready="true"]');
}
test("repro: show anyway override button visibility", async ({ page }) => {
// 1. Navigate to fixture
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.afterEach(async ({ page }) => {
try {
await page.evaluate(({ key, sentinel }) => {
window.localStorage?.removeItem(key);
window.sessionStorage?.removeItem(sentinel);
}, { key: STORAGE_KEY, sentinel: SENTINEL_KEY });
} catch {
// ignore cleanup errors when page already closed
}
});
test.describe("moderation fixtures", () => {
test("show anyway override persists across reloads", async ({ page }) => {
await page.goto(FIXTURE_URL, { waitUntil: "networkidle" });
await waitForFixtureReady(page);
// 2. Locate the card that should have the button
const overrideCard = page.locator('[data-test-id="show-anyway"]');
const thumbnail = overrideCard.locator('img[data-video-thumbnail]');
// 3. Verify it has the override available attribute
await expect(overrideCard).toHaveAttribute(
"data-moderation-override-available",
"true"
);
// 4. Locate the button
const showAnywayButton = overrideCard.getByRole("button", { name: "Show anyway" });
const restoreButtonQuery = overrideCard.getByRole("button", {
name: RESTORE_BUTTON_LABEL,
});
// 5. Assert visibility (this is expected to fail)
console.log("Checking visibility of 'Show anyway' button...");
await expect(showAnywayButton).toBeVisible();
await expect(overrideCard).toHaveAttribute("data-autoplay-policy", "blocked");
await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred");
console.log("'Show anyway' button is visible. Attempting click...");
// 6. Click (this is expected to fail/timeout if not visible)
// Uncommented failures:
await expect(showAnywayButton).toBeVisible();
await expect(restoreButtonQuery).toHaveCount(0);
// Uncommented failure:
await showAnywayButton.click({ force: true });
// 7. Verify state change
/*
await expect(overrideCard).toHaveAttribute(
"data-moderation-override",
"show-anyway"
);
await expect(overrideCard).not.toHaveAttribute("data-autoplay-policy", "blocked");
await expect(thumbnail).not.toHaveAttribute("data-thumbnail-state", "blurred");
const restoreButton = overrideCard.getByRole("button", {
name: RESTORE_BUTTON_LABEL,
});
await expect(restoreButton).toBeVisible();
await expect(overrideCard.getByRole("button", { name: "Show anyway" })).toHaveCount(0);
await page.reload({ waitUntil: "networkidle" });
await waitForFixtureReady(page);
const reloadedCard = page.locator('[data-test-id="show-anyway"]');
const reloadedThumbnail = reloadedCard.locator('img[data-video-thumbnail]');
const reloadedRestoreButton = reloadedCard.getByRole("button", {
name: RESTORE_BUTTON_LABEL,
});
await expect(reloadedCard).toHaveAttribute(
"data-moderation-override",
"show-anyway"
);
await expect(reloadedCard).not.toHaveAttribute("data-autoplay-policy", "blocked");
await expect(reloadedThumbnail).not.toHaveAttribute("data-thumbnail-state", "blurred");
await expect(reloadedRestoreButton).toBeVisible();
await expect(
reloadedCard.getByRole("button", { name: "Show anyway" })
).toHaveCount(0);
await reloadedRestoreButton.click();
await expect(reloadedCard).not.toHaveAttribute(
"data-moderation-override",
"show-anyway"
);
await expect(reloadedCard).toHaveAttribute("data-autoplay-policy", "blocked");
await expect(reloadedThumbnail).toHaveAttribute("data-thumbnail-state", "blurred");
await expect(
reloadedCard.getByRole("button", { name: RESTORE_BUTTON_LABEL })
).toHaveCount(0);
await expect(
reloadedCard.getByRole("button", { name: "Show anyway" })
).toBeVisible();
*/
});
});
@@ -0,0 +1,193 @@
import "../../../tests/test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import { nostrClient } from "../../../js/nostrClientFacade.js";
import { NostrClient, __testExports } from "../../../js/nostr/client.js";
import nip07Provider from "../../../js/services/authProviders/nip07.js";
import { accessControl } from "../../../js/accessControl.js";
const {
DEFAULT_NIP07_ENCRYPTION_METHODS,
} = __testExports ?? {};
const HEX_PUBKEY = "f".repeat(64);
const PERMISSIONS_STORAGE_KEY = "bitvid:nip07:permissions";
function clearStoredPermissions() {
if (typeof localStorage === "undefined" || !localStorage) {
return;
}
try {
localStorage.removeItem(PERMISSIONS_STORAGE_KEY);
} catch (error) {
// Ignore storage cleanup issues in tests
}
}
test.beforeEach(() => {
clearStoredPermissions();
});
test.afterEach(() => {
clearStoredPermissions();
});
function setupLoginEnvironment({ enableImpl, getPublicKey = HEX_PUBKEY } = {}) {
const previousWindow = typeof global.window === "undefined" ? undefined : global.window;
const previousGlobalNostr = global.nostr;
const previousGlobalNostrTools = global.NostrTools;
const previousCanonicalTools = global.__BITVID_CANONICAL_NOSTR_TOOLS__;
const previousNostrToolsReady = global.nostrToolsReady;
const windowRef = previousWindow ?? global;
global.window = windowRef;
const previousWindowNostr = previousWindow?.nostr;
const previousWindowNostrTools = previousWindow?.NostrTools;
const enableCalls = [];
const nostrStub = {
enable: (options) => {
enableCalls.push(options);
if (typeof enableImpl === "function") {
return Promise.resolve(enableImpl(options, enableCalls.length));
}
return Promise.resolve();
},
getPublicKey: () => Promise.resolve(getPublicKey),
};
windowRef.nostr = nostrStub;
global.nostr = nostrStub;
const toolkitStub = {
nip19: {
npubEncode: (hex) => `npub${hex}`,
},
};
windowRef.NostrTools = toolkitStub;
global.NostrTools = toolkitStub;
global.__BITVID_CANONICAL_NOSTR_TOOLS__ = toolkitStub;
global.nostrToolsReady = Promise.resolve({ ok: true, value: toolkitStub });
const originalAccessControl = {
canAccess: accessControl.canAccess,
getWhitelist: accessControl.getWhitelist,
getBlacklist: accessControl.getBlacklist,
isBlacklisted: accessControl.isBlacklisted,
};
accessControl.canAccess = () => true;
accessControl.getWhitelist = () => [];
accessControl.getBlacklist = () => [];
accessControl.isBlacklisted = () => false;
if (
nostrClient.extensionPermissionCache &&
typeof nostrClient.extensionPermissionCache.clear === "function"
) {
nostrClient.extensionPermissionCache.clear();
}
clearStoredPermissions();
return {
enableCalls,
restore() {
accessControl.canAccess = originalAccessControl.canAccess;
accessControl.getWhitelist = originalAccessControl.getWhitelist;
accessControl.getBlacklist = originalAccessControl.getBlacklist;
accessControl.isBlacklisted = originalAccessControl.isBlacklisted;
if (previousWindow === undefined) {
delete global.window;
} else {
global.window = previousWindow;
if (previousWindow) {
previousWindow.nostr = previousWindowNostr;
previousWindow.NostrTools = previousWindowNostrTools;
}
}
if (previousGlobalNostr === undefined) {
delete global.nostr;
} else {
global.nostr = previousGlobalNostr;
}
if (previousGlobalNostrTools === undefined) {
delete global.NostrTools;
} else {
global.NostrTools = previousGlobalNostrTools;
}
global.__BITVID_CANONICAL_NOSTR_TOOLS__ = previousCanonicalTools;
if (previousNostrToolsReady === undefined) {
delete global.nostrToolsReady;
} else {
global.nostrToolsReady = previousNostrToolsReady;
}
if (
nostrClient.extensionPermissionCache &&
typeof nostrClient.extensionPermissionCache.clear === "function"
) {
nostrClient.extensionPermissionCache.clear();
}
clearStoredPermissions();
},
};
}
test("NIP-07 login falls back when structured permissions fail", async () => {
clearStoredPermissions();
const env = setupLoginEnvironment({
enableImpl(options) {
if (
options &&
Array.isArray(options.permissions) &&
options.permissions.length &&
typeof options.permissions[0] === "object"
) {
return Promise.reject(new Error("object permissions unsupported"));
}
if (options && Array.isArray(options.permissions)) {
return Promise.resolve();
}
return Promise.reject(new Error("unexpected enable invocation"));
},
});
try {
const result = await nip07Provider.login({ nostrClient });
const pubkey = result.pubkey;
assert.equal(pubkey, HEX_PUBKEY);
assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads");
const [objectCall, stringCall] = env.enableCalls;
assert.ok(Array.isArray(objectCall?.permissions));
assert.equal(
typeof objectCall.permissions[0],
"object",
"structured permission request should be attempted first",
);
assert.ok(Array.isArray(stringCall?.permissions));
assert.equal(
typeof stringCall.permissions[0],
"string",
"string-based permissions should be attempted after structured payloads",
);
// THIS ASSERTION CAUSES ReferenceError: plainCall is not defined
assert.equal(
plainCall,
undefined,
"plain enable() should only be attempted if prior payloads fail",
);
} finally {
env.restore();
nostrClient.logout();
clearStoredPermissions();
}
});
@@ -0,0 +1,240 @@
import { expect, test } from "@playwright/test";
import type { Page } from "@playwright/test";
declare global {
interface Window {
__testToast?: {
show(message: string, options?: Record<string, unknown>): unknown;
};
}
}
test.describe("overlay layering tokens", () => {
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<HTMLElement>("#disclaimerModal")
.forEach((node) => {
node.classList.add("hidden");
node.setAttribute("data-open", "false");
});
document.documentElement?.classList.remove("modal-open");
document.body?.classList.remove("modal-open");
});
await page.waitForFunction(() =>
Array.from(
document.querySelectorAll("#disclaimerModal")
).every((modalElement) => modalElement.classList.contains("hidden"))
);
}
test("mobile sidebar shares desktop rail behavior", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/index.html", { waitUntil: "networkidle" });
await dismissDisclaimerModal(page);
// Wait for initial fade-in to complete so opacity doesn't interfere with visibility checks
await page.waitForFunction(
() => !document.getElementById("sidebar")?.classList.contains("fade-in")
);
await expect(page.locator("#mobileMenuBtn")).toHaveCount(0);
const collapseToggle = page.locator("#sidebarCollapseToggle");
await expect(collapseToggle).toBeVisible();
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");
}
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));
}
.sidebar-width-probe--expanded {
width: calc(var(--sidebar-width-expanded) + var(--sidebar-content-gap));
}
`;
document.head.appendChild(style);
};
ensureProbeStyles();
const computeMargin = (widthVar) => {
const variant =
widthVar === "--sidebar-width-collapsed"
? "sidebar-width-probe--collapsed"
: widthVar === "--sidebar-width-expanded"
? "sidebar-width-probe--expanded"
: null;
if (!variant) {
throw new Error(`Unsupported sidebar width variable: ${widthVar}`);
}
const probe = document.createElement("div");
probe.classList.add("sidebar-width-probe", variant);
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("--sidebar-width-collapsed"),
expandedMargin: computeMargin("--sidebar-width-expanded"),
overlayExists: Boolean(document.getElementById("sidebarOverlay")),
};
});
expect(initialLayout.overlayExists).toBe(false);
expect(initialLayout.state).toBe("collapsed");
expect(Math.abs(initialLayout.marginLeft - initialLayout.collapsedMargin)).toBeLessThan(0.5);
// Ensure the toggle is initialized
await expect(collapseToggle).toHaveAttribute("data-state", /.*/);
await collapseToggle.click({ force: true });
await page.waitForFunction(() => {
const sidebar = document.getElementById("sidebar");
return (
sidebar?.classList.contains("sidebar-expanded") &&
!sidebar?.classList.contains("sidebar-collapsed")
);
});
// Wait for sidebar transition to complete
await page.waitForTimeout(1000);
// Ensure the footer button is scrolled into view (especially for mobile viewports)
await page.evaluate(() => {
const sidebar = document.getElementById("sidebar");
if (sidebar) {
sidebar.scrollTop = sidebar.scrollHeight;
}
});
const expandedLayout = await page.evaluate(() => {
const sidebar = document.getElementById("sidebar");
const app = document.getElementById("app");
if (!sidebar || !app) {
throw new Error("Missing sidebar layout nodes");
}
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));
}
.sidebar-width-probe--expanded {
width: calc(var(--sidebar-width-expanded) + var(--sidebar-content-gap));
}
`;
document.head.appendChild(style);
};
ensureProbeStyles();
const computeMargin = (widthVar) => {
const variant =
widthVar === "--sidebar-width-collapsed"
? "sidebar-width-probe--collapsed"
: widthVar === "--sidebar-width-expanded"
? "sidebar-width-probe--expanded"
: null;
if (!variant) {
throw new Error(`Unsupported sidebar width variable: ${widthVar}`);
}
const probe = document.createElement("div");
probe.classList.add("sidebar-width-probe", variant);
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,
expectedMargin: computeMargin("--sidebar-width-expanded"),
};
});
expect(expandedLayout.state).toBe("expanded");
expect(Math.abs(expandedLayout.marginLeft - expandedLayout.expectedMargin)).toBeLessThan(0.5);
const footerButton = page.locator("#footerDropdownButton");
await expect(footerButton).toBeVisible();
await footerButton.click();
await expect(footerButton).toHaveAttribute("aria-expanded", "true");
const dropupState = await page.evaluate(() => {
const footerLinks = document.getElementById("footerLinksContainer");
if (!footerLinks) {
throw new Error("Missing footer links container");
}
return {
state: footerLinks.getAttribute("data-state"),
ariaHidden: footerLinks.getAttribute("aria-hidden"),
};
});
expect(dropupState.state).toBe("expanded");
expect(dropupState.ariaHidden).toBe("false");
});
});
export {};
+10 -4
View File
@@ -5,18 +5,24 @@ const PORT = Number.parseInt(process.env.PLAYWRIGHT_PORT ?? "4173", 10);
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://${HOST}:${PORT}`;
export default defineConfig({
testDir: ".",
testDir: "../../",
outputDir: "../../artifacts/test-results-repro",
timeout: 60_000,
reporter: [["list"], ["html", { open: "never" }]],
reporter: [["list"]],
use: {
baseURL: BASE_URL,
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
},
projects: [
{
name: "repro",
testMatch: "examples/reproducers/**/*.spec.ts",
},
],
webServer: {
// Config is in examples/reproducers, so cwd is there. We need to serve the repo root.
command: `npx http-server ../.. -p ${PORT} -a ${HOST} -c-1 --silent`,
command: `npx http-server . -p ${PORT} -a ${HOST} -c-1 --silent`,
cwd: "../../",
url: `${BASE_URL}/docs/kitchen-sink.html`,
reuseExistingServer: !process.env.CI,
timeout: 120_000,