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:
210
web/tests/e2e/05-settings.spec.ts
Normal file
210
web/tests/e2e/05-settings.spec.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { loginViaAPI } from "./helpers/auth.js";
|
||||
import { mockSettings, mockAPIError } from "./helpers/mock-api.js";
|
||||
|
||||
/**
|
||||
* RCA-18: Settings page
|
||||
*
|
||||
* Covers:
|
||||
* - Three tabs: Sync / Security / Appearance
|
||||
* - Settings are pre-populated from GET /admin/settings
|
||||
* - Changes saved via PATCH /admin/settings
|
||||
* - Success toast on save
|
||||
* - Password change (security tab)
|
||||
* - Theme selection (appearance tab)
|
||||
* - Language selection (appearance tab)
|
||||
* - API error on save
|
||||
*/
|
||||
|
||||
test.describe("Settings page", () => {
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
await loginViaAPI(page, request);
|
||||
await mockSettings(page);
|
||||
});
|
||||
|
||||
test("displays three tabs: sync, security, appearance", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
|
||||
await expect(page.getByRole("tab", { name: /sync/i })).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: /security/i })).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: /appearance/i })).toBeVisible();
|
||||
});
|
||||
|
||||
// --- Sync tab ---
|
||||
|
||||
test("sync tab: auto-sync toggle reflects saved value", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
|
||||
await page.getByRole("tab", { name: /sync/i }).click();
|
||||
|
||||
const toggle = page
|
||||
.getByRole("switch", { name: /auto.?sync/i })
|
||||
.or(page.getByLabel(/auto.?sync/i));
|
||||
// Mock returns autoSync: true
|
||||
await expect(toggle).toBeChecked();
|
||||
});
|
||||
|
||||
test("sync tab: frequency selector shows current value", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /sync/i }).click();
|
||||
|
||||
// Mock returns frequency: "realtime"
|
||||
const select = page
|
||||
.getByLabel(/frequency/i)
|
||||
.or(page.getByRole("combobox").filter({ hasText: /realtime/i }));
|
||||
await expect(select).toBeVisible();
|
||||
});
|
||||
|
||||
test("sync tab: saving calls PATCH /admin/settings", async ({ page }) => {
|
||||
let patchCalled = false;
|
||||
await page.route("**/admin/settings", (route) => {
|
||||
if (route.request().method() === "PATCH") {
|
||||
patchCalled = true;
|
||||
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /sync/i }).click();
|
||||
|
||||
// Toggle auto-sync off
|
||||
const toggle = page
|
||||
.getByRole("switch", { name: /auto.?sync/i })
|
||||
.or(page.getByLabel(/auto.?sync/i));
|
||||
await toggle.click();
|
||||
|
||||
// Some implementations save immediately; others have an explicit Save button
|
||||
const saveBtn = page.getByRole("button", { name: /save/i });
|
||||
if (await saveBtn.isVisible()) await saveBtn.click();
|
||||
|
||||
expect(patchCalled).toBe(true);
|
||||
});
|
||||
|
||||
test("sync tab: success toast appears after save", async ({ page }) => {
|
||||
await page.route("**/admin/settings", (route) => {
|
||||
if (route.request().method() === "PATCH") {
|
||||
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /sync/i }).click();
|
||||
|
||||
const toggle = page
|
||||
.getByRole("switch", { name: /auto.?sync/i })
|
||||
.or(page.getByLabel(/auto.?sync/i));
|
||||
await toggle.click();
|
||||
|
||||
const saveBtn = page.getByRole("button", { name: /save/i });
|
||||
if (await saveBtn.isVisible()) await saveBtn.click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/saved|success|updated/i).first(),
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
// --- Security tab ---
|
||||
|
||||
test("security tab: change password requires current + new + confirm", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /security/i }).click();
|
||||
|
||||
await expect(page.getByLabel(/current password/i)).toBeVisible();
|
||||
await expect(page.getByLabel(/new password/i)).toBeVisible();
|
||||
await expect(page.getByLabel(/confirm.*(new )?password/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("security tab: password change with mismatch shows error", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /security/i }).click();
|
||||
|
||||
await page.getByLabel(/current password/i).fill("oldPass123");
|
||||
await page.getByLabel(/new password/i).fill("NewPass456!");
|
||||
await page.getByLabel(/confirm.*(new )?password/i).fill("Different789!");
|
||||
|
||||
await page.getByRole("button", { name: /change|save password|update/i }).click();
|
||||
|
||||
await expect(page.getByText(/do not match|mismatch|must match/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("security tab: session timeout field accepts numeric input", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /security/i }).click();
|
||||
|
||||
const timeoutField = page
|
||||
.getByLabel(/session timeout/i)
|
||||
.or(page.getByRole("spinbutton").filter({ hasText: /timeout/i }));
|
||||
if (await timeoutField.isVisible()) {
|
||||
await timeoutField.fill("120");
|
||||
await expect(timeoutField).toHaveValue("120");
|
||||
}
|
||||
});
|
||||
|
||||
// --- Appearance tab ---
|
||||
|
||||
test("appearance tab: theme options present (light/dark/system)", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /appearance/i }).click();
|
||||
|
||||
await expect(page.getByText(/light/i)).toBeVisible();
|
||||
await expect(page.getByText(/dark/i)).toBeVisible();
|
||||
await expect(page.getByText(/system/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("appearance tab: language selector shows Chinese and English options", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /appearance/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/chinese|中文/i).or(page.getByText("zh")),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/english/i).or(page.getByText("en")),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// --- Error states ---
|
||||
|
||||
test("shows error message when settings fail to load", async ({ page }) => {
|
||||
await page.unroute("**/admin/settings*");
|
||||
await mockAPIError(page, "**/admin/settings*", 500, "Failed to load settings");
|
||||
|
||||
await page.goto("/settings");
|
||||
await expect(
|
||||
page
|
||||
.getByRole("alert")
|
||||
.or(page.getByText(/error|failed|could not load/i))
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows error toast when save fails", async ({ page }) => {
|
||||
await page.route("**/admin/settings", (route) => {
|
||||
if (route.request().method() === "PATCH") {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "Server error" }),
|
||||
});
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: /sync/i }).click();
|
||||
|
||||
const toggle = page
|
||||
.getByRole("switch", { name: /auto.?sync/i })
|
||||
.or(page.getByLabel(/auto.?sync/i));
|
||||
await toggle.click();
|
||||
|
||||
const saveBtn = page.getByRole("button", { name: /save/i });
|
||||
if (await saveBtn.isVisible()) await saveBtn.click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/error|failed|could not save/i).first(),
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user