CI / verify (pull_request) Successful in 1m14s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 49s
CI / migration-smoke (pull_request) Successful in 2m30s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 45s
CI / package-publication (pull_request) Successful in 56s
CI / deploy-smoke (split) (pull_request) Successful in 8m55s
Administrators now remove a display: the card's Remove, "Remove display?", and "Display removed". The platform still revokes the device's credential; the endpoint, fields, and security history keep revoke/revoked (ADR 0065). The default accent is a deep purple anchored on #210050, set in the shared design tokens only (ADR 0066, superseding ADR 0060's teal in part). The anchor is the light primary action with white on it; the light accent text, focus ring, and selection, and the dark theme's lavender accent, are variants on its hue chosen for contrast. The teal-tinted neutrals move to a faint violet-grey at the same lightness, so every contrast pairing holds. The contrast test gains six pairings the accent depends on, and pins the anchor. Co-Authored-By: Claude Opus 5.5 <[email protected]>
551 lines
22 KiB
TypeScript
551 lines
22 KiB
TypeScript
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { DisplaysSettingsPage } from "../src/browser/DisplaysSettingsPage.js";
|
|
|
|
declare global {
|
|
// eslint-disable-next-line no-var
|
|
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
|
|
}
|
|
|
|
/**
|
|
* Settings → Displays (ADR 0064, ADR 0065): the active platform displays, setting up a new screen,
|
|
* pairing it, changing what it shows, and revoking it.
|
|
*/
|
|
|
|
const apiBaseUrl = "https://ekklesia.example.test";
|
|
const NOW = new Date("2026-10-15T12:00:00.000Z");
|
|
|
|
/** A screen that asked from Signage's own display page. */
|
|
const MODULE_CODE = "123456";
|
|
/** A screen that asked from the platform's `/display/setup`, with no module. */
|
|
const PLATFORM_CODE = "654321";
|
|
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
|
|
beforeEach(() => {
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
container = document.createElement("div");
|
|
document.body.append(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await act(async () => root.unmount());
|
|
container.remove();
|
|
});
|
|
|
|
function json(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
headers: { "content-type": "application/json" },
|
|
status,
|
|
});
|
|
}
|
|
|
|
interface Device {
|
|
id: string;
|
|
name: string;
|
|
createdAt: string;
|
|
lastSeenAt?: string;
|
|
permissions: string[];
|
|
revoked: boolean;
|
|
}
|
|
|
|
interface ServerOptions {
|
|
readonly devices?: Device[];
|
|
readonly denied?: boolean;
|
|
/** What `GET /api/v1/devices/display-modules` answers. */
|
|
readonly modules?: unknown[] | "fails";
|
|
}
|
|
|
|
const SIGNAGE = { moduleId: "signage", name: "Signage", permissionId: "signage.display" };
|
|
const BULLETIN = { moduleId: "bulletin", name: "Bulletin", permissionId: "bulletin.display" };
|
|
|
|
/**
|
|
* A fake API that behaves like the real one where it matters: an approved display does not exist
|
|
* until its screen collects its credential, which here happens a couple of list requests later.
|
|
*/
|
|
function server(options: ServerOptions = {}) {
|
|
const devices: Device[] = options.devices ?? [
|
|
{
|
|
createdAt: "2026-10-01T09:00:00.000Z",
|
|
id: "device-1",
|
|
lastSeenAt: "2026-10-15T11:50:00.000Z",
|
|
name: "Foyer TV",
|
|
permissions: [],
|
|
revoked: false,
|
|
},
|
|
];
|
|
const modules =
|
|
options.modules === "fails" ? [] : ((options.modules ?? [SIGNAGE]) as (typeof SIGNAGE)[]);
|
|
let pending: { device: Device; listsUntilClaimed: number } | undefined;
|
|
const requests: { path: string; method: string; body: unknown }[] = [];
|
|
const fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const path = new URL(String(input)).pathname;
|
|
const method = init?.method ?? "GET";
|
|
const body = init?.body === undefined ? undefined : JSON.parse(String(init.body));
|
|
requests.push({ body, method, path });
|
|
if (path === "/api/v1/devices/display-modules") {
|
|
return options.modules === "fails"
|
|
? json({ code: "DEVICE_LOOKUP_UNAVAILABLE" }, 503)
|
|
: json({ modules });
|
|
}
|
|
if (path === "/api/v1/devices") {
|
|
if (options.denied) {
|
|
return json({ code: "AUTHORIZATION_DENIED" }, 403);
|
|
}
|
|
if (pending !== undefined) {
|
|
pending.listsUntilClaimed -= 1;
|
|
if (pending.listsUntilClaimed <= 0) {
|
|
devices.push(pending.device);
|
|
pending = undefined;
|
|
}
|
|
}
|
|
return json({ devices });
|
|
}
|
|
const display = /^\/api\/v1\/devices\/([^/]+)\/display$/u.exec(path);
|
|
if (display && method === "PUT") {
|
|
const device = devices.find((candidate) => candidate.id === display[1]);
|
|
const moduleId = (body as { moduleId: string | null }).moduleId;
|
|
const module = modules.find((candidate) => candidate.moduleId === moduleId);
|
|
if (!device || device.revoked || (moduleId !== null && !module)) {
|
|
return json({ code: "NOT_FOUND" }, 404);
|
|
}
|
|
device.permissions = module ? [module.permissionId] : [];
|
|
return json({ moduleId });
|
|
}
|
|
const revoke = /^\/api\/v1\/devices\/([^/]+)\/revoke$/u.exec(path);
|
|
if (revoke) {
|
|
for (const device of devices) {
|
|
if (device.id === revoke[1]) {
|
|
device.revoked = true;
|
|
device.permissions = [];
|
|
}
|
|
}
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
if (path === "/api/v1/device-pairing/lookups") {
|
|
const code = (body as { code: string }).code;
|
|
if (code !== MODULE_CODE && code !== PLATFORM_CODE) {
|
|
return json({ code: "DEVICE_PAIRING_NOT_FOUND" }, 404);
|
|
}
|
|
const module = code === MODULE_CODE;
|
|
return json({
|
|
expiresAt: "2026-10-15T12:10:00.000Z",
|
|
moduleId: module ? "signage" : null,
|
|
permissionId: module ? "signage.display" : null,
|
|
requestedAt: "2026-10-15T11:59:00.000Z",
|
|
});
|
|
}
|
|
if (path === "/api/v1/device-pairing/approvals") {
|
|
const approval = body as { code: string; deviceName: string; moduleId?: string };
|
|
const shows = approval.code === MODULE_CODE ? "signage" : approval.moduleId;
|
|
const module = modules.find((candidate) => candidate.moduleId === shows);
|
|
pending = {
|
|
device: {
|
|
createdAt: "2026-10-15T12:00:00.000Z",
|
|
id: `device-${devices.length + 1}`,
|
|
name: approval.deviceName,
|
|
permissions: module ? [module.permissionId] : [],
|
|
revoked: false,
|
|
},
|
|
listsUntilClaimed: 2,
|
|
};
|
|
return json({ moduleId: shows ?? null, permissionId: module?.permissionId ?? null });
|
|
}
|
|
return json({ code: "NOT_FOUND" }, 404);
|
|
}) as unknown as typeof globalThis.fetch;
|
|
return { devices, fetch, requests };
|
|
}
|
|
|
|
function query(testId: string): HTMLElement | null {
|
|
return container.querySelector(`[data-testid="${testId}"]`);
|
|
}
|
|
|
|
function required(testId: string): HTMLElement {
|
|
const element = query(testId);
|
|
if (!element) {
|
|
throw new Error(`missing ${testId}`);
|
|
}
|
|
return element;
|
|
}
|
|
|
|
async function settle(): Promise<void> {
|
|
for (let index = 0; index < 6; index += 1) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
}
|
|
|
|
/** Waits, in real time, for something the page does on its own (a revalidation). */
|
|
async function eventually(condition: () => boolean, timeoutMs = 2000): Promise<void> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (!condition()) {
|
|
if (Date.now() > deadline) {
|
|
throw new Error("timed out");
|
|
}
|
|
await act(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
});
|
|
}
|
|
}
|
|
|
|
async function click(testId: string): Promise<void> {
|
|
await act(async () => {
|
|
required(testId).click();
|
|
await settle();
|
|
});
|
|
}
|
|
|
|
async function type(testId: string, value: string): Promise<void> {
|
|
const input = required(testId) as HTMLInputElement;
|
|
await act(async () => {
|
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
setter?.call(input, value);
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
async function submit(testId: string): Promise<void> {
|
|
const form = (required(testId) as HTMLButtonElement).form;
|
|
await act(async () => {
|
|
form?.requestSubmit();
|
|
await settle();
|
|
});
|
|
}
|
|
|
|
/** Opens a SelectMenu and chooses an option by its label, as a pointer would. */
|
|
async function choose(testId: string, label: string): Promise<void> {
|
|
await click(testId);
|
|
const listId = required(testId).getAttribute("aria-controls") ?? "";
|
|
const option = [...(container.querySelector(`#${CSS.escape(listId)}`)?.children ?? [])].find(
|
|
(candidate) => candidate.textContent === label,
|
|
) as HTMLElement | undefined;
|
|
if (!option) {
|
|
throw new Error(`no option ${label}`);
|
|
}
|
|
await act(async () => {
|
|
option.click();
|
|
await settle();
|
|
});
|
|
}
|
|
|
|
async function renderPage(options: ServerOptions = {}) {
|
|
const platform = server(options);
|
|
await act(async () => {
|
|
root.render(
|
|
<DisplaysSettingsPage
|
|
back={{ href: "/settings", label: "Back to Settings", onBack: () => undefined }}
|
|
config={{ apiBaseUrl, fetch: platform.fetch }}
|
|
moduleName={(moduleId) => (moduleId === "signage" ? "Signage" : undefined)}
|
|
now={() => NOW}
|
|
pollMs={5}
|
|
refreshMs={0}
|
|
/>,
|
|
);
|
|
await settle();
|
|
});
|
|
return platform;
|
|
}
|
|
|
|
describe("Settings → Displays", () => {
|
|
it("lists each active display with its status, what it shows, and its actions", async () => {
|
|
const before = document.activeElement;
|
|
await renderPage();
|
|
|
|
const card = required("device-device-1");
|
|
expect(card.textContent).toContain("Foyer TV");
|
|
expect(card.textContent).toContain("Online");
|
|
expect(required("device-device-1-showing").textContent).toBe("Showing: Nothing yet");
|
|
expect(card.textContent).toContain("Last seen within the last hour");
|
|
expect(query("device-change-device-1")?.textContent).toBe("Change");
|
|
expect(query("device-revoke-device-1")?.textContent).toBe("Remove");
|
|
expect(container.querySelector("h1")?.textContent).toBe("Displays");
|
|
// Setting up a new screen comes first, in one place, at the platform's own setup address.
|
|
const setup = required("display-setup");
|
|
expect(required("display-setup-url").textContent).toBe(`${apiBaseUrl}/display/setup`);
|
|
expect(setup.contains(required("add-display"))).toBe(true);
|
|
expect((required("display-setup-steps") as HTMLDetailsElement).open).toBe(false);
|
|
// No pairing form until someone asks for one, and nothing grabbed focus.
|
|
expect(container.querySelector("input")).toBeNull();
|
|
expect(document.activeElement).toBe(before);
|
|
});
|
|
|
|
it("shows only active displays: removed ones are history, not the normal list", async () => {
|
|
await renderPage({
|
|
devices: [
|
|
{
|
|
createdAt: "2026-10-01T09:00:00.000Z",
|
|
id: "a",
|
|
name: "Hall",
|
|
permissions: [],
|
|
revoked: false,
|
|
},
|
|
{
|
|
createdAt: "2026-09-01T09:00:00.000Z",
|
|
id: "b",
|
|
name: "Old test screen",
|
|
permissions: [],
|
|
revoked: true,
|
|
},
|
|
{
|
|
createdAt: "2026-09-02T09:00:00.000Z",
|
|
id: "c",
|
|
name: "Another old one",
|
|
permissions: [],
|
|
revoked: true,
|
|
},
|
|
],
|
|
});
|
|
expect(query("device-a")).not.toBeNull();
|
|
expect(query("device-b")).toBeNull();
|
|
expect(query("device-c")).toBeNull();
|
|
// Removed displays are never described as "revoked" in the normal interface.
|
|
expect(container.textContent).not.toMatch(/revok/iu);
|
|
expect(required("device-a").textContent).toContain("Not connected yet");
|
|
});
|
|
|
|
it("says when nothing is paired yet, and when the list is not this person's to see", async () => {
|
|
await renderPage({ devices: [] });
|
|
expect(required("device-list-empty").textContent).toContain("No displays are paired yet");
|
|
|
|
await act(async () => root.unmount());
|
|
root = createRoot(container);
|
|
await renderPage({ denied: true });
|
|
expect(container.textContent).toContain("Displays aren't available to you");
|
|
expect(query("add-display")).toBeNull();
|
|
});
|
|
|
|
it("shows a newly paired display without a page refresh, once its screen connects", async () => {
|
|
const platform = await renderPage();
|
|
|
|
await click("add-display");
|
|
expect(document.activeElement).toBe(required("device-pairing-code-input"));
|
|
await type("device-pairing-code-input", "999999");
|
|
await submit("device-pairing-lookup");
|
|
expect(required("device-pairing-error").textContent).toContain("not waiting for approval");
|
|
|
|
await type("device-pairing-code-input", MODULE_CODE);
|
|
await submit("device-pairing-lookup");
|
|
expect(required("device-pairing-found").textContent).toContain("Signage");
|
|
expect(query("device-pairing-shows")).toBeNull();
|
|
expect(document.activeElement).toBe(required("device-pairing-name"));
|
|
await type("device-pairing-name", "Sanctuary left");
|
|
await submit("device-pairing-approve");
|
|
|
|
// The dialog closes and the page says so at once; the screen has not connected yet.
|
|
expect(query("add-display-dialog")).toBeNull();
|
|
expect(required("device-pairing-success").textContent).toContain(
|
|
"Sanctuary left is paired and shows Signage.",
|
|
);
|
|
expect(required("device-awaiting").textContent).toContain("Waiting for the screen to connect");
|
|
|
|
// The page checks again on its own until the display exists — no reload.
|
|
await eventually(() => query("device-device-2") !== null);
|
|
expect(query("device-awaiting")).toBeNull();
|
|
expect(required("device-device-2-showing").textContent).toBe("Showing: Signage");
|
|
// Never twice, however many times it is fetched again.
|
|
expect(container.querySelectorAll('[data-testid="device-device-2"]')).toHaveLength(1);
|
|
expect(platform.requests.filter((r) => r.path === "/api/v1/devices").length).toBeGreaterThan(2);
|
|
expect(document.activeElement).toBe(required("add-display"));
|
|
});
|
|
|
|
it("closes the Add display flow on Cancel without pairing anything", async () => {
|
|
const platform = await renderPage();
|
|
await click("add-display");
|
|
await click("add-display-cancel");
|
|
|
|
expect(query("add-display-dialog")).toBeNull();
|
|
expect(platform.requests.map((r) => r.path).sort()).toEqual([
|
|
"/api/v1/devices",
|
|
"/api/v1/devices/display-modules",
|
|
]);
|
|
expect(document.activeElement).toBe(required("add-display"));
|
|
});
|
|
});
|
|
|
|
describe("setting up a new screen (ADR 0065)", () => {
|
|
it("copies the platform setup address, and says so when the browser cannot", async () => {
|
|
const writeText = vi.fn(async () => undefined);
|
|
Object.defineProperty(window.navigator, "clipboard", {
|
|
configurable: true,
|
|
value: { writeText },
|
|
});
|
|
await renderPage();
|
|
await click("display-setup-copy");
|
|
expect(writeText).toHaveBeenCalledWith(`${apiBaseUrl}/display/setup`);
|
|
expect(required("display-setup").textContent).toContain("Link copied.");
|
|
|
|
Object.defineProperty(window.navigator, "clipboard", { configurable: true, value: undefined });
|
|
await click("display-setup-copy");
|
|
expect(required("display-setup").textContent).toContain("couldn't copy it");
|
|
});
|
|
|
|
it("pairs a platform display with zero display modules installed", async () => {
|
|
const platform = await renderPage({ modules: [] });
|
|
expect(required("display-setup-url").textContent).toBe(`${apiBaseUrl}/display/setup`);
|
|
expect(required("display-setup").textContent).not.toContain("Signage");
|
|
|
|
await click("add-display");
|
|
await type("device-pairing-code-input", PLATFORM_CODE);
|
|
await submit("device-pairing-lookup");
|
|
expect(required("device-pairing-found").textContent).toContain(
|
|
"A new screen is waiting to be paired",
|
|
);
|
|
expect(query("device-pairing-shows")).toBeNull();
|
|
expect(required("device-pairing-no-modules").textContent).toContain(
|
|
"No installed module shows content on screens yet",
|
|
);
|
|
await type("device-pairing-name", "Foyer TV 2");
|
|
await submit("device-pairing-approve");
|
|
|
|
expect(
|
|
platform.requests.find((r) => r.path === "/api/v1/device-pairing/approvals")?.body,
|
|
).toEqual({ code: PLATFORM_CODE, deviceName: "Foyer TV 2" });
|
|
await eventually(() => query("device-device-2") !== null);
|
|
expect(required("device-device-2-showing").textContent).toBe("Showing: Nothing yet");
|
|
});
|
|
|
|
it("chooses what a new platform display shows with the themed select, not a native one", async () => {
|
|
const platform = await renderPage();
|
|
await click("add-display");
|
|
await type("device-pairing-code-input", PLATFORM_CODE);
|
|
await submit("device-pairing-lookup");
|
|
|
|
const dialog = required("add-display-dialog");
|
|
expect(dialog.querySelector("select")).toBeNull();
|
|
const shows = required("device-pairing-shows");
|
|
expect(shows.getAttribute("role")).toBe("combobox");
|
|
expect(shows.textContent).toBe("Nothing yet");
|
|
await choose("device-pairing-shows", "Signage");
|
|
expect(shows.textContent).toBe("Signage");
|
|
await type("device-pairing-name", "Lobby screen");
|
|
await submit("device-pairing-approve");
|
|
|
|
expect(
|
|
platform.requests.find((r) => r.path === "/api/v1/device-pairing/approvals")?.body,
|
|
).toEqual({ code: PLATFORM_CODE, deviceName: "Lobby screen", moduleId: "signage" });
|
|
await eventually(() => query("device-device-2") !== null);
|
|
expect(required("device-device-2-showing").textContent).toBe("Showing: Signage");
|
|
});
|
|
|
|
it("still pairs, showing nothing yet, when the display modules cannot be read", async () => {
|
|
const platform = await renderPage({ modules: "fails" });
|
|
await click("add-display");
|
|
await type("device-pairing-code-input", PLATFORM_CODE);
|
|
await submit("device-pairing-lookup");
|
|
expect(query("device-pairing-shows")).toBeNull();
|
|
await type("device-pairing-name", "Hall");
|
|
await submit("device-pairing-approve");
|
|
expect(
|
|
platform.requests.find((r) => r.path === "/api/v1/device-pairing/approvals")?.body,
|
|
).toEqual({ code: PLATFORM_CODE, deviceName: "Hall" });
|
|
// Without the module list the card neither guesses what it shows nor offers to change it.
|
|
expect(query("device-device-1-showing")).toBeNull();
|
|
expect(query("device-change-device-1")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("changing what a display shows (ADR 0065)", () => {
|
|
it("moves a display from nothing to a module, between modules, and back, as the same display", async () => {
|
|
const platform = await renderPage({ modules: [SIGNAGE, BULLETIN] });
|
|
const puts = () =>
|
|
platform.requests.filter((r) => r.method === "PUT").map((r) => [r.path, r.body]);
|
|
|
|
// Nothing yet → Signage.
|
|
await click("device-change-device-1");
|
|
const dialog = required("device-change-dialog");
|
|
expect(dialog.textContent).toContain("Change what Foyer TV shows");
|
|
expect(dialog.querySelector("select")).toBeNull();
|
|
expect(required("device-change-shows").textContent).toBe("Nothing yet");
|
|
// Nothing to save until something changes.
|
|
expect((required("device-change-save") as HTMLButtonElement).disabled).toBe(true);
|
|
await choose("device-change-shows", "Signage");
|
|
await click("device-change-save");
|
|
expect(query("device-change-dialog")).toBeNull();
|
|
expect(required("device-device-1-showing").textContent).toBe("Showing: Signage");
|
|
expect(required("device-change-success").textContent).toBe("Foyer TV now shows Signage.");
|
|
expect(document.activeElement).toBe(required("device-change-device-1"));
|
|
|
|
// Signage → Bulletin.
|
|
await click("device-change-device-1");
|
|
expect(required("device-change-shows").textContent).toBe("Signage");
|
|
await choose("device-change-shows", "Bulletin");
|
|
await click("device-change-save");
|
|
expect(required("device-device-1-showing").textContent).toBe("Showing: Bulletin");
|
|
|
|
// Bulletin → nothing.
|
|
await click("device-change-device-1");
|
|
await choose("device-change-shows", "Nothing yet");
|
|
await click("device-change-save");
|
|
expect(required("device-device-1-showing").textContent).toBe("Showing: Nothing yet");
|
|
expect(required("device-change-success").textContent).toContain("Foyer TV now shows nothing");
|
|
|
|
expect(puts()).toEqual([
|
|
["/api/v1/devices/device-1/display", { moduleId: "signage" }],
|
|
["/api/v1/devices/device-1/display", { moduleId: "bulletin" }],
|
|
["/api/v1/devices/device-1/display", { moduleId: null }],
|
|
]);
|
|
// Always the one display: no new identity, and the same name.
|
|
expect(platform.devices.map((device) => device.id)).toEqual(["device-1"]);
|
|
expect(container.querySelectorAll('[data-testid^="device-device-"]').length).toBeGreaterThan(0);
|
|
expect(container.querySelectorAll('[data-testid="device-device-1"]')).toHaveLength(1);
|
|
});
|
|
|
|
it("closes on Cancel or Escape without changing anything", async () => {
|
|
const platform = await renderPage();
|
|
await click("device-change-device-1");
|
|
await choose("device-change-shows", "Signage");
|
|
await click("device-change-cancel");
|
|
expect(query("device-change-dialog")).toBeNull();
|
|
expect(platform.requests.some((r) => r.method === "PUT")).toBe(false);
|
|
expect(required("device-device-1-showing").textContent).toBe("Showing: Nothing yet");
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Removing a display (revoking its credential) strands that screen until someone re-pairs it in
|
|
* person, so the page explains the consequence in its own dialog first — never with a
|
|
* browser-native confirm (DESIGN §21). The interface says "remove"; only the platform says "revoke".
|
|
*/
|
|
describe("removing a display", () => {
|
|
it("asks in a product dialog before removing, and does nothing on cancel", async () => {
|
|
const confirm = vi.fn(() => true);
|
|
Object.assign(window, { confirm });
|
|
const platform = await renderPage();
|
|
|
|
await click("device-revoke-device-1");
|
|
const dialog = container.querySelector('[role="dialog"]');
|
|
// Administrators remove a display; revoking its credential is the platform's word, not theirs.
|
|
expect(dialog?.textContent).toContain("Remove display?");
|
|
expect(dialog?.textContent).toContain("Foyer TV stops showing content");
|
|
expect(dialog?.textContent).not.toMatch(/revok/iu);
|
|
expect(dialog?.textContent).toContain("pairing code");
|
|
|
|
await click("device-revoke-cancel");
|
|
expect(container.querySelector('[role="dialog"]')).toBeNull();
|
|
expect(platform.requests.some((r) => r.path.endsWith("/revoke"))).toBe(false);
|
|
expect(confirm).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("takes a removed display off the list at once, and says what happened", async () => {
|
|
const platform = await renderPage();
|
|
|
|
await click("device-revoke-device-1");
|
|
await click("device-revoke-confirm");
|
|
await act(async () => settle());
|
|
|
|
expect(platform.requests.filter((r) => r.path.endsWith("/revoke"))).toHaveLength(1);
|
|
expect(container.querySelector('[role="dialog"]')).toBeNull();
|
|
// Gone from the active list without a reload, though the server keeps it as history.
|
|
expect(query("device-device-1")).toBeNull();
|
|
expect(platform.devices[0]?.revoked).toBe(true);
|
|
expect(required("device-list-empty")).not.toBeNull();
|
|
expect(required("device-outcome").textContent).toContain("Display removed");
|
|
expect(required("device-revoke-success").textContent).toContain("Foyer TV was removed");
|
|
expect(container.textContent).not.toMatch(/revok/iu);
|
|
// Its card is gone, so focus lands on what happened rather than on the page.
|
|
expect(document.activeElement).toBe(required("device-outcome"));
|
|
});
|
|
});
|