mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-09 14:22:53 +00:00
improved playright integration
This commit is contained in:
1 parent
20314a5b5c
commit
0cf1dc0f55
17 files changed
+1165
-57
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import { test as base, expect, chromium, type BrowserContext, type Page } from "@playwright/test";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
type PersistentExtensionFixtures = {
|
||||
persistentContext: BrowserContext;
|
||||
extensionPath: string;
|
||||
extensionUserDataDir: string;
|
||||
};
|
||||
|
||||
export const test = base.extend<PersistentExtensionFixtures>({
|
||||
extensionPath: [
|
||||
async ({}, use) => {
|
||||
const rawPath = process.env.PLAYWRIGHT_EXTENSION_PATH || "";
|
||||
const resolved = rawPath ? path.resolve(rawPath) : "";
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
"PLAYWRIGHT_EXTENSION_PATH is required for extension E2E tests.",
|
||||
);
|
||||
}
|
||||
await use(resolved);
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
extensionUserDataDir: [
|
||||
async ({}, use, testInfo) => {
|
||||
const provided = process.env.PLAYWRIGHT_EXTENSION_USER_DATA_DIR || "";
|
||||
if (provided.trim()) {
|
||||
await use(path.resolve(provided.trim()));
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = await fs.mkdtemp(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
`bitvid-playwright-extension-${testInfo.workerIndex}-`,
|
||||
),
|
||||
);
|
||||
try {
|
||||
await use(dir);
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
persistentContext: [
|
||||
async ({ extensionPath, extensionUserDataDir }, use) => {
|
||||
const context = await chromium.launchPersistentContext(
|
||||
extensionUserDataDir,
|
||||
{
|
||||
headless: false,
|
||||
viewport: { width: 1280, height: 720 },
|
||||
args: [
|
||||
`--disable-extensions-except=${extensionPath}`,
|
||||
`--load-extension=${extensionPath}`,
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await use(context);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
context: async ({ persistentContext }, use) => {
|
||||
await use(persistentContext);
|
||||
},
|
||||
|
||||
page: async ({ persistentContext, baseURL }, use) => {
|
||||
const existing = persistentContext.pages()[0] || null;
|
||||
const page = existing || (await persistentContext.newPage());
|
||||
if (baseURL) {
|
||||
await page.goto(baseURL);
|
||||
}
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test, expect } from "./helpers/persistentExtensionFixture";
|
||||
|
||||
test.describe("NIP-07 extension smoke", () => {
|
||||
test("extension injects a NIP-07 API surface", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (window as any).nostr === "object" &&
|
||||
(typeof (window as any).nostr.getPublicKey === "function" ||
|
||||
typeof (window as any).nostr.nip04?.decrypt === "function" ||
|
||||
typeof (window as any).nostr.nip44?.decrypt === "function"),
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
const capabilities = await page.evaluate(() => {
|
||||
const api = (window as any).nostr;
|
||||
return {
|
||||
hasGetPublicKey: typeof api?.getPublicKey === "function",
|
||||
hasSignEvent: typeof api?.signEvent === "function",
|
||||
hasNip04Decrypt: typeof api?.nip04?.decrypt === "function",
|
||||
hasNip44Decrypt:
|
||||
typeof api?.nip44?.decrypt === "function" ||
|
||||
typeof api?.nip44?.v2?.decrypt === "function",
|
||||
};
|
||||
});
|
||||
|
||||
expect(
|
||||
capabilities.hasGetPublicKey ||
|
||||
capabilities.hasNip04Decrypt ||
|
||||
capabilities.hasNip44Decrypt,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ test.describe("Agent testability infrastructure", () => {
|
||||
});
|
||||
|
||||
expect(harness).not.toBeNull();
|
||||
expect(harness!.version).toBe(1);
|
||||
expect(harness!.version).toBe(2);
|
||||
});
|
||||
|
||||
test("relay overrides are applied via query params", async ({
|
||||
@@ -49,6 +49,29 @@ test.describe("Agent testability infrastructure", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("setTestRelays aligns relayManager and nostrClient", async ({
|
||||
page,
|
||||
gotoApp,
|
||||
relayUrl,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
const result = await page.evaluate((url) => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (!harness || typeof harness.setTestRelays !== "function") {
|
||||
throw new Error("setTestRelays missing");
|
||||
}
|
||||
return harness.setTestRelays([url]);
|
||||
}, relayUrl);
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
const state = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
expect(state.relays.read).toEqual([relayUrl]);
|
||||
expect(state.relayManager.read).toEqual([relayUrl]);
|
||||
});
|
||||
|
||||
test("programmatic login works via test harness", async ({
|
||||
page,
|
||||
relay,
|
||||
@@ -133,10 +156,15 @@ test.describe("Data-testid selectors are present", () => {
|
||||
// Open the login modal
|
||||
await page.locator('[data-testid="login-button"]').click();
|
||||
|
||||
// Wait for modal to be visible
|
||||
await expect(page.locator('[data-testid="login-modal"]')).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
// Wait for modal to transition into an open, visible state.
|
||||
await page.waitForFunction(() => {
|
||||
const modal = document.querySelector('[data-testid="login-modal"]');
|
||||
if (!(modal instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
const openAttr = modal.getAttribute("data-open");
|
||||
return openAttr === "true" && !modal.classList.contains("hidden");
|
||||
}, { timeout: 15000 });
|
||||
|
||||
// Check provider buttons exist
|
||||
const providerButtons = page.locator(
|
||||
@@ -186,4 +214,32 @@ test.describe("App state inspection", () => {
|
||||
});
|
||||
expect(state.isLoggedIn).toBe(false);
|
||||
});
|
||||
|
||||
test("list sync event capture APIs are available", async ({
|
||||
page,
|
||||
gotoApp,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (!harness) {
|
||||
throw new Error("Harness missing");
|
||||
}
|
||||
harness.clearListSyncEvents();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bitvid:auth-loading-state", {
|
||||
detail: { lists: "loading", listsDetail: { ready: false } },
|
||||
}),
|
||||
);
|
||||
const events = harness.getListSyncEvents();
|
||||
return {
|
||||
count: events.length,
|
||||
firstSource: events[0]?.source || "",
|
||||
};
|
||||
});
|
||||
|
||||
expect(result.count).toBeGreaterThan(0);
|
||||
expect(result.firstSource).toBe("auth-loading-state");
|
||||
});
|
||||
});
|
||||
@@ -149,6 +149,22 @@ type BitvidFixtures = {
|
||||
clearRelay: () => Promise<void>;
|
||||
gotoApp: (path?: string) => Promise<void>;
|
||||
loginAs: (page: Page) => Promise<string>;
|
||||
setTestRelays: (page: Page, relays: string[]) => Promise<any>;
|
||||
setDecryptBehavior: (
|
||||
page: Page,
|
||||
mode: "passthrough" | "timeout" | "error" | "delay",
|
||||
options?: { delayMs?: number; errorMessage?: string },
|
||||
) => Promise<any>;
|
||||
startDiagnostics: (
|
||||
page: Page,
|
||||
options?: { captureSyncEvents?: boolean },
|
||||
) => Promise<{
|
||||
stop: () => Promise<{
|
||||
console: Array<{ type: string; text: string }>;
|
||||
pageErrors: string[];
|
||||
syncEvents: any[];
|
||||
}>;
|
||||
}>;
|
||||
testPubkey: string;
|
||||
testPrivateKey: string;
|
||||
relayUrl: string;
|
||||
@@ -207,6 +223,92 @@ export const test = base.extend<BitvidFixtures>({
|
||||
});
|
||||
},
|
||||
|
||||
setTestRelays: async ({}, use) => {
|
||||
await use(async (page: Page, relays: string[]) => {
|
||||
return page.evaluate((urls) => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (!harness || typeof harness.setTestRelays !== "function") {
|
||||
throw new Error("setTestRelays is not available in test harness");
|
||||
}
|
||||
return harness.setTestRelays(urls);
|
||||
}, relays);
|
||||
});
|
||||
},
|
||||
|
||||
setDecryptBehavior: async ({}, use) => {
|
||||
await use(
|
||||
async (
|
||||
page: Page,
|
||||
mode: "passthrough" | "timeout" | "error" | "delay",
|
||||
options = {},
|
||||
) => {
|
||||
return page.evaluate(
|
||||
({ nextMode, nextOptions }) => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (!harness || typeof harness.setSignerDecryptBehavior !== "function") {
|
||||
throw new Error(
|
||||
"setSignerDecryptBehavior is not available in test harness",
|
||||
);
|
||||
}
|
||||
return harness.setSignerDecryptBehavior(nextMode, nextOptions);
|
||||
},
|
||||
{ nextMode: mode, nextOptions: options },
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
startDiagnostics: async ({}, use) => {
|
||||
await use(async (page: Page, options = {}) => {
|
||||
const consoleEntries: Array<{ type: string; text: string }> = [];
|
||||
const pageErrors: string[] = [];
|
||||
const captureSyncEvents = options.captureSyncEvents !== false;
|
||||
|
||||
const onConsole = (message: any) => {
|
||||
consoleEntries.push({
|
||||
type: message.type(),
|
||||
text: message.text(),
|
||||
});
|
||||
};
|
||||
const onPageError = (error: Error) => {
|
||||
pageErrors.push(error?.message || String(error));
|
||||
};
|
||||
|
||||
page.on("console", onConsole);
|
||||
page.on("pageerror", onPageError);
|
||||
|
||||
if (captureSyncEvents) {
|
||||
await page.evaluate(() => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (harness && typeof harness.clearListSyncEvents === "function") {
|
||||
harness.clearListSyncEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
stop: async () => {
|
||||
page.off("console", onConsole);
|
||||
page.off("pageerror", onPageError);
|
||||
const syncEvents = captureSyncEvents
|
||||
? await page.evaluate(() => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (harness && typeof harness.getListSyncEvents === "function") {
|
||||
return harness.getListSyncEvents();
|
||||
}
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
console: consoleEntries,
|
||||
pageErrors,
|
||||
syncEvents,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
testPubkey: TEST_PK,
|
||||
testPrivateKey: TEST_PRIVATE_KEY_HEX,
|
||||
relayUrl: async ({ relay }, use) => {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { test, expect } from "./helpers/bitvidTestFixture";
|
||||
import { finalizeEvent, getPublicKey } from "nostr-tools";
|
||||
|
||||
function makeHex(char: string) {
|
||||
return char.repeat(64);
|
||||
}
|
||||
|
||||
test.describe("Profile list sync diagnostics", () => {
|
||||
test("captures console + page errors during login/profile sync baseline", async ({
|
||||
page,
|
||||
gotoApp,
|
||||
loginAs,
|
||||
testPubkey,
|
||||
relayUrl,
|
||||
setTestRelays,
|
||||
startDiagnostics,
|
||||
}) => {
|
||||
const diagnostics = await startDiagnostics(page);
|
||||
|
||||
await gotoApp();
|
||||
await loginAs(page);
|
||||
await setTestRelays(page, [relayUrl]);
|
||||
|
||||
await page.waitForFunction(
|
||||
() => Boolean((window as any).__bitvidTest__?.getAppState?.()?.isLoggedIn),
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
const appState = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
expect(appState.isLoggedIn).toBe(true);
|
||||
|
||||
const syncState = await page.evaluate(async (activePubkey) => {
|
||||
const module = await import("/js/userBlocks.js");
|
||||
const manager = module.userBlocks;
|
||||
const statuses: any[] = [];
|
||||
const unsubscribe = manager.on("status", (detail: any) => {
|
||||
statuses.push(detail);
|
||||
});
|
||||
try {
|
||||
await manager.loadBlocks(activePubkey, {
|
||||
allowPermissionPrompt: false,
|
||||
decryptTimeoutMs: 250,
|
||||
});
|
||||
} finally {
|
||||
if (typeof unsubscribe === "function") unsubscribe();
|
||||
}
|
||||
return {
|
||||
statuses,
|
||||
blockedPubkeys: manager.getBlockedPubkeys(),
|
||||
};
|
||||
}, testPubkey);
|
||||
|
||||
const diagnosticsResult = await diagnostics.stop();
|
||||
const appErrorLogs = diagnosticsResult.console.filter(
|
||||
(entry) =>
|
||||
entry.type === "error" ||
|
||||
entry.text.includes("[UserBlockList]") ||
|
||||
entry.text.includes("profile list") ||
|
||||
entry.text.includes("blocked creators"),
|
||||
);
|
||||
|
||||
expect(Array.isArray(syncState.statuses)).toBe(true);
|
||||
expect(Array.isArray(syncState.blockedPubkeys)).toBe(true);
|
||||
|
||||
// Diagnostic output is intentionally logged for local triage.
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
phase: "baseline",
|
||||
appState,
|
||||
consoleCount: diagnosticsResult.console.length,
|
||||
pageErrorCount: diagnosticsResult.pageErrors.length,
|
||||
appErrorLogs,
|
||||
syncEventsTail: diagnosticsResult.syncEvents.slice(-8),
|
||||
statusTail: syncState.statuses.slice(-8),
|
||||
blockedPubkeys: syncState.blockedPubkeys,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("forced decrypt-timeout still ingests public mute tags", async ({
|
||||
page,
|
||||
gotoApp,
|
||||
loginAs,
|
||||
seedRawEvent,
|
||||
testPubkey,
|
||||
testPrivateKey,
|
||||
relayUrl,
|
||||
setTestRelays,
|
||||
setDecryptBehavior,
|
||||
startDiagnostics,
|
||||
}) => {
|
||||
const publicBlocked = makeHex("a");
|
||||
const privateKeyBytes = Uint8Array.from(Buffer.from(testPrivateKey, "hex"));
|
||||
const derivedPubkey = getPublicKey(privateKeyBytes);
|
||||
const staleEvent = finalizeEvent(
|
||||
{
|
||||
kind: 10000,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["encrypted", "nip44"],
|
||||
["p", publicBlocked],
|
||||
],
|
||||
content: "ciphertext-timeout",
|
||||
},
|
||||
privateKeyBytes,
|
||||
);
|
||||
await seedRawEvent(staleEvent);
|
||||
|
||||
const diagnostics = await startDiagnostics(page);
|
||||
|
||||
await gotoApp();
|
||||
await loginAs(page);
|
||||
await setTestRelays(page, [relayUrl]);
|
||||
await page.waitForFunction(
|
||||
() => Boolean((window as any).__bitvidTest__?.getAppState?.()?.isLoggedIn),
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
let result: any = null;
|
||||
try {
|
||||
await setDecryptBehavior(page, "timeout");
|
||||
result = await page.evaluate(
|
||||
async ({ activePubkey, blockedPubkey }) => {
|
||||
const blocksModule = await import("/js/userBlocks.js");
|
||||
const manager = blocksModule.userBlocks;
|
||||
|
||||
const statuses: any[] = [];
|
||||
const unsubscribe = manager.on("status", (detail: any) => {
|
||||
statuses.push(detail);
|
||||
});
|
||||
|
||||
try {
|
||||
await manager.loadBlocks(activePubkey, {
|
||||
allowPermissionPrompt: false,
|
||||
decryptTimeoutMs: 80,
|
||||
});
|
||||
} finally {
|
||||
if (typeof unsubscribe === "function") unsubscribe();
|
||||
}
|
||||
|
||||
return {
|
||||
statuses,
|
||||
blockedPubkeys: manager.getBlockedPubkeys(),
|
||||
containsPublicMute: manager.getBlockedPubkeys().includes(blockedPubkey),
|
||||
};
|
||||
},
|
||||
{ activePubkey: testPubkey, blockedPubkey: publicBlocked },
|
||||
);
|
||||
|
||||
expect(testPubkey).toBe(derivedPubkey);
|
||||
expect(result.containsPublicMute).toBe(true);
|
||||
expect(
|
||||
result.statuses.some(
|
||||
(entry: any) =>
|
||||
entry?.status === "stale" && entry?.reason === "decrypt-timeout",
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await setDecryptBehavior(page, "passthrough");
|
||||
}
|
||||
|
||||
const diagnosticsResult = await diagnostics.stop();
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
phase: "forced-timeout",
|
||||
consoleCount: diagnosticsResult.console.length,
|
||||
pageErrorCount: diagnosticsResult.pageErrors.length,
|
||||
syncEventsTail: diagnosticsResult.syncEvents.slice(-10),
|
||||
statusTail: result?.statuses?.slice?.(-10) || [],
|
||||
blockedPubkeys: result?.blockedPubkeys || [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1469,3 +1469,110 @@ await (async () => {
|
||||
window.nostr = originalNostr;
|
||||
}
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
// Test: decrypt timeout should still apply public p-tags while preserving
|
||||
// existing private blocks.
|
||||
const actor = "6".repeat(64);
|
||||
const existingPrivateBlocked = "7".repeat(64);
|
||||
const publicBlocked = "8".repeat(64);
|
||||
|
||||
const UserBlockListManager = userBlocks.constructor;
|
||||
const manager = new UserBlockListManager();
|
||||
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
: nostrClient.relays;
|
||||
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
||||
? [...nostrClient.writeRelays]
|
||||
: nostrClient.writeRelays;
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalSigner = getActiveSigner();
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
|
||||
const relayUrls = ["wss://decrypt-timeout-partial.example"];
|
||||
relayManager.setEntries(
|
||||
relayUrls.map((url) => ({ url, mode: "both" })),
|
||||
{ allowEmpty: false, updateClient: false },
|
||||
);
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.writeRelays = relayUrls;
|
||||
nostrClient.pool = {
|
||||
list: async () => [
|
||||
{
|
||||
id: "event-timeout-partial",
|
||||
kind: 10000,
|
||||
created_at: 21_000,
|
||||
pubkey: actor,
|
||||
content: "ciphertext-timeout",
|
||||
tags: [
|
||||
["encrypted", "nip44"],
|
||||
["p", publicBlocked],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
setActiveSigner({
|
||||
getPublicKey: async () => actor,
|
||||
nip44Decrypt: async () => new Promise(() => {}),
|
||||
});
|
||||
|
||||
manager._privateBlocks = new Set([existingPrivateBlocked]);
|
||||
manager._publicMutes = new Set();
|
||||
manager.blockedPubkeys = new Set([existingPrivateBlocked]);
|
||||
manager.blockEventId = "event-previous";
|
||||
manager.blockEventCreatedAt = 20_000;
|
||||
manager.loaded = true;
|
||||
|
||||
const statusEvents = [];
|
||||
const unsubscribe = manager.on(USER_BLOCK_EVENTS.STATUS, (detail) => {
|
||||
statusEvents.push(detail);
|
||||
});
|
||||
|
||||
try {
|
||||
await manager.loadBlocks(actor, {
|
||||
allowPermissionPrompt: false,
|
||||
decryptTimeoutMs: 30,
|
||||
});
|
||||
|
||||
const blocked = manager.getBlockedPubkeys();
|
||||
assert.equal(
|
||||
blocked.includes(existingPrivateBlocked),
|
||||
true,
|
||||
"timeout fallback should preserve previously decrypted private blocks",
|
||||
);
|
||||
assert.equal(
|
||||
blocked.includes(publicBlocked),
|
||||
true,
|
||||
"timeout fallback should still ingest public mute p-tags",
|
||||
);
|
||||
assert.equal(
|
||||
manager.blockEventId,
|
||||
"event-timeout-partial",
|
||||
"timeout fallback should keep metadata anchored to the newest relay event",
|
||||
);
|
||||
assert(
|
||||
statusEvents.some(
|
||||
(detail) =>
|
||||
detail?.status === "stale" && detail?.reason === "decrypt-timeout",
|
||||
),
|
||||
"timeout fallback should emit stale decrypt-timeout status",
|
||||
);
|
||||
} finally {
|
||||
unsubscribe?.();
|
||||
relayManager.setEntries(
|
||||
originalRelayEntries,
|
||||
{ allowEmpty: false, updateClient: false },
|
||||
);
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.writeRelays = originalWriteRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
clearActiveSigner();
|
||||
if (originalSigner) {
|
||||
setActiveSigner(originalSigner);
|
||||
}
|
||||
manager.reset();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user