From a7520be0e7a96c4929b10c12366c2ad4521c3255 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 05:14:15 +0000 Subject: [PATCH] test: add Playwright E2E infrastructure with smoke test Set up Playwright with Chromium, dev server auto-start, test helpers for seeding bots and programmatic auth. Added smoke spec that verifies homepage and leaderboard load without crashes. Co-Authored-By: Claude Opus 4.6 --- e2e/helpers/auth.ts | 37 ++++++++++++++++++++++++++++++++ e2e/helpers/setup.ts | 46 ++++++++++++++++++++++++++++++++++++++++ e2e/playwright.config.ts | 31 +++++++++++++++++++++++++++ e2e/smoke.spec.ts | 19 +++++++++++++++++ package.json | 4 +++- pnpm-lock.yaml | 38 +++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 e2e/helpers/auth.ts create mode 100644 e2e/helpers/setup.ts create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/smoke.spec.ts diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts new file mode 100644 index 0000000..2d214ed --- /dev/null +++ b/e2e/helpers/auth.ts @@ -0,0 +1,37 @@ +/** + * E2E authentication helpers. + * Provides programmatic login for tests without browser extension interaction. + */ + +import { randomPubkey } from './setup.js' + +/** + * Create a test identity (pubkey + nsec equivalent). + * For E2E tests, we use direct pubkey-based login (legacy endpoint) + * since we can't interact with NIP-07 browser extensions. + */ +export function createTestIdentity() { + return { + pubkey: randomPubkey(), + // In a real NIP-98 flow, this would be a signed event + // For testing, we use the legacy login endpoint + } +} + +/** + * Login via legacy endpoint and get bot data. + * Returns bot info if the pubkey has a registered bot. + */ +export async function loginWithPubkey(baseURL: string, pubkey: string): Promise<{ bot?: { id: string; name: string } }> { + const res = await fetch(`${baseURL}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey }), + }) + + if (!res.ok) { + return {} + } + + return res.json() +} diff --git a/e2e/helpers/setup.ts b/e2e/helpers/setup.ts new file mode 100644 index 0000000..61545dd --- /dev/null +++ b/e2e/helpers/setup.ts @@ -0,0 +1,46 @@ +/** + * E2E test setup helpers. + * Provides utilities for seeding test data and managing test state. + */ + +/** Wait for the dev server to be ready */ +export async function waitForServer(baseURL: string, timeoutMs = 10_000): Promise { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + try { + const res = await fetch(baseURL) + if (res.ok) return + } catch { + // Server not ready yet + } + await new Promise(r => setTimeout(r, 500)) + } + throw new Error(`Server at ${baseURL} did not start within ${timeoutMs}ms`) +} + +/** Seed a mock bot via the API for testing */ +export async function seedBot(baseURL: string, name: string, pubkey: string): Promise<{ id: string; secret: string }> { + const res = await fetch(`${baseURL}/api/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + pubkey, + name, + webhookUrl: 'http://mock.local', + }), + }) + + if (!res.ok) { + const body = await res.text() + throw new Error(`Failed to seed bot ${name}: ${res.status} ${body}`) + } + + return res.json() +} + +/** Generate a random hex pubkey for testing */ +export function randomPubkey(): string { + const bytes = new Uint8Array(32) + crypto.getRandomValues(bytes) + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..2121be7 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: '.', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + timeout: 30_000, + + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'pnpm dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 30_000, + cwd: '..', + }, +}) diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 0000000..5eb8487 --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test' + +test('homepage loads', async ({ page }) => { + await page.goto('/') + // Page should load without errors + await expect(page).toHaveTitle(/botfights/i) +}) + +test('leaderboard page loads', async ({ page }) => { + await page.goto('/leaderboard') + // Should render without console errors + const errors: string[] = [] + page.on('console', msg => { + if (msg.type() === 'error') errors.push(msg.text()) + }) + await page.waitForTimeout(1000) + // Allow some errors (e.g., missing API data) but no crashes + expect(errors.filter(e => e.includes('TypeError') || e.includes('ReferenceError'))).toHaveLength(0) +}) diff --git a/package.json b/package.json index 5fad5e5..dcfc5cf 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "lint": "eslint .", "typecheck": "vue-tsc --noEmit -p frontend/tsconfig.json && tsc --noEmit -p server/tsconfig.json", "clean": "rm -rf frontend/dist server/dist", - "seed": "pnpm --filter server seed" + "seed": "pnpm --filter server seed", + "test:e2e": "playwright test --config e2e/playwright.config.ts" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "concurrently": "^9.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 084b517..1fe0c9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: devDependencies: + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@typescript-eslint/eslint-plugin': specifier: ^8.56.1 version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) @@ -1131,6 +1134,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -2277,6 +2285,11 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2962,6 +2975,16 @@ packages: platform@1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4729,6 +4752,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -5898,6 +5925,9 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6552,6 +6582,14 @@ snapshots: platform@1.3.6: {} + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.5.8: