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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5e40221a2f
commit
a7520be0e7
@@ -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()
|
||||
}
|
||||
@@ -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<void> {
|
||||
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('')
|
||||
}
|
||||
@@ -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: '..',
|
||||
},
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user