fix: stabilize terminal dashboard states

This commit is contained in:
golem
2026-08-21 03:44:42 -06:00
parent 10507e769d
commit d1a86ad162
5 changed files with 82 additions and 9 deletions
+13
View File
@@ -100,6 +100,19 @@ describe("read-only operations console", () => {
expect(screen.getByText("500.00 mUSDC")).toBeTruthy(); expect(screen.getByText("500.00 mUSDC")).toBeTruthy();
}); });
it("makes every learning abbreviation keyboard-focusable with its explanation intact", () => {
const { container } = render(<App dashboard={ready()} />);
const abbreviations = Array.from(container.querySelectorAll("abbr"));
expect(abbreviations).toHaveLength(5);
for (const abbreviation of abbreviations) {
expect(abbreviation.getAttribute("title")?.trim().length).toBeGreaterThan(0);
expect(abbreviation.getAttribute("tabindex")).toBe("0");
(abbreviation as HTMLElement).focus();
expect(document.activeElement).toBe(abbreviation);
}
});
it("renders valid V1 activity newest first beside an isolated decode warning", () => { it("renders valid V1 activity newest first beside an isolated decode warning", () => {
render(<App dashboard={ready()} />); render(<App dashboard={ready()} />);
+2 -2
View File
@@ -9,8 +9,8 @@ export function AccountingGrid({ snapshot }: { snapshot: DashboardSnapshot }) {
<p className="helper">Solvent when reserves are greater than or equal to liabilities.</p> <p className="helper">Solvent when reserves are greater than or equal to liabilities.</p>
</div> </div>
<dl className="accounting-grid"> <dl className="accounting-grid">
<div className="hero-figure"><dt><abbr title="MockUSDC held by the stable proxy address">Reserves</abbr></dt><dd>{formatAmount(snapshot.reserves)}</dd></div> <div className="hero-figure"><dt><abbr title="MockUSDC held by the stable proxy address" tabIndex={0}>Reserves</abbr></dt><dd>{formatAmount(snapshot.reserves)}</dd></div>
<div className="hero-figure"><dt><abbr title="The sum of all balances recorded in the bank ledger">Liabilities</abbr></dt><dd>{formatAmount(snapshot.liabilities)}</dd></div> <div className="hero-figure"><dt><abbr title="The sum of all balances recorded in the bank ledger" tabIndex={0}>Liabilities</abbr></dt><dd>{formatAmount(snapshot.liabilities)}</dd></div>
<div><dt>Surplus</dt><dd>{formatSurplus(snapshot.surplus)}</dd></div> <div><dt>Surplus</dt><dd>{formatSurplus(snapshot.surplus)}</dd></div>
<div><dt>Reserve ratio</dt><dd>{formatReserveRatio(snapshot.reserves, snapshot.liabilities)}</dd></div> <div><dt>Reserve ratio</dt><dd>{formatReserveRatio(snapshot.reserves, snapshot.liabilities)}</dd></div>
</dl> </dl>
+3 -3
View File
@@ -30,10 +30,10 @@ export function ContractIdentity({ manifest, snapshot }: { manifest: DeploymentM
<p className="helper">Reads target the proxy. The implementation address identifies its current logic.</p> <p className="helper">Reads target the proxy. The implementation address identifies its current logic.</p>
</div> </div>
<dl className="identity-list"> <dl className="identity-list">
<div className="proxy-row"><dt><abbr title="The stable application address that holds storage and reserves">Proxy</abbr></dt><AddressValue label="Proxy" address={snapshot.proxy} manifest={manifest} primary /></div> <div className="proxy-row"><dt><abbr title="The stable application address that holds storage and reserves" tabIndex={0}>Proxy</abbr></dt><AddressValue label="Proxy" address={snapshot.proxy} manifest={manifest} primary /></div>
<div><dt><abbr title="The replaceable contract containing the current executable logic">Implementation</abbr></dt><AddressValue label="Implementation" address={snapshot.implementation} manifest={manifest} /></div> <div><dt><abbr title="The replaceable contract containing the current executable logic" tabIndex={0}>Implementation</abbr></dt><AddressValue label="Implementation" address={snapshot.implementation} manifest={manifest} /></div>
<div><dt>Token</dt><AddressValue label="Token" address={snapshot.asset} manifest={manifest} /></div> <div><dt>Token</dt><AddressValue label="Token" address={snapshot.asset} manifest={manifest} /></div>
<div><dt><abbr title="The account authorized to pause and upgrade the proxy">Owner</abbr></dt><AddressValue label="Owner" address={snapshot.owner} manifest={manifest} /></div> <div><dt><abbr title="The account authorized to pause and upgrade the proxy" tabIndex={0}>Owner</abbr></dt><AddressValue label="Owner" address={snapshot.owner} manifest={manifest} /></div>
<div><dt>Deployment block</dt><dd>{manifest.deploymentBlock.toString()}</dd></div> <div><dt>Deployment block</dt><dd>{manifest.deploymentBlock.toString()}</dd></div>
</dl> </dl>
</section> </section>
+56 -4
View File
@@ -1,8 +1,8 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { focusManager, onlineManager, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook, waitFor } from "@testing-library/react"; import { act, renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { Address } from "viem"; import type { Address } from "viem";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DashboardSnapshot, DeploymentManifest } from "../types/dashboard"; import type { DashboardSnapshot, DeploymentManifest } from "../types/dashboard";
import { useBankDashboard } from "./useBankDashboard"; import { useBankDashboard } from "./useBankDashboard";
@@ -55,8 +55,7 @@ function deferred<T>() {
return { promise, resolve, reject }; return { promise, resolve, reject };
} }
function createWrapper() { function createWrapper(client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } })) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
return function Wrapper({ children }: { children: ReactNode }) { return function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={client}>{children}</QueryClientProvider>; return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}; };
@@ -65,6 +64,13 @@ function createWrapper() {
describe("useBankDashboard", () => { describe("useBankDashboard", () => {
beforeEach(() => { beforeEach(() => {
notifyBlock = undefined; notifyBlock = undefined;
focusManager.setFocused(true);
onlineManager.setOnline(true);
});
afterEach(() => {
focusManager.setFocused(undefined);
onlineManager.setOnline(true);
}); });
it("starts in loading while the manifest and first reconciled snapshot are pending", () => { it("starts in loading while the manifest and first reconciled snapshot are pending", () => {
@@ -151,6 +157,52 @@ describe("useBankDashboard", () => {
expect(loader).toHaveBeenCalledTimes(1); expect(loader).toHaveBeenCalledTimes(1);
}); });
it("does not retry a terminal manifest-fetch failure on focus, reconnect, or remount", async () => {
const loadManifest = vi.fn().mockRejectedValue(new Error("manifest request failed"));
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Number.POSITIVE_INFINITY } } });
const wrapper = createWrapper(client);
const options = { loadManifest, loader: vi.fn() };
const first = renderHook(() => useBankDashboard(options), { wrapper });
await waitFor(() => expect(first.result.current.status).toBe("invalid-manifest"));
await act(async () => {
focusManager.setFocused(false);
focusManager.setFocused(true);
onlineManager.setOnline(false);
onlineManager.setOnline(true);
await new Promise((resolve) => setTimeout(resolve, 0));
});
first.unmount();
const second = renderHook(() => useBankDashboard(options), { wrapper });
await waitFor(() => expect(second.result.current.status).toBe("invalid-manifest"));
expect(loadManifest).toHaveBeenCalledTimes(1);
});
it("does not retry a terminal chain mismatch on focus, reconnect, or remount", async () => {
const loader = vi.fn().mockRejectedValue(new Error(
"endpoint chain ID 84532 does not match manifest chain ID 31337",
));
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Number.POSITIVE_INFINITY } } });
const wrapper = createWrapper(client);
const options = { loadManifest: async () => manifestJson, loader };
const first = renderHook(() => useBankDashboard(options), { wrapper });
await waitFor(() => expect(first.result.current.status).toBe("chain-mismatch"));
await act(async () => {
focusManager.setFocused(false);
focusManager.setFocused(true);
onlineManager.setOnline(false);
onlineManager.setOnline(true);
await new Promise((resolve) => setTimeout(resolve, 0));
});
first.unmount();
const second = renderHook(() => useBankDashboard(options), { wrapper });
await waitFor(() => expect(second.result.current.status).toBe("chain-mismatch"));
expect(loader).toHaveBeenCalledTimes(1);
});
it("reconciles exactly once when a new watched block arrives", async () => { it("reconciles exactly once when a new watched block arrives", async () => {
const nextSnapshot = { ...snapshot, blockNumber: 13n, synchronizedAt: new Date("2026-08-21T10:01:00.000Z") }; const nextSnapshot = { ...snapshot, blockNumber: 13n, synchronizedAt: new Date("2026-08-21T10:01:00.000Z") };
const loader = vi.fn().mockResolvedValueOnce(snapshot).mockResolvedValueOnce(nextSnapshot); const loader = vi.fn().mockResolvedValueOnce(snapshot).mockResolvedValueOnce(nextSnapshot);
+8
View File
@@ -54,7 +54,11 @@ export function useBankDashboard(options: BankDashboardOptions = {}): DashboardS
queryKey: manifestQueryKey, queryKey: manifestQueryKey,
queryFn: loadManifest, queryFn: loadManifest,
retry: false, retry: false,
retryOnMount: false,
staleTime: Number.POSITIVE_INFINITY, staleTime: Number.POSITIVE_INFINITY,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}); });
const parsedManifest = useMemo(() => { const parsedManifest = useMemo(() => {
@@ -79,6 +83,10 @@ export function useBankDashboard(options: BankDashboardOptions = {}): DashboardS
}, },
enabled: manifest !== undefined, enabled: manifest !== undefined,
retry: false, retry: false,
retryOnMount: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}); });
const mismatch = snapshotQuery.error !== null && isChainMismatch(snapshotQuery.error); const mismatch = snapshotQuery.error !== null && isChainMismatch(snapshotQuery.error);
const latestSnapshot = useRef<DashboardSnapshot | undefined>(undefined); const latestSnapshot = useRef<DashboardSnapshot | undefined>(undefined);