125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
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 <SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>{children}</SWRConfig>
|
|
}
|
|
|
|
function item(overrides: Partial<Item> = {}): 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),
|
|
)
|
|
})
|
|
})
|