Milestone A of the frontend test plan. Sets up the test rig — config, globalSetup that probes the backend and seeds an admin session into storageState, lightweight fixtures, and a 3-test smoke spec — without yet covering any user journeys (those land in Milestone B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
import { chromium, request } from '@playwright/test';
|
|
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const API_BASE = process.env.E2E_API_BASE ?? 'http://127.0.0.1:18080';
|
|
const DASHBOARD_PORT = Number(process.env.PICLOUD_DASHBOARD_PORT ?? 5173);
|
|
const DASHBOARD_ORIGIN = process.env.E2E_DASHBOARD_ORIGIN ?? `http://localhost:${DASHBOARD_PORT}`;
|
|
const ADMIN_USERNAME = process.env.E2E_ADMIN_USERNAME ?? 'admin';
|
|
const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? 'admin';
|
|
|
|
const AUTH_DIR = path.join(__dirname, '.auth');
|
|
const ADMIN_STATE_PATH = path.join(AUTH_DIR, 'admin.json');
|
|
|
|
export default async function globalSetup(): Promise<void> {
|
|
await assertBackendUp();
|
|
await fs.mkdir(AUTH_DIR, { recursive: true });
|
|
const token = await loginAsAdmin();
|
|
await persistAdminStorageState(token);
|
|
}
|
|
|
|
async function assertBackendUp(): Promise<void> {
|
|
const probe = await request.newContext();
|
|
try {
|
|
const res = await probe.get(`${API_BASE}/healthz`, { timeout: 5_000 });
|
|
if (!res.ok()) {
|
|
throw new Error(
|
|
`backend /healthz returned ${res.status()} — is \`cargo run -p picloud\` listening on ${API_BASE}?`
|
|
);
|
|
}
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Could not reach backend at ${API_BASE}/healthz. ` +
|
|
`Bring it up before running E2E tests:\n\n` +
|
|
` docker compose up -d postgres\n` +
|
|
` PICLOUD_BIND=127.0.0.1:18080 \\\n` +
|
|
` PICLOUD_ADMIN_USERNAME=${ADMIN_USERNAME} \\\n` +
|
|
` PICLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} \\\n` +
|
|
` DATABASE_URL=postgres://picloud:picloud@127.0.0.1:15432/picloud \\\n` +
|
|
` cargo run -p picloud\n\n` +
|
|
`Underlying error: ${(err as Error).message}`
|
|
);
|
|
} finally {
|
|
await probe.dispose();
|
|
}
|
|
}
|
|
|
|
async function loginAsAdmin(): Promise<string> {
|
|
const ctx = await request.newContext();
|
|
try {
|
|
const res = await ctx.post(`${API_BASE}/api/v1/admin/auth/login`, {
|
|
data: { username: ADMIN_USERNAME, password: ADMIN_PASSWORD },
|
|
headers: { 'content-type': 'application/json' }
|
|
});
|
|
if (!res.ok()) {
|
|
const body = await res.text();
|
|
throw new Error(
|
|
`Admin login failed (${res.status()}): ${body}. ` +
|
|
`Verify PICLOUD_ADMIN_USERNAME / PICLOUD_ADMIN_PASSWORD match the seeded bootstrap admin.`
|
|
);
|
|
}
|
|
const payload = (await res.json()) as { token?: string };
|
|
if (!payload.token) {
|
|
throw new Error('Admin login response missing token field');
|
|
}
|
|
return payload.token;
|
|
} finally {
|
|
await ctx.dispose();
|
|
}
|
|
}
|
|
|
|
// The dashboard reads its session from localStorage under the key
|
|
// `picloud.admin.token` (see src/lib/auth.ts). We can't write to
|
|
// localStorage without a browser context, so launch a throwaway one,
|
|
// seed the value, then save storageState for every test to reuse.
|
|
async function persistAdminStorageState(token: string): Promise<void> {
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
await page.goto(`${DASHBOARD_ORIGIN}/admin/login`);
|
|
await page.evaluate(
|
|
([key, value]) => {
|
|
localStorage.setItem(key, value);
|
|
},
|
|
['picloud.admin.token', token]
|
|
);
|
|
await context.storageState({ path: ADMIN_STATE_PATH });
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|