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.
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { render, screen, waitFor } from "@testing-library/react";
|
|
import { QueryClientProvider } from "@tanstack/react-query";
|
|
import { createQueryClient } from "@/lib/queryClient";
|
|
import DefinitionList from "@/ui/DefinitionList";
|
|
import ConfigLockIndicator from "./ConfigLockIndicator";
|
|
import { CONFIG_PATH, DATABASE, MANAGED_FILE, stubApi } from "./testFixtures";
|
|
import type { ConfigStatus } from "@/lib/types";
|
|
|
|
/**
|
|
* The two shared pieces the redesign adds: the file-mode rendering of a scalar
|
|
* and the compact lock a configuration control outside a configuration page
|
|
* carries.
|
|
*/
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function renderIndicator(status: ConfigStatus | "failed") {
|
|
if (status === "failed") {
|
|
stubApi(DATABASE, {
|
|
responses: {
|
|
"GET /api/config/status": new Response(JSON.stringify({ error: "unavailable" }), { status: 503 }),
|
|
},
|
|
});
|
|
} else {
|
|
stubApi(status);
|
|
}
|
|
render(
|
|
<QueryClientProvider client={createQueryClient()}>
|
|
<ConfigLockIndicator />
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
test("a definition names the value in words and the file key beside it", () => {
|
|
render(
|
|
<DefinitionList
|
|
items={[
|
|
{ label: "Port", zonKey: "dns.port", value: "53" },
|
|
{ label: "Authentication", value: "required" },
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
const port = screen.getByText("Port");
|
|
expect(port.tagName).toBe("DT");
|
|
expect(port.textContent).toBe("Portdns.port");
|
|
// A derived value has no key: inventing one would send the reader looking
|
|
// for a line that is not in the file.
|
|
expect(screen.getByText("Authentication").textContent).toBe("Authentication");
|
|
expect(screen.getByText("required").tagName).toBe("DD");
|
|
});
|
|
|
|
test("the lock is silent when the database owns the configuration", async () => {
|
|
renderIndicator(DATABASE);
|
|
|
|
await waitFor(() => expect(screen.queryByText("Locked")).toBeNull());
|
|
});
|
|
|
|
test("under file authority the lock names the file, in words a reader hears", async () => {
|
|
renderIndicator(MANAGED_FILE);
|
|
|
|
// The reason is visible text beside the tag, not a title and not a label: a
|
|
// tooltip reaches neither a keyboard nor a touch screen, and screen-reader-only
|
|
// text is the same failure pointed the other way.
|
|
const lock = await screen.findByText("Locked");
|
|
await waitFor(() =>
|
|
expect(screen.getByText(`Managed by ${CONFIG_PATH}; edit the file and restart nxdns`)).toBeTruthy(),
|
|
);
|
|
expect(lock.getAttribute("title")).toBeNull();
|
|
expect(lock.getAttribute("aria-label")).toBeNull();
|
|
});
|
|
|
|
test("an unanswered status still locks, and says that is why", async () => {
|
|
renderIndicator("failed");
|
|
|
|
await screen.findByText("Locked");
|
|
await waitFor(() => expect(screen.getByText(/^Configuration status unavailable/)).toBeTruthy());
|
|
});
|