Files
CookieBridge/web/tests/e2e/04-devices.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

172 lines
4.9 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { loginViaAPI } from "./helpers/auth.js";
import { mockDevices, mockAPIError } from "./helpers/mock-api.js";
/**
* RCA-17: Device management page
*
* Covers:
* - Device card grid layout
* - Online/offline status badge
* - Platform icons (chrome, firefox, edge, safari)
* - Last seen time displayed
* - Remote revoke with confirmation dialog
* - Device detail expansion
* - Filter by online status
* - API error state
*/
test.describe("Device management", () => {
test.beforeEach(async ({ page, request }) => {
await loginViaAPI(page, request);
await mockDevices(page);
});
test("displays device cards in a grid", async ({ page }) => {
await page.goto("/devices");
await expect(page.getByText("Chrome on macOS")).toBeVisible();
await expect(page.getByText("Firefox on Windows")).toBeVisible();
});
test("shows online badge for online device", async ({ page }) => {
await page.goto("/devices");
// Find the Chrome on macOS card and verify it has an online indicator
const chromeCard = page.locator("[class*=card], [class*=device]").filter({
hasText: "Chrome on macOS",
});
await expect(chromeCard).toBeVisible();
await expect(
chromeCard.getByText(/online/i).or(chromeCard.locator("[class*=online]")),
).toBeVisible();
});
test("shows offline badge for offline device", async ({ page }) => {
await page.goto("/devices");
const ffCard = page.locator("[class*=card], [class*=device]").filter({
hasText: "Firefox on Windows",
});
await expect(ffCard).toBeVisible();
await expect(
ffCard.getByText(/offline/i).or(ffCard.locator("[class*=offline]")),
).toBeVisible();
});
test("shows last active time for each device", async ({ page }) => {
await page.goto("/devices");
await expect(page.getByText(/last seen|last active/i).first()).toBeVisible();
});
test("remote revoke opens confirmation dialog", async ({ page }) => {
await page.goto("/devices");
const revokeBtn = page
.getByRole("button", { name: /revoke|logout|sign out/i })
.first();
await revokeBtn.click();
await expect(
page
.getByRole("dialog")
.or(page.getByText(/confirm|are you sure|revoke/i))
.first(),
).toBeVisible();
});
test("confirming revoke calls POST /admin/devices/:id/revoke", async ({ page }) => {
let revokeCalled = false;
await page.route("**/admin/devices/d1/revoke", (route) => {
if (route.request().method() === "POST") {
revokeCalled = true;
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
}
return route.continue();
});
await page.goto("/devices");
const revokeBtn = page
.getByRole("button", { name: /revoke|logout|sign out/i })
.first();
await revokeBtn.click();
await page
.getByRole("button", { name: /confirm|yes|revoke/i })
.last()
.click();
expect(revokeCalled).toBe(true);
});
test("cancelling revoke dialog does not call API", async ({ page }) => {
let revokeCalled = false;
await page.route("**/admin/devices/*/revoke", (route) => {
revokeCalled = true;
return route.continue();
});
await page.goto("/devices");
const revokeBtn = page
.getByRole("button", { name: /revoke|logout|sign out/i })
.first();
await revokeBtn.click();
await page
.getByRole("button", { name: /cancel|no/i })
.last()
.click();
expect(revokeCalled).toBe(false);
});
test("device detail expansion shows extra fields", async ({ page }) => {
await page.goto("/devices");
// Click a device card or expand button to reveal detail
const card = page
.locator("[class*=card], [class*=device]")
.filter({ hasText: "Chrome on macOS" });
await card.click();
await expect(
page
.getByText(/extension version|version/i)
.or(page.getByText(/registered|first seen/i))
.first(),
).toBeVisible();
});
test("filter by 'online' shows only online devices", async ({ page }) => {
await page.goto("/devices");
const filterSelect = page
.getByLabel(/filter|status/i)
.or(page.getByRole("combobox"))
.or(page.getByRole("listbox"));
if ((await filterSelect.count()) > 0) {
await filterSelect.first().selectOption({ label: /online/i });
await expect(page.getByText("Chrome on macOS")).toBeVisible();
await expect(page.getByText("Firefox on Windows")).not.toBeVisible();
}
});
test("shows error message when devices API fails", async ({ page }) => {
await page.unroute("**/admin/devices*");
await mockAPIError(page, "**/admin/devices*", 500, "Failed to load devices");
await page.goto("/devices");
await expect(
page
.getByRole("alert")
.or(page.getByText(/error|failed|could not load/i))
.first(),
).toBeVisible();
});
});