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