admin: live query detail becomes a modal, live/history and period pickers become tabs and radios, client delete confirms in a dialog
the streamed-query detail panel is now a rac modal dialog with focus containment and restore. the live/history switch is rac tabs driven by the url, the overview period picker is a rac radio group, and the clients delete flow uses the shared confirm dialog; an authority turn keeps the dialog open and withdraws only the destructive action.
This commit is contained in:
@@ -73,6 +73,39 @@ test("the address links to the client's detail page", async () => {
|
||||
expect(link.getAttribute("href")).toBe("/clients/1");
|
||||
});
|
||||
|
||||
test("delete asks first, naming the row, and the confirmation carries out the delete", async () => {
|
||||
const { fetchMock } = await renderClientsPage({ ...BASE, "DELETE /api/clients/2": {} });
|
||||
|
||||
fireEvent.click(within(clientRow("192.168.1.11")).getByRole("button", { name: "Delete" }));
|
||||
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete client" });
|
||||
// The operator has to be able to tell from the dialog alone which row this is.
|
||||
expect(within(dialog).getByText(/kids-tablet\.lan \(192\.168\.1\.11\)/)).toBeTruthy();
|
||||
expect(within(dialog).getByText(/re-materialize on their next DNS query/)).toBeTruthy();
|
||||
// Asking is not deleting.
|
||||
expect(fetchMock.mock.calls.filter(([, init]) => init?.method === "DELETE")).toEqual([]);
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Delete" }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
fetchMock.mock.calls.filter(([input, init]) => init?.method === "DELETE" && String(input).endsWith("/2")),
|
||||
).toHaveLength(1),
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
|
||||
});
|
||||
|
||||
test("cancelling the confirmation keeps the client", async () => {
|
||||
const { fetchMock } = await renderClientsPage({ ...BASE, "DELETE /api/clients/2": {} });
|
||||
|
||||
fireEvent.click(within(clientRow("192.168.1.11")).getByRole("button", { name: "Delete" }));
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete client" });
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
|
||||
expect(screen.getByText("192.168.1.11")).toBeTruthy();
|
||||
expect(fetchMock.mock.calls.filter(([, init]) => init?.method === "DELETE")).toEqual([]);
|
||||
});
|
||||
|
||||
test("shows the DNS-activity empty state when there are no clients", async () => {
|
||||
await renderClientsPage({ ...BASE, "GET /api/clients": { clients: [] } });
|
||||
|
||||
@@ -218,7 +251,7 @@ describe.each([
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
});
|
||||
|
||||
test("locks an open declared-client delete confirmation and leaves cancel working", async () => {
|
||||
test("locks an open declared-client delete confirmation in place and leaves cancel working", async () => {
|
||||
const map = { ...BASE };
|
||||
const { queryClient, fetchMock } = await renderClientsPage(map);
|
||||
// The Edit affordance appearing is the proof authority resolved to
|
||||
@@ -227,23 +260,66 @@ describe.each([
|
||||
|
||||
const declared = clientRow("192.168.1.10");
|
||||
fireEvent.click(within(declared).getByRole("button", { name: "Delete" }));
|
||||
expect(within(clientRow("192.168.1.10")).getByRole("button", { name: "Confirm delete" })).toBeTruthy();
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete client" });
|
||||
|
||||
await setConfigStatus(map, queryClient, status);
|
||||
|
||||
const confirming = clientRow("192.168.1.10");
|
||||
expect(within(confirming).queryByRole("button", { name: "Confirm delete" })).toBeNull();
|
||||
expect(within(confirming).getByText(lockNote)).toBeTruthy();
|
||||
expect(within(confirming).queryByText(otherNote)).toBeNull();
|
||||
expect(within(confirming).getByLabelText(/^Locked\./)).toBeTruthy();
|
||||
// The dialog stays put. Closing it would throw focus at the Delete button
|
||||
// the same turn disabled, and the reason would survive only as a title
|
||||
// attribute; here the reason is the dialog's own message.
|
||||
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();
|
||||
|
||||
fireEvent.click(within(confirming).getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() =>
|
||||
expect(within(clientRow("192.168.1.10")).getByRole("button", { name: "Delete" })).toBeTruthy(),
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
|
||||
expect(fetchMock.mock.calls.filter(([, init]) => init?.method === "DELETE")).toEqual([]);
|
||||
});
|
||||
|
||||
test("a cancelled confirmation stays closed when authority comes back", async () => {
|
||||
const map = { ...BASE };
|
||||
const { queryClient } = await renderClientsPage(map);
|
||||
await unlockedEdit(0);
|
||||
|
||||
fireEvent.click(within(clientRow("192.168.1.10")).getByRole("button", { name: "Delete" }));
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete client" });
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
|
||||
|
||||
// A poll that fails and then recovers must not raise a destructive
|
||||
// question the operator already answered.
|
||||
await setConfigStatus(map, queryClient, status);
|
||||
await setConfigStatus(map, queryClient, BASE["GET /api/config/status"]);
|
||||
|
||||
await waitFor(() => expect(screen.getAllByRole("button", { name: "Edit" }).length).toBeGreaterThan(0));
|
||||
expect(screen.queryByRole("alertdialog")).toBeNull();
|
||||
});
|
||||
|
||||
test("the confirm action comes back when authority does, without a second prompt", async () => {
|
||||
const map = { ...BASE, "DELETE /api/clients/1": {} };
|
||||
const { queryClient, fetchMock } = await renderClientsPage(map);
|
||||
await unlockedEdit(0);
|
||||
|
||||
fireEvent.click(within(clientRow("192.168.1.10")).getByRole("button", { name: "Delete" }));
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete client" });
|
||||
|
||||
await setConfigStatus(map, queryClient, status);
|
||||
await waitFor(() => expect(within(dialog).queryByRole("button", { name: "Delete" })).toBeNull());
|
||||
|
||||
await setConfigStatus(map, queryClient, BASE["GET /api/config/status"]);
|
||||
|
||||
// The question was never withdrawn, so the answer returns to the same
|
||||
// dialog rather than asking the operator to start again.
|
||||
const confirm = await within(dialog).findByRole("button", { name: "Delete" });
|
||||
fireEvent.click(confirm);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
fetchMock.mock.calls.filter(([input, init]) => init?.method === "DELETE" && String(input).endsWith("/1")),
|
||||
).toHaveLength(1),
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps an open observed-client delete confirmation live (R3-4)", async () => {
|
||||
const map = { ...BASE, "DELETE /api/clients/2": {} };
|
||||
const { queryClient, fetchMock } = await renderClientsPage(map);
|
||||
@@ -253,10 +329,11 @@ describe.each([
|
||||
|
||||
const observed = clientRow("192.168.1.11");
|
||||
fireEvent.click(within(observed).getByRole("button", { name: "Delete" }));
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete client" });
|
||||
|
||||
await setConfigStatus(map, queryClient, status);
|
||||
|
||||
fireEvent.click(within(clientRow("192.168.1.11")).getByRole("button", { name: "Confirm delete" }));
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Delete" }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
fetchMock.mock.calls.filter(
|
||||
|
||||
Reference in New Issue
Block a user