Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ describe("DeadLetterQueuePanel", () => {
).toBeTruthy();
});

it("REGRESSION (#8668): shows the WifiOff icon, not the generic AlertTriangle, when the fetch fails with a network errorKind", async () => {
apiFetch.mockResolvedValue({ ok: false, kind: "network", message: "fetch failed" });
const { container } = render(<DeadLetterQueuePanel />);
await screen.findByText("Couldn't load the dead-letter queue");
expect(container.querySelector(".lucide-wifi-off")).toBeTruthy();
expect(container.querySelector(".lucide-triangle-alert")).toBeNull();
});

it("keeps the generic AlertTriangle icon for a non-network (e.g. http) errorKind", async () => {
apiFetch.mockResolvedValue({ ok: false, kind: "http", message: "insufficient_role" });
const { container } = render(<DeadLetterQueuePanel />);
await screen.findByText("Couldn't load the dead-letter queue");
expect(container.querySelector(".lucide-triangle-alert")).toBeTruthy();
expect(container.querySelector(".lucide-wifi-off")).toBeNull();
});

it("expands and collapses a truncated error message", async () => {
const longError = "x".repeat(120);
apiFetch.mockResolvedValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function DeadLetterQueuePanel() {
<StateBoundary
isLoading={resource.status === "loading"}
isError={resource.status === "error" || isMalformed}
errorKind={resource.status === "error" ? resource.errorKind : undefined}
isEmpty={page !== null && page.items.length === 0}
onRetry={resource.reload}
onRefresh={resource.reload}
Expand Down
82 changes: 79 additions & 3 deletions apps/loopover-ui/src/routes/app.index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { ReactNode } from "react";

import { TooltipProvider } from "@/components/ui/tooltip";
import { SparkStat } from "./app.index";
import { AppOverview, SparkStat } from "./app.index";

vi.mock("@tanstack/react-router", () => ({
createFileRoute: () => () => ({}),
useNavigate: () => () => Promise.resolve(),
Link: ({ to, children }: { to: string; children: ReactNode }) => <a href={to}>{children}</a>,
}));

const { useSession } = vi.hoisted(() => ({ useSession: vi.fn() }));
vi.mock("@/lib/api/session", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/api/session")>();
return { ...actual, useSession: () => useSession() };
});

const { useApiResource } = vi.hoisted(() => ({ useApiResource: vi.fn() }));
vi.mock("@/lib/api/use-api-resource", () => ({
useApiResource: (...args: unknown[]) => useApiResource(...args),
}));

// #6984: SparkStat's loading branch hand-rolled its own animate-pulse divs instead of the shared
// Skeleton primitive every other loading placeholder in this app already uses.
Expand Down Expand Up @@ -39,3 +57,61 @@ describe("SparkStat loading state (#6984)", () => {
expect(screen.queryByRole("status", { name: "Loading Open PRs" })).toBeNull();
});
});

describe("AppOverview error state (#8668)", () => {
const SESSION = {
login: "octo",
roles: ["miner"] as const,
confirmed_miner: true,
};

it("REGRESSION: shows the WifiOff icon, not the generic AlertTriangle, when the overview fetch fails with a network errorKind", () => {
useSession.mockReturnValue({ session: SESSION });
useApiResource.mockReturnValue({
status: "error",
data: null,
error: "fetch failed",
errorKind: "network",
loadedAt: null,
reload: vi.fn(),
});

const { container } = render(<AppOverview />);
expect(screen.getByText("App overview is unavailable right now")).toBeTruthy();
expect(container.querySelector(".lucide-wifi-off")).toBeTruthy();
expect(container.querySelector(".lucide-triangle-alert")).toBeNull();
});

it("keeps the generic AlertTriangle icon for a non-network (e.g. http) errorKind", () => {
useSession.mockReturnValue({ session: SESSION });
useApiResource.mockReturnValue({
status: "error",
data: null,
error: "server exploded",
errorKind: "http",
loadedAt: null,
reload: vi.fn(),
});

const { container } = render(<AppOverview />);
expect(container.querySelector(".lucide-triangle-alert")).toBeTruthy();
expect(container.querySelector(".lucide-wifi-off")).toBeNull();
});

it("REGRESSION: clicking retry on the overview error state re-triggers the fetch", () => {
const reload = vi.fn();
useSession.mockReturnValue({ session: SESSION });
useApiResource.mockReturnValue({
status: "error",
data: null,
error: "fetch failed",
errorKind: "network",
loadedAt: null,
reload,
});

render(<AppOverview />);
fireEvent.click(screen.getByRole("button", { name: /try again/i }));
expect(reload).toHaveBeenCalledTimes(1);
});
});
4 changes: 3 additions & 1 deletion apps/loopover-ui/src/routes/app.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ type AppOverviewResponse = {
}>;
};

function AppOverview() {
export function AppOverview() {
const { session } = useSession();
const { status, connection } = useApiStatus();
const overview = useApiResource<AppOverviewResponse>("/v1/app/overview", "App overview");
Expand Down Expand Up @@ -175,6 +175,8 @@ function AppOverview() {
className="col-span-full"
title="App overview is unavailable right now"
description={overview.error}
errorKind={overview.errorKind}
onRetry={overview.reload}
/>
)}
{series.length === 0 ? (
Expand Down
Loading