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:
171
web/tests/e2e/04-devices.spec.ts
Normal file
171
web/tests/e2e/04-devices.spec.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user