feat: implement full backend + frontend server detail, settings, and create server pages
Backend: - Complete FastAPI backend with 42+ REST endpoints (auth, servers, config, players, bans, missions, mods, games, system) - Game adapter architecture with Arma 3 as first-class adapter - WebSocket real-time events for status, metrics, logs, players - Background thread system (process monitor, metrics, log tail, RCon poller) - Fernet encryption for sensitive config fields at rest - JWT auth with admin/viewer roles, bcrypt password hashing - SQLite with WAL mode, parameterized queries, migration system - APScheduler cleanup jobs for logs, metrics, events Frontend: - Server Detail page with 7 tabs (overview, config, players, bans, missions, mods, logs) - Settings page with password change and admin user management - Create Server wizard (4-step; known bug: silent validation failure) - New hooks: useServerDetail, useAuth, useGames - New components: ServerHeader, ConfigEditor, PlayerTable, BanTable, MissionList, ModList, LogViewer, PasswordChange, UserManager - WebSocket onEvent callback for real-time log accumulation - 120 unit tests passing (Vitest + React Testing Library) Docs: - Added .gitignore, CLAUDE.md, README.md - Updated FRONTEND.md, ARCHITECTURE.md with current implementation state - Added .env.example for backend configuration Known issues: - Create Server form: "Next" buttons don't validate before advancing, causing silent submit failure when fields are invalid - Config sub-tabs need UX redesign for non-technical users
This commit is contained in:
146
frontend/tests-e2e/integration/fullstack.spec.ts
Normal file
146
frontend/tests-e2e/integration/fullstack.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Full-stack integration tests against the real backend (http://localhost:8000).
|
||||
* These tests do NOT mock API routes — they exercise the real API + frontend.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Backend running on port 8000
|
||||
* - Frontend dev server running on port 5173
|
||||
* - Admin account with username "admin" / password "admin123"
|
||||
* - A3Master server record exists
|
||||
*/
|
||||
|
||||
const REAL_API = "http://localhost:8000";
|
||||
|
||||
test.describe("Full Stack Integration", () => {
|
||||
test("should login and see A3Master server on dashboard", async ({ page }) => {
|
||||
// Navigate to login page
|
||||
await page.goto("/login");
|
||||
await expect(page.locator('[data-testid="login-card"]')).toBeVisible();
|
||||
|
||||
// Fill in real credentials
|
||||
await page.locator('[data-testid="login-username"]').fill("admin");
|
||||
await page.locator('[data-testid="login-password"]').fill("admin123");
|
||||
await page.locator('[data-testid="login-submit"]').click();
|
||||
|
||||
// Should navigate to dashboard
|
||||
await page.waitForURL("/", { timeout: 10_000 });
|
||||
|
||||
// Dashboard should render
|
||||
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Should show at least one server (A3Master) — use .first() to avoid strict mode
|
||||
await expect(page.locator("text=A3Master").first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Should show server count
|
||||
const serverCount = await page.locator("[data-testid^='server-card-']").count();
|
||||
expect(serverCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("should show A3Master server details in card", async ({ page }) => {
|
||||
// Login via API and set auth state
|
||||
const loginRes = await page.request.post(`${REAL_API}/api/auth/login`, {
|
||||
data: { username: "admin", password: "admin123" },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const loginData = await loginRes.json();
|
||||
const token = loginData.data.access_token;
|
||||
const user = loginData.data.user;
|
||||
|
||||
// Set auth state in localStorage
|
||||
await page.addInitScript(
|
||||
({ token, user }) => {
|
||||
localStorage.setItem("languard_token", token);
|
||||
localStorage.setItem(
|
||||
"languard-auth",
|
||||
JSON.stringify({ state: { token, user }, version: 0 }),
|
||||
);
|
||||
},
|
||||
{ token, user },
|
||||
);
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Server card should show the A3Master name
|
||||
await expect(page.locator("text=A3Master").first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Server card should show arma3 game type
|
||||
await expect(page.locator("text=arma3").first()).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
|
||||
test("should navigate to server detail page", async ({ page }) => {
|
||||
// Login via API
|
||||
const loginRes = await page.request.post(`${REAL_API}/api/auth/login`, {
|
||||
data: { username: "admin", password: "admin123" },
|
||||
});
|
||||
const loginData = await loginRes.json();
|
||||
const token = loginData.data.access_token;
|
||||
const user = loginData.data.user;
|
||||
|
||||
await page.addInitScript(
|
||||
({ token, user }) => {
|
||||
localStorage.setItem("languard_token", token);
|
||||
localStorage.setItem(
|
||||
"languard-auth",
|
||||
JSON.stringify({ state: { token, user }, version: 0 }),
|
||||
);
|
||||
},
|
||||
{ token, user },
|
||||
);
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Click the A3Master server card link in the dashboard content area
|
||||
const serverLink = page.locator('[data-testid="dashboard-content"] a[href="/servers/1"]');
|
||||
await serverLink.click();
|
||||
|
||||
// Should navigate to server detail
|
||||
await expect(page).toHaveURL(/\/servers\/1/, { timeout: 5_000 });
|
||||
});
|
||||
|
||||
test("should redirect to login when unauthenticated", async ({ page }) => {
|
||||
// Navigate to dashboard without auth — should redirect to login
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
|
||||
await expect(page.locator('[data-testid="login-card"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test("should authenticate and check server API response shape", async ({ page }) => {
|
||||
// Login via API to verify backend response format
|
||||
const loginRes = await page.request.post(`${REAL_API}/api/auth/login`, {
|
||||
data: { username: "admin", password: "admin123" },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const loginData = await loginRes.json();
|
||||
expect(loginData.success).toBe(true);
|
||||
expect(loginData.data.access_token).toBeDefined();
|
||||
expect(loginData.data.user.username).toBe("admin");
|
||||
expect(loginData.data.user.role).toBe("admin");
|
||||
|
||||
// Fetch servers via API
|
||||
const token = loginData.data.access_token;
|
||||
const serversRes = await page.request.get(`${REAL_API}/api/servers`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(serversRes.ok()).toBe(true);
|
||||
const serversData = await serversRes.json();
|
||||
expect(serversData.success).toBe(true);
|
||||
expect(Array.isArray(serversData.data)).toBe(true);
|
||||
expect(serversData.data.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Verify A3Master server has expected fields
|
||||
const a3master = serversData.data.find((s: { name: string }) => s.name === "A3Master");
|
||||
expect(a3master).toBeDefined();
|
||||
expect(a3master.game_type).toBe("arma3");
|
||||
expect(a3master.exe_path).toContain("A3Master");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user