admin: ui polish pass, thirty findings from the emil audit
Gates / frontend (push) Failing after 1m6s
Gates / package (push) Skipped
Gates / container (push) Skipped
Gates / test (push) Failing after 3m29s
Gates / test-aarch64 (push) Successful in 9m53s
CI / gates (push) Failing after 13m23s

selected states stop changing font weight, buttons gain a pressed scale and scoped 120ms transitions with a reduced-motion override, every loading and empty state reserves its height, charts measure before first paint, hit targets rise to the 44px enhanced target where layout permits, long domains clamp to two lines on an unpadded inner span, chips and name cells truncate, tabular figures on counts and time columns, a z-index layer scale replaces magic numbers and fixes the dialog-over-confirm tie, history's empty state gains a clear-filters action, page headings balance, font smoothing and color-scheme land on the html reset.
This commit is contained in:
2026-09-01 23:23:07 +02:00
parent 2ae0c974a4
commit e656670dd4
30 changed files with 399 additions and 82 deletions
@@ -146,6 +146,26 @@ function submitFilters() {
fireEvent.submit(domainInput().closest("form")!);
}
/**
* The footer's count line, matched on the whole sentence.
*
* The number sits in a span of its own so it can carry tabular digits, so the
* line is several text nodes and the default string matcher — which reads one
* node at a time — cannot see it whole.
*/
function countMatcher(pattern: RegExp) {
return (_: string, element: Element | null): boolean =>
element?.tagName === "P" && pattern.test(element.textContent ?? "");
}
function countLine(pattern: RegExp): HTMLElement {
return screen.getByText(countMatcher(pattern));
}
function findCountLine(pattern: RegExp): Promise<HTMLElement> {
return screen.findByText(countMatcher(pattern));
}
/** The custom range lives behind the Time menu; the two bounds only exist there. */
function openCustomRange() {
fireEvent.click(screen.getByRole("button", { name: /^Time: / }));
@@ -176,7 +196,7 @@ test("renders the first page with the seven columns filled in", async () => {
// Route cell says how: this is the pair the old Status column could not show.
expect(within(blocked).getAllByText("Blocked")).toHaveLength(2);
expect(within(blocked).getByText("—")).toBeTruthy();
expect(screen.getByText(/Showing 2 queries/)).toBeTruthy();
expect(countLine(/Showing 2 queries/)).toBeTruthy();
});
test("resolves each row's client to its display name, reading the IP out with it", async () => {
@@ -225,7 +245,7 @@ test("load more appends the next page and stops at the end of the log", async ()
await screen.findByText("older.example");
expect(screen.getByText("first.example")).toBeTruthy();
expect(screen.getByText(/Showing 3 queries — end of log/)).toBeTruthy();
expect(countLine(/Showing 3 queries — end of log/)).toBeTruthy();
expect(screen.queryByRole("button", { name: "Load more" })).toBeNull();
});
@@ -239,7 +259,7 @@ test("applying a filter puts it in the url, refetches, and resets the accumulate
fireEvent.change(domainInput(), { target: { value: "ads" } });
submitFilters();
await screen.findByText(/Showing 1 query /);
await findCountLine(/Showing 1 query /);
expect(history.location.search).toContain("domain=ads");
expect(screen.getByText("ads.example")).toBeTruthy();
expect(screen.queryByText("first.example")).toBeNull();
@@ -264,7 +284,7 @@ test("a load-more that resolves after a filter change is discarded", async () =>
fireEvent.change(domainInput(), { target: { value: "ads" } });
submitFilters();
await screen.findByText(/Showing 1 query /);
await findCountLine(/Showing 1 query /);
releaseLoadMore();
await act(async () => {
@@ -272,7 +292,7 @@ test("a load-more that resolves after a filter change is discarded", async () =>
});
expect(screen.queryByText("older.example")).toBeNull();
expect(screen.getByText(/Showing 1 query /)).toBeTruthy();
expect(countLine(/Showing 1 query /)).toBeTruthy();
expect(screen.queryByRole("alert")).toBeNull();
});
@@ -320,7 +340,7 @@ test("load more is disabled while a filter change shows placeholder data, then u
await screen.findByText("ads.older.example");
expect(queryCalls()).toContain("/api/queries?domain=ads&before=7");
expect(screen.getByText(/Showing 2 queries — end of log/)).toBeTruthy();
expect(countLine(/Showing 2 queries — end of log/)).toBeTruthy();
});
test("a background refetch after new rows arrive leaves no gap between the loaded pages", async () => {
@@ -371,7 +391,7 @@ test("a background refetch after new rows arrive leaves no gap between the loade
await screen.findByText("n22.example");
const shown = screen.getAllByText(/^n\d+\.example$/).map((cell) => cell.textContent);
expect(shown).toEqual(["n22.example", "n21.example", "n20.example", "n19.example", "n18.example", "n17.example"]);
expect(screen.getByText(/Showing 6 queries — end of log/)).toBeTruthy();
expect(countLine(/Showing 6 queries — end of log/)).toBeTruthy();
});
test("a 401 on load more routes through handleUnauthorized instead of the inline error", async () => {
@@ -619,6 +639,38 @@ test("Clear empties the url as well as the form", async () => {
expect(domainInput()).toHaveProperty("value", "");
});
test("the empty result offers the clear it names, and it clears the same filters the toolbar does", async () => {
stubFetch((url) => {
if (url === "/api/clients") return json({ clients: CLIENTS });
if (url === "/api/queries") return json(PAGES["/api/queries"]);
return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage);
});
const { history } = renderPage("/activity?mode=history&domain=nothing&blocked=true");
await screen.findByText("No queries match the current filters.");
fireEvent.click(screen.getByRole("button", { name: "Clear filters" }));
await waitFor(() => {
expect(history.location.search).not.toContain("domain");
});
expect(history.location.search).not.toContain("blocked");
expect(domainInput()).toHaveProperty("value", "");
// The unfiltered log is back, so the button did the toolbar's Clear and not a
// reset of its own that only emptied the form.
expect(await screen.findByText("first.example")).toBeTruthy();
});
test("an empty log offers no clear, because there is no filter to blame for it", async () => {
stubFetch((url) => {
if (url === "/api/clients") return json({ clients: CLIENTS });
return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage);
});
renderPage();
await screen.findByText("No queries logged yet.");
expect(screen.queryByRole("button", { name: "Clear filters" })).toBeNull();
});
test("the domain field debounces into the url, and Enter flushes it at once", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
try {
@@ -739,7 +791,7 @@ test("the coverage watermark reads under the results, never over them", async ()
await screen.findByText("kept.example");
const watermark = screen.getByText(/Query history is available from/);
const count = screen.getByText(/Showing 1 query/);
const count = countLine(/Showing 1 query/);
// After the count in document order, which is what "footer" means here.
expect(count.compareDocumentPosition(watermark) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});