import { act, renderHook, waitFor } from '@testing-library/react' import type { ReactNode } from 'react' import { SWRConfig } from 'swr' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useItemActions, useItems } from '../src/hooks/useItems' import type { Item } from '../src/api/items' /* The api module is the seam: hooks are tested against a mocked module, exactly like pages will consume the hooks. */ vi.mock('../src/api/items', () => ({ listItems: vi.fn(), getItem: vi.fn(), createItem: vi.fn(), updateItem: vi.fn(), deleteItem: vi.fn(), })) import * as itemsApi from '../src/api/items' /* Fresh cache per test so revalidation assertions never share state. */ function wrapper({ children }: { children: ReactNode }) { return new Map(), dedupingInterval: 0 }}>{children} } function item(overrides: Partial = {}): Item { return { id: 1, title: 'First', description: '', status: 'active', createdAt: '2026-09-15T10:00:00Z', updatedAt: '2026-09-15T10:00:00Z', ...overrides, } } /* useItems and useItemActions must share one SWR cache for action revalidation to be observable — render both through one harness. */ function useItemsHarness() { const list = useItems() const actions = useItemActions() return { list, actions } } beforeEach(() => { vi.mocked(itemsApi.listItems).mockResolvedValue([item()]) vi.mocked(itemsApi.createItem).mockResolvedValue(item({ id: 2, title: 'Second' })) vi.mocked(itemsApi.updateItem).mockResolvedValue(item({ status: 'archived' })) vi.mocked(itemsApi.deleteItem).mockResolvedValue(undefined) }) afterEach(() => { vi.clearAllMocks() }) describe('useItems', () => { it('loads the list through the api module', async () => { const { result } = renderHook(() => useItems(), { wrapper }) await waitFor(() => expect(result.current.items).toHaveLength(1)) expect(itemsApi.listItems).toHaveBeenCalledWith({}) expect(result.current.isLoading).toBe(false) expect(result.current.error).toBeUndefined() }) it('feeds the filter into the api query', async () => { const { result } = renderHook(() => useItems({ status: 'archived' }), { wrapper }) await waitFor(() => expect(result.current.items).toHaveLength(1)) expect(itemsApi.listItems).toHaveBeenCalledWith({ status: 'archived' }) }) it('exposes load failures as errors', async () => { vi.mocked(itemsApi.listItems).mockRejectedValueOnce(new Error('backend down')) const { result } = renderHook(() => useItems(), { wrapper }) await waitFor(() => expect(result.current.error).toBeInstanceOf(Error)) expect(result.current.items).toBeUndefined() }) }) describe('useItemActions', () => { it('creates through the api module and revalidates the list', async () => { const { result } = renderHook(() => useItemsHarness(), { wrapper }) await waitFor(() => expect(result.current.list.items).toHaveLength(1)) const fetchesBefore = vi.mocked(itemsApi.listItems).mock.calls.length await act(() => result.current.actions.create({ title: 'Second' })) expect(itemsApi.createItem).toHaveBeenCalledWith({ title: 'Second' }) await waitFor(() => expect(vi.mocked(itemsApi.listItems).mock.calls.length).toBeGreaterThan(fetchesBefore), ) }) it('updates through the api module and revalidates the list', async () => { const { result } = renderHook(() => useItemsHarness(), { wrapper }) await waitFor(() => expect(result.current.list.items).toHaveLength(1)) const fetchesBefore = vi.mocked(itemsApi.listItems).mock.calls.length await act(() => result.current.actions.update(1, { status: 'archived' })) expect(itemsApi.updateItem).toHaveBeenCalledWith(1, { status: 'archived' }) await waitFor(() => expect(vi.mocked(itemsApi.listItems).mock.calls.length).toBeGreaterThan(fetchesBefore), ) }) it('removes through the api module and revalidates the list', async () => { const { result } = renderHook(() => useItemsHarness(), { wrapper }) await waitFor(() => expect(result.current.list.items).toHaveLength(1)) const fetchesBefore = vi.mocked(itemsApi.listItems).mock.calls.length await act(() => result.current.actions.remove(1)) expect(itemsApi.deleteItem).toHaveBeenCalledWith(1) await waitFor(() => expect(vi.mocked(itemsApi.listItems).mock.calls.length).toBeGreaterThan(fetchesBefore), ) }) })