From a9c4e18a7d5dc75f61d90189e308a1a51b6e3101 Mon Sep 17 00:00:00 2001 From: Annie <168873935+AnnieIj@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:07:54 +0000 Subject: [PATCH] fix(test): type MetricsBar's useAsync mock against the real hook shape (Closes #429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MetricsBar.test.tsx mocked useAsync with only { state, refresh }, omitting reload and mutate, so tsc reported TS2345 at all four call sites and the component was verified against a contract the real hook never returns. Replace the hand-written mocks with a typed mockUseAsync factory whose return type is ReturnType; a future change to the hook's shape now breaks compilation instead of silently testing a fabricated interface. No as any, @ts-expect-error or Partial cast used. MetricsBar itself only consumes state and refresh (reload/mutate are intentionally unused: the manual refresh must stay silent, which is refresh's contract), so no behavioural gap was found. Repo-wide tsc error count drops from 9 to 5; the remaining 5 are the out-of-scope SettlementTable.test.tsx errors. Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- src/components/MetricsBar.test.tsx | 59 ++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/components/MetricsBar.test.tsx b/src/components/MetricsBar.test.tsx index fce2f0e..be2b63b 100644 --- a/src/components/MetricsBar.test.tsx +++ b/src/components/MetricsBar.test.tsx @@ -7,6 +7,26 @@ vi.mock('@/hooks/useAsync', () => ({ useAsync: vi.fn(), })); +type UseAsyncResult = ReturnType; + +/** + * 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 { + return { + state: { status: 'loading' }, + reload: vi.fn(), + refresh: vi.fn(() => Promise.resolve()), + mutate: vi.fn(), + ...overrides, + }; +} + describe('MetricsBar', () => { beforeEach(() => { vi.useFakeTimers(); @@ -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(); expect(screen.getByText('Active anchors')).toBeInTheDocument(); expect(screen.getByText('Pools')).toBeInTheDocument(); @@ -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(); 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(); act(() => { unmount(); @@ -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(); act(() => { vi.advanceTimersByTime(30000); @@ -78,4 +99,4 @@ describe('MetricsBar', () => { expect(consoleWarn).not.toHaveBeenCalled(); consoleWarn.mockRestore(); }); -}); \ No newline at end of file +});