Skip to content
Merged
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
59 changes: 40 additions & 19 deletions src/components/MetricsBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ vi.mock('@/hooks/useAsync', () => ({
useAsync: vi.fn(),
}));

type UseAsyncResult = ReturnType<typeof useAsync>;

/**
* Returns a complete, typed `useAsync` result with no-op defaults.
*
* The hook's return shape is the contract under test: if `useAsync` ever
* gains, loses or renames a member, this factory (and every call site that
* overrides it) stops compiling, instead of silently verifying the component
* against a fabricated interface like the old `{ state, refresh }` mocks did.
*/
function mockUseAsync(overrides: Partial<UseAsyncResult> = {}): UseAsyncResult {
return {
state: { status: 'loading' },
reload: vi.fn(),
refresh: vi.fn(() => Promise.resolve()),
mutate: vi.fn(),
...overrides,
};
}

describe('MetricsBar', () => {
beforeEach(() => {
vi.useFakeTimers();
Expand All @@ -18,10 +38,7 @@ describe('MetricsBar', () => {
});

it('shows a stable four-card skeleton grid before metrics resolve', () => {
vi.mocked(useAsync).mockReturnValue({
state: { status: 'loading' },
refresh: vi.fn(),
});
vi.mocked(useAsync).mockReturnValue(mockUseAsync());
render(<MetricsBar />);
expect(screen.getByText('Active anchors')).toBeInTheDocument();
expect(screen.getByText('Pools')).toBeInTheDocument();
Expand All @@ -30,24 +47,31 @@ describe('MetricsBar', () => {
});

it('keeps the auto-refresh interval on schedule', () => {
const mockReload = vi.fn();
vi.mocked(useAsync).mockReturnValue({
state: { status: 'ready', data: { activeAnchors: 50, anchors: 100, pools: 10, totalLiquidity: 500000, settlements: 1000 } },
refresh: mockReload,
});
const mockRefresh = vi.fn(() => Promise.resolve());
vi.mocked(useAsync).mockReturnValue(mockUseAsync({
state: {
status: 'ready',
data: {
activeAnchors: 50,
anchors: 100,
pools: 10,
totalLiquidity: 500000,
settlements: 1000,
pendingSettlements: 4,
},
},
refresh: mockRefresh,
}));
render(<MetricsBar />);
act(() => {
vi.advanceTimersByTime(30000);
});
expect(mockReload).toHaveBeenCalled();
expect(mockRefresh).toHaveBeenCalled();
});

it('handles unmount mid-refresh without warnings', () => {
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.mocked(useAsync).mockReturnValue({
state: { status: 'loading' },
refresh: vi.fn(),
});
vi.mocked(useAsync).mockReturnValue(mockUseAsync());
const { unmount } = render(<MetricsBar />);
act(() => {
unmount();
Expand All @@ -61,10 +85,7 @@ describe('MetricsBar', () => {

it('does not update state after unmount when interval-triggered refresh resolves', () => {
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.mocked(useAsync).mockReturnValue({
state: { status: 'loading' },
refresh: vi.fn(),
});
vi.mocked(useAsync).mockReturnValue(mockUseAsync());
const { unmount } = render(<MetricsBar />);
act(() => {
vi.advanceTimersByTime(30000);
Expand All @@ -78,4 +99,4 @@ describe('MetricsBar', () => {
expect(consoleWarn).not.toHaveBeenCalled();
consoleWarn.mockRestore();
});
});
});
Loading