fix(diagnostics): honest live console after review
- Live stops polling on a failed read and marks the list as possibly stale (with Retry), instead of re-raising the error dialog every tick. - While live, a refetch reads the newest page only, whatever the cache holds (a late "Load older", a revisited filter set). - Copy is disabled and the list dimmed while a filter change loads. - Dates follow the UI language; the pivot chip uses the shared .chip. - Tests: query-string wire test, Copy, every facet's skip key.
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
//
|
||||
// E22.6: the admin log console. A request id narrows the list to that request (and says so, in the
|
||||
// filter row and the URL); a filter value the facets no longer count stays pickable; "Live" watches
|
||||
// the newest page only; a failed read is an error, not an empty log.
|
||||
// the newest page only, and stops on a failed read instead of re-raising it; a failed read is an
|
||||
// error, not an empty log; Copy takes every loaded line, oldest first.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import type { LogFacets, LogPage, LogQuery, ServerLogRow } from "../lib/api";
|
||||
@@ -35,6 +36,14 @@ vi.mock("../lib/api", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const copied: string[] = [];
|
||||
vi.mock("../lib/clipboard", () => ({
|
||||
copyText: (text: string) => {
|
||||
copied.push(text);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
}));
|
||||
|
||||
import AdminLogs from "./AdminLogs";
|
||||
|
||||
function row(id: number, extra: Partial<ServerLogRow> = {}): ServerLogRow {
|
||||
@@ -75,6 +84,9 @@ function renderConsole() {
|
||||
);
|
||||
}
|
||||
|
||||
// react-query hands its updates to React on a (real) timer tick, after the promise that caused them.
|
||||
const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 20)));
|
||||
|
||||
const lines = () =>
|
||||
within(screen.getByTestId("admin-logs-lines"))
|
||||
.queryAllByRole("button")
|
||||
@@ -85,6 +97,7 @@ beforeEach(() => {
|
||||
facetsMock.mockResolvedValue(FACETS);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
cleanup();
|
||||
logsMock.mockReset();
|
||||
facetsMock.mockReset();
|
||||
@@ -142,6 +155,69 @@ describe("AdminLogs", () => {
|
||||
).toBe("true");
|
||||
});
|
||||
|
||||
it("a page that lands after Live went on is dropped at the next tick", async () => {
|
||||
let landOlder!: (page: LogPage) => void;
|
||||
logsMock.mockImplementation((_q, before) =>
|
||||
before === null
|
||||
? Promise.resolve({ items: [row(3), row(2)], accounts: {}, next_cursor: "c1" })
|
||||
: new Promise<LogPage>((resolve) => (landOlder = resolve)),
|
||||
);
|
||||
renderConsole();
|
||||
await waitFor(() => expect(lines()).toHaveLength(2));
|
||||
fireEvent.click(screen.getByRole("button", { name: "adminLogs.older" }));
|
||||
await waitFor(() => expect(landOlder).toBeTypeOf("function"));
|
||||
|
||||
vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] });
|
||||
fireEvent.click(screen.getByRole("button", { name: "adminLogs.live" }));
|
||||
await act(async () => landOlder({ items: [row(1)], accounts: {}, next_cursor: null }));
|
||||
await settle();
|
||||
expect(lines()).toHaveLength(3);
|
||||
const olderReads = logsMock.mock.calls.filter(([, before]) => before !== null).length;
|
||||
|
||||
await act(async () => vi.advanceTimersByTimeAsync(4000));
|
||||
await settle();
|
||||
|
||||
expect(lines()).toHaveLength(2);
|
||||
expect(logsMock.mock.calls.filter(([, before]) => before !== null)).toHaveLength(olderReads);
|
||||
});
|
||||
|
||||
it("a failed live read stops the polling and says the list may be old", async () => {
|
||||
logsMock.mockResolvedValueOnce({ items: [row(1)], accounts: {}, next_cursor: null });
|
||||
renderConsole();
|
||||
await waitFor(() => expect(lines()).toHaveLength(1));
|
||||
|
||||
logsMock.mockRejectedValue(new Error("500"));
|
||||
vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] });
|
||||
fireEvent.click(screen.getByRole("button", { name: "adminLogs.live" }));
|
||||
await act(async () => vi.advanceTimersByTimeAsync(4000));
|
||||
await settle();
|
||||
|
||||
expect(screen.getByRole("alert").textContent).toContain("adminLogs.stale");
|
||||
expect(lines()).toHaveLength(1);
|
||||
const reads = logsMock.mock.calls.length;
|
||||
await act(async () => vi.advanceTimersByTimeAsync(12000));
|
||||
expect(logsMock.mock.calls.length).toBe(reads);
|
||||
});
|
||||
|
||||
it("copies every loaded line, oldest first", async () => {
|
||||
logsMock.mockImplementation(async (_q, before) =>
|
||||
before === null
|
||||
? { items: [row(3), row(2)], accounts: {}, next_cursor: "c1" }
|
||||
: { items: [row(1)], accounts: {}, next_cursor: null },
|
||||
);
|
||||
renderConsole();
|
||||
await waitFor(() => expect(lines()).toHaveLength(2));
|
||||
fireEvent.click(screen.getByRole("button", { name: "adminLogs.older" }));
|
||||
await waitFor(() => expect(lines()).toHaveLength(3));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "adminLogs.copy" }));
|
||||
});
|
||||
|
||||
const ids = copied[copied.length - 1]!.split("\n").map((l) => JSON.parse(l).id);
|
||||
expect(ids).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("the keyboard can walk the lines and open one (NavGrid items)", async () => {
|
||||
logsMock.mockResolvedValue({ items: [row(2), row(1)], accounts: {}, next_cursor: null });
|
||||
renderConsole();
|
||||
|
||||
Reference in New Issue
Block a user