Files
fusion/packages/dashboard/app/hooks/__tests__/useUsageData.test.ts
gsxdsm cf3b9a575e FN-5886: show empty usage state after empty fetch
Handle empty usage responses without leaving the panel in its loading skeleton.

- add a hasFetched flag to useUsageData so the UI can distinguish initial loading from an empty completed response
- update UsageIndicator to render the skeleton only before the first fetch completes and show the empty state afterward
- refresh dashboard hook and component tests to cover empty fetches, refresh behavior, and the new loading semantics

Files changed:
 .../dashboard/app/components/UsageIndicator.tsx    |  19 +-
 .../components/__tests__/UsageIndicator.test.tsx   | 395 +++++++++++----------
 .../app/hooks/__tests__/useUsageData.test.ts       |  42 ++-
 packages/dashboard/app/hooks/useUsageData.ts       |   5 +
 4 files changed, 245 insertions(+), 216 deletions(-)

Fusion-Task-Id: FN-5886

Fusion-Task-Lineage: 8fa901a9-3167-4e33-a082-e5de95f692da
2026-06-02 09:24:42 -07:00

133 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { act, renderHook, waitFor } from "@testing-library/react";
import { useUsageData } from "../useUsageData";
import * as api from "../../api";
describe("useUsageData", () => {
const mockFetchUsageData = vi.spyOn(api, "fetchUsageData");
beforeEach(() => {
mockFetchUsageData.mockReset();
});
it("fetches data on initial mount", async () => {
const mockData = {
providers: [
{
name: "Claude",
icon: "🟠",
status: "ok" as const,
windows: [],
},
],
};
mockFetchUsageData.mockResolvedValue(mockData);
const { result } = renderHook(() => useUsageData({ autoRefresh: false }));
expect(result.current.loading).toBe(true);
expect(result.current.providers).toEqual([]);
expect(result.current.hasFetched).toBe(false);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.providers).toEqual(mockData.providers);
expect(result.current.error).toBeNull();
expect(result.current.lastUpdated).toBeInstanceOf(Date);
expect(result.current.hasFetched).toBe(true);
});
it("handles fetch errors", async () => {
mockFetchUsageData.mockRejectedValue(new Error("Network error"));
const { result } = renderHook(() => useUsageData({ autoRefresh: false }));
expect(result.current.hasFetched).toBe(false);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Network error");
expect(result.current.providers).toEqual([]);
expect(result.current.hasFetched).toBe(true);
});
it("manual refresh fetches new data", async () => {
const mockData1 = {
providers: [{ name: "Claude", icon: "🟠", status: "ok" as const, windows: [] }],
};
const mockData2 = {
providers: [{ name: "Codex", icon: "🟢", status: "ok" as const, windows: [] }],
};
mockFetchUsageData
.mockResolvedValueOnce(mockData1)
.mockResolvedValueOnce(mockData2);
const { result } = renderHook(() => useUsageData({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.providers).toEqual(mockData1.providers);
expect(result.current.hasFetched).toBe(true);
await act(async () => {
await result.current.refresh();
});
await waitFor(() => expect(result.current.providers).toEqual(mockData2.providers));
expect(result.current.hasFetched).toBe(true);
});
it("clears error on successful manual refresh after error", async () => {
mockFetchUsageData
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce({
providers: [{ name: "Claude", icon: "🟠", status: "ok" as const, windows: [] }],
});
const { result } = renderHook(() => useUsageData({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Network error");
expect(result.current.hasFetched).toBe(true);
await act(async () => {
await result.current.refresh();
});
await waitFor(() => expect(result.current.error).toBeNull());
expect(result.current.providers).toHaveLength(1);
expect(result.current.hasFetched).toBe(true);
});
it("sets hasFetched to true after a successful empty fetch", async () => {
mockFetchUsageData.mockResolvedValue({ providers: [] });
const { result } = renderHook(() => useUsageData({ autoRefresh: false }));
expect(result.current.hasFetched).toBe(false);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.providers).toEqual([]);
expect(result.current.error).toBeNull();
expect(result.current.hasFetched).toBe(true);
});
it("exports the correct interface", () => {
expect(typeof useUsageData).toBe("function");
});
it("returns expected default values before first fetch", () => {
mockFetchUsageData.mockImplementation(() => new Promise(() => {}));
const { result } = renderHook(() => useUsageData({ autoRefresh: false }));
expect(result.current.providers).toEqual([]);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeNull();
expect(result.current.lastUpdated).toBeNull();
expect(result.current.hasFetched).toBe(false);
expect(typeof result.current.refresh).toBe("function");
});
});