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 }); }); });