Files
CookieBridge/web/tests/e2e/01-login.spec.ts
徐枫 6504d3c7b9 fix: resolve 6 QA bugs in frontend admin panel (RCA-19)
Bug 1: Dashboard child route path "" → "dashboard" + redirect from /
Bug 2: Test localStorage key "admin_token" → "cb_admin_token"
Bug 3: Router setup check data.isSetUp → data.initialised
Bug 4: Setup wizard button text to match test selectors
Bug 5: loginViaAPI helper sets localStorage directly instead of hitting relay
Bug 6: Login button disabled when fields are empty

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 01:47:21 +08:00

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("cb_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("cb_admin_token"));
expect(token).toBeNull();
});
});