Files
CookieBridge/web/tests/e2e/03-cookies.spec.ts
徐枫 f4144c96f1 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>
2026-03-17 20:24:22 +08:00

187 lines
5.9 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { loginViaAPI } from "./helpers/auth.js";
import { mockCookies, mockAPIError } from "./helpers/mock-api.js";
/**
* RCA-16: Cookie management page
*
* Covers:
* - Cookies grouped by domain
* - Search by domain name
* - Search by cookie name
* - Detail panel shows all fields
* - Delete single cookie with confirmation
* - Bulk delete
* - Domain group collapse/expand
* - Pagination / scroll
* - API error state
*/
test.describe("Cookie management", () => {
test.beforeEach(async ({ page, request }) => {
await loginViaAPI(page, request);
await mockCookies(page);
});
test("lists cookies grouped by domain", async ({ page }) => {
await page.goto("/cookies");
await expect(page.getByText("example.com")).toBeVisible();
await expect(page.getByText("other.io")).toBeVisible();
});
test("search by domain filters results", async ({ page }) => {
await page.goto("/cookies");
const searchInput = page
.getByPlaceholder(/search/i)
.or(page.getByRole("searchbox"))
.or(page.getByLabel(/search/i));
await searchInput.fill("other.io");
await expect(page.getByText("other.io")).toBeVisible();
await expect(page.getByText("example.com")).not.toBeVisible();
});
test("search by cookie name filters results", async ({ page }) => {
await page.goto("/cookies");
const searchInput = page
.getByPlaceholder(/search/i)
.or(page.getByRole("searchbox"))
.or(page.getByLabel(/search/i));
await searchInput.fill("session");
// "session" cookie under example.com should be visible
await expect(page.getByText("session")).toBeVisible();
// "token" under other.io should not be visible
await expect(page.getByText("token")).not.toBeVisible();
});
test("clicking a cookie shows detail panel with all fields", async ({ page }) => {
await page.goto("/cookies");
// Click the "session" cookie row
await page.getByText("session").first().click();
// Detail panel should show all cookie fields
await expect(page.getByText(/name/i)).toBeVisible();
await expect(page.getByText(/value/i)).toBeVisible();
await expect(page.getByText(/domain/i)).toBeVisible();
await expect(page.getByText(/path/i)).toBeVisible();
await expect(page.getByText(/expires/i)).toBeVisible();
await expect(page.getByText(/secure/i)).toBeVisible();
await expect(page.getByText(/httponly/i)).toBeVisible();
});
test("deletes a single cookie after confirmation", async ({ page }) => {
let deleteCalled = false;
await page.route("**/admin/cookies/c1", (route) => {
if (route.request().method() === "DELETE") {
deleteCalled = true;
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
}
return route.continue();
});
await page.goto("/cookies");
// Click the first cookie's delete button
const deleteBtn = page
.getByRole("button", { name: /delete/i })
.first();
await deleteBtn.click();
// Confirmation dialog should appear
await expect(
page.getByRole("dialog").or(page.getByText(/confirm|are you sure/i)),
).toBeVisible();
// Confirm deletion
await page.getByRole("button", { name: /confirm|yes|delete/i }).last().click();
expect(deleteCalled).toBe(true);
});
test("cancel on delete dialog does not delete the cookie", async ({ page }) => {
let deleteCalled = false;
await page.route("**/admin/cookies/*", (route) => {
if (route.request().method() === "DELETE") {
deleteCalled = true;
}
return route.continue();
});
await page.goto("/cookies");
const deleteBtn = page.getByRole("button", { name: /delete/i }).first();
await deleteBtn.click();
await page
.getByRole("button", { name: /cancel|no/i })
.last()
.click();
expect(deleteCalled).toBe(false);
});
test("can select multiple cookies and bulk delete", async ({ page }) => {
let bulkDeleteCalled = false;
await page.route("**/admin/cookies", (route) => {
if (route.request().method() === "DELETE") {
bulkDeleteCalled = true;
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
}
return route.continue();
});
await page.goto("/cookies");
// Select checkboxes
const checkboxes = page.getByRole("checkbox");
const count = await checkboxes.count();
if (count > 0) {
await checkboxes.first().check();
if (count > 1) await checkboxes.nth(1).check();
const bulkBtn = page.getByRole("button", { name: /delete selected|bulk delete/i });
if (await bulkBtn.isVisible()) {
await bulkBtn.click();
await page.getByRole("button", { name: /confirm|yes|delete/i }).last().click();
expect(bulkDeleteCalled).toBe(true);
}
}
});
test("domain group collapses and expands", async ({ page }) => {
await page.goto("/cookies");
// Find a domain group header and click to collapse
const groupHeader = page.getByText("example.com").first();
await groupHeader.click();
// After collapse, cookies within that domain should be hidden
// (exact selector depends on implementation — check one of the children)
const sessionCookie = page.getByText("session");
// It may be hidden or removed; either is acceptable
const isVisible = await sessionCookie.isVisible().catch(() => false);
// Click again to expand
await groupHeader.click();
await expect(page.getByText("session")).toBeVisible();
});
test("shows error message when cookies API fails", async ({ page }) => {
await page.unroute("**/admin/cookies*");
await mockAPIError(page, "**/admin/cookies*", 500, "Failed to load cookies");
await page.goto("/cookies");
await expect(
page
.getByRole("alert")
.or(page.getByText(/error|failed|could not load/i))
.first(),
).toBeVisible();
});
});