admin: title-only information becomes visible text
the client address follows its name as visible muted text in the query tables, the config lock indicator prints its reason beside the tag except in table rows where a page-level note explains the lock instead, and the locked delete buttons describe themselves through that one visible note. the chart legend tooltip is deleted because a named client is deliberately not addressed in the chart, and the dead series address field went with it. titles that merely repeat visible copyable text stay.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { ClientName, type ClientNames } from "./clientNames";
|
||||
import { BASE, MANAGED_FILE, NEVER, renderClientsPage, setConfigStatus } from "./testFixtures";
|
||||
import { BASE, CLIENTS, MANAGED_FILE, NEVER, renderClientsPage, setConfigStatus } from "./testFixtures";
|
||||
|
||||
/** The text of the elements an input points at with `aria-describedby`. */
|
||||
function describedText(input: HTMLElement): string {
|
||||
@@ -187,9 +187,15 @@ test("an unknown group id filters to nothing and offers a way out", async () =>
|
||||
expect(router.state.location.search).toEqual({});
|
||||
});
|
||||
|
||||
// Both statuses lock the declared delete, but only file authority proves the
|
||||
// file declares the row; the anchors keep the two sentences apart.
|
||||
const DECLARED_NOTE = /^This client is declared in the configuration file/;
|
||||
const UNKNOWN_NOTE = /^nxdns cannot say whether this client is declared/;
|
||||
test("file mode drops every edit affordance and keeps the observed delete live (R2-4)", async () => {
|
||||
await renderClientsPage({ ...BASE, "GET /api/config/status": MANAGED_FILE });
|
||||
await screen.findAllByLabelText(/Managed by \/etc\/nxdns\/config\.zon/);
|
||||
// The settled sentence, not the tag: "Locked" is already on screen while
|
||||
// authority is pending, so waiting on it would not wait for this status.
|
||||
await screen.findAllByText(/^Managed by \/etc\/nxdns\/config\.zon/);
|
||||
|
||||
expect(screen.queryAllByRole("button", { name: "Edit" })).toEqual([]);
|
||||
|
||||
@@ -197,6 +203,34 @@ test("file mode drops every edit affordance and keeps the observed delete live (
|
||||
const observed = clientRow("192.168.1.11");
|
||||
expect((within(declared).getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((within(observed).getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(false);
|
||||
|
||||
// Why the locked Delete will not answer, in visible text and exactly once:
|
||||
// per row it would repeat down the whole page, and on the button it was a
|
||||
// title that a keyboard and a touch screen never reached.
|
||||
expect(screen.getAllByText(DECLARED_NOTE, { selector: "p" })).toHaveLength(1);
|
||||
expect(within(declared).queryByText(DECLARED_NOTE, { selector: "p" })).toBeNull();
|
||||
// The description stays on the button itself too, for a reader on that control.
|
||||
const locked = within(declared).getByRole("button", { name: "Delete" });
|
||||
expect(document.getElementById(locked.getAttribute("aria-describedby") ?? "")?.textContent).toMatch(DECLARED_NOTE);
|
||||
});
|
||||
|
||||
test("an all-observed page still says why Edit is gone, with no delete note to carry it", async () => {
|
||||
// Every row observed, so no Delete is locked. The edit lock is still real, and
|
||||
// "Locked" appearing with nothing to explain it is the failure this guards.
|
||||
const observedOnly = { clients: [CLIENTS.clients[1]] };
|
||||
await renderClientsPage({
|
||||
...BASE,
|
||||
"GET /api/clients": observedOnly,
|
||||
"GET /api/config/status": MANAGED_FILE,
|
||||
});
|
||||
// The settled sentence is both the anchor and the assertion: it is the whole
|
||||
// explanation for the missing Edit action.
|
||||
await screen.findAllByText(/^Managed by \/etc\/nxdns\/config\.zon/);
|
||||
expect(await screen.findAllByText("Locked")).not.toHaveLength(0);
|
||||
|
||||
// The delete sentence belongs only to a row that has one.
|
||||
expect(screen.queryByText(DECLARED_NOTE)).toBeNull();
|
||||
expect((screen.getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(false);
|
||||
});
|
||||
|
||||
test("file mode renders network assignments with no mutation control at all (R2-4)", async () => {
|
||||
@@ -217,7 +251,7 @@ test("file mode renders network assignments with no mutation control at all (R2-
|
||||
|
||||
test("a failed config status exposes no configuration mutation, and still deletes an observed client (R3-4)", async () => {
|
||||
await renderClientsPage({ ...BASE, "GET /api/config/status": undefined });
|
||||
await screen.findAllByLabelText(/Configuration status unavailable/);
|
||||
await screen.findAllByText(/^Configuration status unavailable/);
|
||||
|
||||
expect(screen.queryAllByRole("button", { name: "Edit" })).toEqual([]);
|
||||
expect(screen.queryByRole("button", { name: "Save assignments" })).toBeNull();
|
||||
@@ -233,10 +267,6 @@ test("a failed config status exposes no configuration mutation, and still delete
|
||||
// confirmation is already open. `undefined` is the failed status: the fetch stub
|
||||
// answers 404 for a key it does not hold.
|
||||
//
|
||||
// Both statuses lock the declared delete, but only file authority proves the
|
||||
// file declares the row; the anchors keep the two sentences apart.
|
||||
const DECLARED_NOTE = /^This client is declared in the configuration file/;
|
||||
const UNKNOWN_NOTE = /^nxdns cannot say whether this client is declared/;
|
||||
describe.each([
|
||||
["file authority", MANAGED_FILE, DECLARED_NOTE, UNKNOWN_NOTE],
|
||||
["a failed status", undefined, UNKNOWN_NOTE, DECLARED_NOTE],
|
||||
@@ -259,7 +289,7 @@ describe.each([
|
||||
expect(within(dialog).queryByRole("button", { name: "Save" })).toBeNull();
|
||||
expect((within(dialog).getByLabelText("Name") as HTMLInputElement).value).toBe("laptop");
|
||||
expect(within(dialog).getByText(/can no longer be saved/)).toBeTruthy();
|
||||
expect(within(dialog).getByLabelText(/^Locked\./)).toBeTruthy();
|
||||
expect(within(dialog).getByText("Locked")).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
@@ -284,7 +314,7 @@ describe.each([
|
||||
await waitFor(() => expect(within(dialog).queryByRole("button", { name: "Delete" })).toBeNull());
|
||||
expect(within(dialog).getByText(lockNote)).toBeTruthy();
|
||||
expect(within(dialog).queryByText(otherNote)).toBeNull();
|
||||
expect(within(dialog).getByLabelText(/^Locked\./)).toBeTruthy();
|
||||
expect(within(dialog).getByText("Locked")).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
|
||||
@@ -364,7 +394,7 @@ test("a pending config status holds the same line as a failed one (R3-4)", async
|
||||
// The status request never settles, so authority stays pending for the whole
|
||||
// test: nothing configuration owns may be offered on that guess.
|
||||
await renderClientsPage({ ...BASE, "GET /api/config/status": NEVER });
|
||||
await screen.findAllByLabelText(/Checking which configuration source/);
|
||||
await screen.findAllByText(/^Checking which configuration source/);
|
||||
|
||||
expect(screen.queryAllByRole("button", { name: "Edit" })).toEqual([]);
|
||||
expect(screen.queryByRole("button", { name: "Save assignments" })).toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user