test(web): add Playwright E2E and admin API test suite for RCA-19

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>
This commit is contained in:
徐枫
2026-03-17 20:24:22 +08:00
parent e3a9d9f63c
commit f4144c96f1
11 changed files with 1529 additions and 1 deletions

View File

@@ -0,0 +1,153 @@
import { type Page } from "@playwright/test";
/**
* Intercept /admin/dashboard and return a canned response so UI tests
* don't depend on a running relay server with real data.
*/
export async function mockDashboard(page: Page): Promise<void> {
await page.route("**/admin/dashboard", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
devices: { total: 3, online: 2, offline: 1 },
cookies: { total: 142, domains: 8 },
syncCount: 57,
uptimeSeconds: 86400,
}),
}),
);
}
/**
* Intercept /admin/cookies and return a paginated list.
*/
export async function mockCookies(page: Page): Promise<void> {
await page.route("**/admin/cookies*", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
cookies: [
{
id: "c1",
domain: "example.com",
name: "session",
value: "abc123",
path: "/",
expires: "2027-01-01T00:00:00Z",
secure: true,
httpOnly: true,
},
{
id: "c2",
domain: "example.com",
name: "pref",
value: "dark",
path: "/",
expires: "2027-06-01T00:00:00Z",
secure: false,
httpOnly: false,
},
{
id: "c3",
domain: "other.io",
name: "token",
value: "xyz",
path: "/",
expires: null,
secure: true,
httpOnly: true,
},
],
total: 3,
page: 1,
}),
}),
);
}
/**
* Intercept /admin/devices and return device list.
*/
export async function mockDevices(page: Page): Promise<void> {
await page.route("**/admin/devices*", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
devices: [
{
id: "d1",
name: "Chrome on macOS",
platform: "chrome",
online: true,
lastSeen: new Date().toISOString(),
registeredAt: "2026-01-01T00:00:00Z",
ipAddress: "192.168.1.10",
extensionVersion: "2.0.0",
},
{
id: "d2",
name: "Firefox on Windows",
platform: "firefox",
online: false,
lastSeen: "2026-03-15T10:00:00Z",
registeredAt: "2026-02-01T00:00:00Z",
ipAddress: null,
extensionVersion: "2.0.0",
},
],
}),
}),
);
}
/**
* Intercept /admin/settings and return settings object.
*/
export async function mockSettings(page: Page): Promise<void> {
await page.route("**/admin/settings*", (route) => {
if (route.request().method() === "GET") {
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
sync: {
autoSync: true,
frequency: "realtime",
domainWhitelist: [],
domainBlacklist: [],
},
security: {
sessionTimeoutMinutes: 60,
requirePairingPin: false,
},
appearance: {
theme: "system",
language: "zh",
},
}),
});
}
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
});
}
/**
* Simulate a 500 error on the given path — used for error-handling tests.
*/
export async function mockAPIError(
page: Page,
urlPattern: string,
status = 500,
message = "Internal Server Error",
): Promise<void> {
await page.route(urlPattern, (route) =>
route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify({ error: message }),
}),
);
}