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)
|
||||||
|
})
|
||||||
+3
-1
@@ -10,9 +10,11 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "vue-tsc --noEmit -p frontend/tsconfig.json && tsc --noEmit -p server/tsconfig.json",
|
"typecheck": "vue-tsc --noEmit -p frontend/tsconfig.json && tsc --noEmit -p server/tsconfig.json",
|
||||||
"clean": "rm -rf frontend/dist server/dist",
|
"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": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
"@typescript-eslint/parser": "^8.56.1",
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
|
|||||||
Generated
+38
@@ -12,6 +12,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: ^1.58.2
|
||||||
|
version: 1.58.2
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^8.56.1
|
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)
|
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==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@playwright/test@1.58.2':
|
||||||
|
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@protobufjs/aspromise@1.1.2':
|
'@protobufjs/aspromise@1.1.2':
|
||||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||||
|
|
||||||
@@ -2277,6 +2285,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||||
engines: {node: '>=10'}
|
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:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -2962,6 +2975,16 @@ packages:
|
|||||||
platform@1.3.6:
|
platform@1.3.6:
|
||||||
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
|
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:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4729,6 +4752,10 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@playwright/test@1.58.2':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.58.2
|
||||||
|
|
||||||
'@protobufjs/aspromise@1.1.2': {}
|
'@protobufjs/aspromise@1.1.2': {}
|
||||||
|
|
||||||
'@protobufjs/base64@1.1.2': {}
|
'@protobufjs/base64@1.1.2': {}
|
||||||
@@ -5898,6 +5925,9 @@ snapshots:
|
|||||||
jsonfile: 6.2.0
|
jsonfile: 6.2.0
|
||||||
universalify: 2.0.1
|
universalify: 2.0.1
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -6552,6 +6582,14 @@ snapshots:
|
|||||||
|
|
||||||
platform@1.3.6: {}
|
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: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss@8.5.8:
|
postcss@8.5.8:
|
||||||
|
|||||||
Reference in New Issue
Block a user