The act runner executes steps inside a container where localhost does not reach the Docker host. Use E2E_API_URL env var (set to the host IP 192.168.1.10:8100 in CI) so both the global setup and Vite proxy can reach the test API container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
import { execSync } from 'node:child_process'
|
|
import { writeFileSync } from 'node:fs'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const API_HOST = process.env.E2E_API_URL || 'http://localhost:8100'
|
|
const API_BASE = `${API_HOST}/api/v1`
|
|
const COMPOSE_FILE = resolve(__dirname, '../../docker-compose.test.yml')
|
|
const COMPOSE = `docker compose -p nuzlocke-test -f ${COMPOSE_FILE}`
|
|
const FIXTURES_PATH = resolve(__dirname, '.fixtures.json')
|
|
|
|
function run(cmd: string): string {
|
|
console.log(`[setup] ${cmd}`)
|
|
return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'inherit'] })
|
|
}
|
|
|
|
async function waitForApi(url: string, maxAttempts = 30): Promise<void> {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
try {
|
|
const res = await fetch(url)
|
|
if (res.ok) return
|
|
} catch {
|
|
// not ready yet
|
|
}
|
|
await new Promise((r) => setTimeout(r, 2000))
|
|
}
|
|
throw new Error(`API at ${url} not ready after ${maxAttempts} attempts`)
|
|
}
|
|
|
|
async function api<T>(
|
|
path: string,
|
|
options?: RequestInit,
|
|
): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options,
|
|
})
|
|
if (!res.ok) {
|
|
const body = await res.text()
|
|
throw new Error(`API ${options?.method ?? 'GET'} ${path} → ${res.status}: ${body}`)
|
|
}
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
export default async function globalSetup() {
|
|
// 1. Start test DB + API
|
|
run(`${COMPOSE} up -d --build`)
|
|
|
|
// 2. Wait for API to be healthy
|
|
console.log('[setup] Waiting for API to be ready...')
|
|
await waitForApi(`${API_HOST}/`)
|
|
|
|
// 3. Run migrations
|
|
run(`${COMPOSE} exec -T test-api alembic -c /app/alembic.ini upgrade head`)
|
|
|
|
// 4. Seed reference data (run from /app/src where the app package lives)
|
|
run(`${COMPOSE} exec -T -w /app/src test-api python -m app.seeds`)
|
|
|
|
// 5. Create test fixtures via API
|
|
const games = await api<Array<{ id: number; name: string }>>('/games')
|
|
const game = games[0]
|
|
if (!game) throw new Error('No games found after seeding')
|
|
|
|
const routes = await api<Array<{ id: number; name: string; parentRouteId: number | null }>>(
|
|
`/games/${game.id}/routes?flat=true`,
|
|
)
|
|
// Pick leaf routes (no children — a route is a leaf if no other route has it as parent)
|
|
const parentIds = new Set(routes.map((r) => r.parentRouteId).filter(Boolean))
|
|
const leafRoutes = routes.filter((r) => !parentIds.has(r.id))
|
|
if (leafRoutes.length < 3) throw new Error(`Need ≥3 leaf routes, found ${leafRoutes.length}`)
|
|
|
|
const pokemonRes = await api<{ items: Array<{ id: number; name: string }> }>(
|
|
'/pokemon?limit=10',
|
|
)
|
|
const pokemon = pokemonRes.items
|
|
if (pokemon.length < 3) throw new Error(`Need ≥3 pokemon, found ${pokemon.length}`)
|
|
|
|
// Create a test run
|
|
const testRun = await api<{ id: number }>('/runs', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
gameId: game.id,
|
|
name: 'E2E Test Run',
|
|
rules: { duplicatesClause: true, shinyClause: true },
|
|
}),
|
|
})
|
|
|
|
// Create encounters: caught, fainted, missed
|
|
const statuses = ['caught', 'fainted', 'missed'] as const
|
|
for (let i = 0; i < 3; i++) {
|
|
await api(`/runs/${testRun.id}/encounters`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
routeId: leafRoutes[i]!.id,
|
|
pokemonId: pokemon[i]!.id,
|
|
nickname: `Test ${statuses[i]}`,
|
|
status: statuses[i],
|
|
catchLevel: statuses[i] === 'missed' ? null : 5 + i * 10,
|
|
}),
|
|
})
|
|
}
|
|
|
|
// Create a genlocke with 2 game legs
|
|
const secondGame = games[1] ?? game
|
|
const genlocke = await api<{ id: number }>('/genlockes', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
name: 'E2E Test Genlocke',
|
|
gameIds: [game.id, secondGame.id],
|
|
genlockeRules: {},
|
|
nuzlockeRules: { duplicatesClause: true },
|
|
}),
|
|
})
|
|
|
|
// 6. Write fixtures file
|
|
const fixtures = {
|
|
gameId: game.id,
|
|
runId: testRun.id,
|
|
genlockeId: genlocke.id,
|
|
}
|
|
writeFileSync(FIXTURES_PATH, JSON.stringify(fixtures, null, 2))
|
|
console.log('[setup] Fixtures written:', fixtures)
|
|
}
|