Prepares the full QA test infrastructure for the admin frontend before all prerequisite feature tasks (RCA-12–18) are complete. - playwright.config.ts: 6 browser/device projects (Chromium, Firefox, WebKit, mobile Chrome, mobile Safari, tablet) - tests/e2e/01-login.spec.ts: login form, route guards, setup wizard - tests/e2e/02-dashboard.spec.ts: stats cards, device list, quick actions - tests/e2e/03-cookies.spec.ts: cookie list, search, detail panel, delete - tests/e2e/04-devices.spec.ts: device cards, revoke flow, status filter - tests/e2e/05-settings.spec.ts: three-tab layout, save/error toasts - tests/e2e/06-responsive.spec.ts: no horizontal scroll on mobile/tablet - tests/api/admin-api.spec.ts: REST API contract tests for all /admin/* endpoints - helpers/auth.ts: loginViaUI + loginViaAPI helpers - helpers/mock-api.ts: route intercept fixtures for all pages Co-Authored-By: Paperclip <noreply@paperclip.ing>
240 lines
7.9 KiB
TypeScript
240 lines
7.9 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
/**
|
|
* RCA-14: Login page + first-run setup wizard
|
|
*
|
|
* Covers:
|
|
* - Login / logout flow
|
|
* - Form validation and error display
|
|
* - Route guard: unauthenticated redirect to /login
|
|
* - Route guard: authenticated redirect away from /login → dashboard
|
|
* - First-run setup wizard (GET /admin/setup/status → redirect to /setup)
|
|
*/
|
|
|
|
test.describe("Login page", () => {
|
|
test("shows username and password fields", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await expect(page.getByLabel(/username/i)).toBeVisible();
|
|
await expect(page.getByLabel(/password/i)).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: /log in|sign in/i }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("disables submit while fields are empty", async ({ page }) => {
|
|
await page.goto("/login");
|
|
const btn = page.getByRole("button", { name: /log in|sign in/i });
|
|
// Should either be disabled or clicking it shows a validation error
|
|
const isEmpty = (await btn.getAttribute("disabled")) !== null;
|
|
if (!isEmpty) {
|
|
await btn.click();
|
|
// At least one validation error should appear
|
|
const hasError = await page
|
|
.getByRole("alert")
|
|
.or(page.locator("[class*=error]"))
|
|
.or(page.locator("[class*=invalid]"))
|
|
.count();
|
|
expect(hasError).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("shows error on invalid credentials", async ({ page }) => {
|
|
await page.route("**/admin/auth/login", (route) =>
|
|
route.fulfill({
|
|
status: 401,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ error: "Invalid credentials" }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await page.getByLabel(/username/i).fill("wrong");
|
|
await page.getByLabel(/password/i).fill("wrong");
|
|
await page.getByRole("button", { name: /log in|sign in/i }).click();
|
|
|
|
await expect(page.getByText(/invalid credentials|wrong|incorrect/i)).toBeVisible();
|
|
// Should remain on /login
|
|
await expect(page).toHaveURL(/\/login/);
|
|
});
|
|
|
|
test("submits form on Enter key", async ({ page }) => {
|
|
await page.route("**/admin/auth/login", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ token: "test-jwt", expiresAt: "2099-01-01" }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await page.getByLabel(/username/i).fill("admin");
|
|
await page.getByLabel(/password/i).fill("password");
|
|
await page.getByLabel(/password/i).press("Enter");
|
|
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
|
});
|
|
|
|
test("redirects to dashboard on successful login", async ({ page }) => {
|
|
await page.route("**/admin/auth/login", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ token: "test-jwt", expiresAt: "2099-01-01" }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await page.getByLabel(/username/i).fill("admin");
|
|
await page.getByLabel(/password/i).fill("password");
|
|
await page.getByRole("button", { name: /log in|sign in/i }).click();
|
|
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
|
});
|
|
});
|
|
|
|
test.describe("Route guards", () => {
|
|
test("unauthenticated user is redirected to /login from protected routes", async ({
|
|
page,
|
|
}) => {
|
|
for (const route of ["/dashboard", "/cookies", "/devices", "/settings"]) {
|
|
await page.goto(route);
|
|
await expect(page).toHaveURL(/\/login/);
|
|
}
|
|
});
|
|
|
|
test("authenticated user visiting /login is redirected to /dashboard", async ({
|
|
page,
|
|
}) => {
|
|
// Seed a token so the app thinks we're logged in
|
|
await page.goto("/login");
|
|
await page.evaluate(() => localStorage.setItem("admin_token", "fake-jwt"));
|
|
|
|
// Mock /admin/auth/me to return a valid user
|
|
await page.route("**/admin/auth/me", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ username: "admin" }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
|
});
|
|
});
|
|
|
|
test.describe("First-run setup wizard", () => {
|
|
test("redirects to /setup when not yet initialised", async ({ page }) => {
|
|
await page.route("**/admin/setup/status", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ initialised: false }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/");
|
|
await expect(page).toHaveURL(/\/setup/);
|
|
});
|
|
|
|
test("wizard has 4 steps and can be completed", async ({ page }) => {
|
|
await page.route("**/admin/setup/status", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ initialised: false }),
|
|
}),
|
|
);
|
|
await page.route("**/admin/setup/init", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ ok: true }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/setup");
|
|
|
|
// Step 1: Welcome
|
|
await expect(page.getByText(/welcome|cookiebridge/i)).toBeVisible();
|
|
await page.getByRole("button", { name: /next|continue/i }).click();
|
|
|
|
// Step 2: Create admin account
|
|
await page.getByLabel(/username/i).fill("admin");
|
|
await page.getByLabel(/^password$/i).fill("Secure123!");
|
|
await page.getByLabel(/confirm password/i).fill("Secure123!");
|
|
await page.getByRole("button", { name: /next|continue/i }).click();
|
|
|
|
// Step 3: Basic config (port, HTTPS)
|
|
await expect(
|
|
page.getByLabel(/port/i).or(page.getByText(/port|https/i)),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: /next|continue/i }).click();
|
|
|
|
// Step 4: Completion
|
|
await expect(page.getByText(/done|complete|finish/i)).toBeVisible();
|
|
await page.getByRole("button", { name: /go to login|finish/i }).click();
|
|
await expect(page).toHaveURL(/\/login/);
|
|
});
|
|
|
|
test("password mismatch in setup shows error", async ({ page }) => {
|
|
await page.route("**/admin/setup/status", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ initialised: false }),
|
|
}),
|
|
);
|
|
|
|
await page.goto("/setup");
|
|
await page.getByRole("button", { name: /next|continue/i }).click();
|
|
|
|
await page.getByLabel(/username/i).fill("admin");
|
|
await page.getByLabel(/^password$/i).fill("Secure123!");
|
|
await page.getByLabel(/confirm password/i).fill("Mismatch999!");
|
|
await page.getByRole("button", { name: /next|continue/i }).click();
|
|
|
|
await expect(page.getByText(/do not match|mismatch|must match/i)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("Logout", () => {
|
|
test("logout clears session and redirects to /login", async ({ page }) => {
|
|
await page.route("**/admin/auth/login", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ token: "test-jwt", expiresAt: "2099-01-01" }),
|
|
}),
|
|
);
|
|
await page.route("**/admin/auth/logout", (route) =>
|
|
route.fulfill({ status: 204 }),
|
|
);
|
|
await page.route("**/admin/dashboard", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ devices: {}, cookies: {}, syncCount: 0, uptimeSeconds: 0 }),
|
|
}),
|
|
);
|
|
|
|
// Log in first
|
|
await page.goto("/login");
|
|
await page.getByLabel(/username/i).fill("admin");
|
|
await page.getByLabel(/password/i).fill("password");
|
|
await page.getByRole("button", { name: /log in|sign in/i }).click();
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
|
|
|
// Log out
|
|
const logoutBtn = page
|
|
.getByRole("button", { name: /log ?out|sign ?out/i })
|
|
.or(page.getByRole("link", { name: /log ?out|sign ?out/i }));
|
|
await logoutBtn.click();
|
|
await expect(page).toHaveURL(/\/login/);
|
|
|
|
// Token should be gone
|
|
const token = await page.evaluate(() => localStorage.getItem("admin_token"));
|
|
expect(token).toBeNull();
|
|
});
|
|
});
|