mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
Merge pull request #2297 from PR0M3TH3AN/claude/improve-playwright-testability-8mNO7
Add Playwright agent testing infrastructure with test harness and mock relay
This commit is contained in:
15 files changed
+972
-29
No files matched your search
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Agent testability E2E tests.
|
||||
*
|
||||
* These tests demonstrate that bitvid is fully testable by Playwright CLI agents.
|
||||
* They exercise:
|
||||
* 1. Test harness installation and relay override
|
||||
* 2. Programmatic login (bypassing the UI)
|
||||
* 3. Seeding test data into a local mock relay
|
||||
* 4. Inspecting app state through the harness API
|
||||
* 5. Key data-testid selectors for UI interaction
|
||||
*/
|
||||
|
||||
import { test, expect } from "./helpers/bitvidTestFixture";
|
||||
|
||||
test.describe("Agent testability infrastructure", () => {
|
||||
test("test harness installs when __test__=1 is set", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
const harness = await page.evaluate(() => {
|
||||
const h = (window as any).__bitvidTest__;
|
||||
return h ? { version: h.version } : null;
|
||||
});
|
||||
|
||||
expect(harness).not.toBeNull();
|
||||
expect(harness!.version).toBe(1);
|
||||
});
|
||||
|
||||
test("relay overrides are applied via query params", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
relayUrl,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
const state = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
|
||||
// Relay overrides should include our test relay
|
||||
expect(state.relays).not.toBeNull();
|
||||
if (state.relays) {
|
||||
const allRelays = state.relays.all || [];
|
||||
expect(allRelays).toContain(relayUrl);
|
||||
}
|
||||
});
|
||||
|
||||
test("programmatic login works via test harness", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
loginAs,
|
||||
testPubkey,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
const pubkey = await loginAs(page);
|
||||
expect(pubkey).toBe(testPubkey);
|
||||
|
||||
const state = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
|
||||
expect(state.isLoggedIn).toBe(true);
|
||||
expect(state.activePubkey).toBe(testPubkey);
|
||||
});
|
||||
|
||||
test("can seed events into mock relay and retrieve them", async ({
|
||||
relay,
|
||||
seedEvent,
|
||||
clearRelay,
|
||||
}) => {
|
||||
await clearRelay();
|
||||
|
||||
await seedEvent({
|
||||
title: "Test Video Alpha",
|
||||
url: "https://example.com/alpha.mp4",
|
||||
dTag: "alpha-001",
|
||||
});
|
||||
|
||||
await seedEvent({
|
||||
title: "Test Video Beta",
|
||||
url: "https://example.com/beta.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:0000000000000000000000000000000000000000",
|
||||
dTag: "beta-001",
|
||||
});
|
||||
|
||||
// Verify via HTTP API
|
||||
const resp = await fetch("http://127.0.0.1:8878/events");
|
||||
const events = await resp.json();
|
||||
expect(events.length).toBe(2);
|
||||
});
|
||||
|
||||
test("relay health endpoint works", async ({ relay }) => {
|
||||
const resp = await fetch("http://127.0.0.1:8878/health");
|
||||
const health = await resp.json();
|
||||
expect(health.ok).toBe(true);
|
||||
expect(typeof health.eventCount).toBe("number");
|
||||
expect(typeof health.connectionCount).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Data-testid selectors are present", () => {
|
||||
test("critical UI elements have data-testid attributes", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
// Header controls
|
||||
await expect(page.locator('[data-testid="login-button"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="search-input"]')).toBeVisible();
|
||||
|
||||
// Upload button is hidden until logged in — verify it exists in DOM
|
||||
const uploadBtn = page.locator('[data-testid="upload-button"]');
|
||||
await expect(uploadBtn).toBeAttached();
|
||||
});
|
||||
|
||||
test("login modal has testid selectors", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
// 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: 5000,
|
||||
});
|
||||
|
||||
// Check provider buttons exist
|
||||
const providerButtons = page.locator(
|
||||
'[data-testid="login-provider-button"]',
|
||||
);
|
||||
// At least one provider button should be present
|
||||
await expect(providerButtons.first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("App state inspection", () => {
|
||||
test("getAppState reports logged-out state by default", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
|
||||
const state = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
|
||||
expect(state.isLoggedIn).toBe(false);
|
||||
expect(state.activePubkey).toBeNull();
|
||||
});
|
||||
|
||||
test("logout clears active signer", async ({
|
||||
page,
|
||||
relay,
|
||||
gotoApp,
|
||||
loginAs,
|
||||
}) => {
|
||||
await gotoApp();
|
||||
await loginAs(page);
|
||||
|
||||
let state = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
expect(state.isLoggedIn).toBe(true);
|
||||
|
||||
await page.evaluate(() => {
|
||||
(window as any).__bitvidTest__.logout();
|
||||
});
|
||||
|
||||
state = await page.evaluate(() => {
|
||||
return (window as any).__bitvidTest__.getAppState();
|
||||
});
|
||||
expect(state.isLoggedIn).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Playwright test fixture for bitvid agent testing.
|
||||
*
|
||||
* Provides:
|
||||
* - Local mock relay with HTTP seeding API
|
||||
* - Programmatic login via the test harness
|
||||
* - Test data seeding helpers
|
||||
* - App state inspection utilities
|
||||
*
|
||||
* Usage:
|
||||
* import { test, expect } from './helpers/bitvidTestFixture';
|
||||
*
|
||||
* test('can publish a video', async ({ page, relay, seedEvent, loginAs }) => {
|
||||
* await loginAs(page);
|
||||
* // ... interact with the app
|
||||
* });
|
||||
*/
|
||||
|
||||
import { test as base, expect, type Page } from "@playwright/test";
|
||||
import { startRelay } from "../../../scripts/agent/simple-relay.mjs";
|
||||
import { finalizeEvent, getPublicKey } from "nostr-tools";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
// Polyfill WebSocket for node environment
|
||||
if (!global.WebSocket) {
|
||||
(global as any).WebSocket = WebSocket;
|
||||
}
|
||||
|
||||
// Fixed test key for deterministic testing.
|
||||
// This is a throwaway key used only in test environments.
|
||||
const TEST_PRIVATE_KEY_HEX =
|
||||
"7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b";
|
||||
const TEST_SK = Uint8Array.from(Buffer.from(TEST_PRIVATE_KEY_HEX, "hex"));
|
||||
const TEST_PK = getPublicKey(TEST_SK);
|
||||
|
||||
const RELAY_PORT = 8877;
|
||||
const RELAY_HTTP_PORT = 8878;
|
||||
const RELAY_WS_URL = `ws://127.0.0.1:${RELAY_PORT}`;
|
||||
const RELAY_HTTP_URL = `http://127.0.0.1:${RELAY_HTTP_PORT}`;
|
||||
|
||||
export interface TestVideoEvent {
|
||||
title: string;
|
||||
url?: string;
|
||||
magnet?: string;
|
||||
description?: string;
|
||||
dTag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signed Nostr video event (kind 30078) for test seeding.
|
||||
*/
|
||||
function createVideoEvent(video: TestVideoEvent) {
|
||||
const dTag = video.dTag || `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const contentObj: Record<string, unknown> = {
|
||||
version: 3,
|
||||
title: video.title,
|
||||
videoRootId: dTag,
|
||||
mode: "dev",
|
||||
isPrivate: false,
|
||||
deleted: false,
|
||||
};
|
||||
if (video.url) contentObj.url = video.url;
|
||||
if (video.magnet) contentObj.magnet = video.magnet;
|
||||
if (video.description) contentObj.description = video.description;
|
||||
|
||||
const tags: string[][] = [
|
||||
["d", dTag],
|
||||
["title", video.title],
|
||||
];
|
||||
if (video.url) tags.push(["url", video.url]);
|
||||
|
||||
return finalizeEvent(
|
||||
{
|
||||
kind: 30078,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: JSON.stringify(contentObj),
|
||||
pubkey: TEST_PK,
|
||||
},
|
||||
TEST_SK,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed an event into the relay via HTTP API.
|
||||
*/
|
||||
async function seedEventViaHttp(event: any) {
|
||||
const resp = await fetch(`${RELAY_HTTP_URL}/seed`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(event),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to seed event: ${resp.status} ${await resp.text()}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all events from the relay.
|
||||
*/
|
||||
async function clearRelayEvents() {
|
||||
const resp = await fetch(`${RELAY_HTTP_URL}/events`, { method: "DELETE" });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to clear events: ${resp.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to bitvid with test mode and relay overrides enabled.
|
||||
*/
|
||||
async function gotoWithTestMode(page: Page, path = "/") {
|
||||
const separator = path.includes("?") ? "&" : "?";
|
||||
const testUrl = `${path}${separator}__test__=1&__testRelays__=${encodeURIComponent(RELAY_WS_URL)}`;
|
||||
|
||||
// Set up localStorage before navigation
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("hasSeenDisclaimer", "true");
|
||||
localStorage.setItem("__bitvidTestMode__", "1");
|
||||
});
|
||||
|
||||
await page.goto(testUrl);
|
||||
|
||||
// Wait for the test harness to be available
|
||||
await page.waitForFunction(
|
||||
() => typeof (window as any).__bitvidTest__ === "object",
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login programmatically via the test harness.
|
||||
*/
|
||||
async function loginWithTestKey(page: Page) {
|
||||
const pubkey = await page.evaluate(async (hexKey) => {
|
||||
const harness = (window as any).__bitvidTest__;
|
||||
if (!harness) throw new Error("Test harness not installed");
|
||||
return harness.loginWithNsec(hexKey);
|
||||
}, TEST_PRIVATE_KEY_HEX);
|
||||
return pubkey;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Fixture type definitions
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
type BitvidFixtures = {
|
||||
relay: Awaited<ReturnType<typeof startRelay>>;
|
||||
seedEvent: (video: TestVideoEvent) => Promise<any>;
|
||||
seedRawEvent: (event: any) => Promise<any>;
|
||||
clearRelay: () => Promise<void>;
|
||||
gotoApp: (path?: string) => Promise<void>;
|
||||
loginAs: (page: Page) => Promise<string>;
|
||||
testPubkey: string;
|
||||
testPrivateKey: string;
|
||||
relayUrl: string;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Shared relay fixture (one relay per worker)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
export const test = base.extend<BitvidFixtures>({
|
||||
relay: [
|
||||
async ({}, use) => {
|
||||
const relayInstance = startRelay(RELAY_PORT, { httpPort: RELAY_HTTP_PORT });
|
||||
await use(relayInstance);
|
||||
await relayInstance.close();
|
||||
},
|
||||
{ scope: "test" },
|
||||
],
|
||||
|
||||
seedEvent: async ({ relay }, use) => {
|
||||
await use(async (video: TestVideoEvent) => {
|
||||
const event = createVideoEvent(video);
|
||||
return seedEventViaHttp(event);
|
||||
});
|
||||
},
|
||||
|
||||
seedRawEvent: async ({ relay }, use) => {
|
||||
await use(async (event: any) => {
|
||||
return seedEventViaHttp(event);
|
||||
});
|
||||
},
|
||||
|
||||
clearRelay: async ({ relay }, use) => {
|
||||
await use(async () => {
|
||||
await clearRelayEvents();
|
||||
});
|
||||
},
|
||||
|
||||
gotoApp: async ({ page }, use) => {
|
||||
await use(async (path = "/") => {
|
||||
await gotoWithTestMode(page, path);
|
||||
});
|
||||
},
|
||||
|
||||
loginAs: async ({}, use) => {
|
||||
await use(async (page: Page) => {
|
||||
return loginWithTestKey(page);
|
||||
});
|
||||
},
|
||||
|
||||
testPubkey: TEST_PK,
|
||||
testPrivateKey: TEST_PRIVATE_KEY_HEX,
|
||||
relayUrl: RELAY_WS_URL,
|
||||
});
|
||||
|
||||
export { expect };
|
||||
Reference in new issue
Block a user