95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import type { ReactNode } from 'react'
|
|
import { SWRConfig } from 'swr'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ItemsPage } from '../src/pages/ItemsPage'
|
|
import { ApiError } from '../src/api/client'
|
|
import * as itemsApi from '../src/api/items'
|
|
import type { Item } from '../src/api/items'
|
|
|
|
/* Page-level wiring through the same seam the hooks tests use: the api
|
|
module is mocked, everything else (SWR, page states, dialog) is real. */
|
|
vi.mock('../src/api/items', () => ({
|
|
listItems: vi.fn(),
|
|
getItem: vi.fn(),
|
|
createItem: vi.fn(),
|
|
updateItem: vi.fn(),
|
|
deleteItem: vi.fn(),
|
|
}))
|
|
|
|
function item(overrides: Partial<Item> = {}): Item {
|
|
return {
|
|
id: 1,
|
|
title: 'First',
|
|
description: '',
|
|
status: 'active',
|
|
createdAt: '2026-09-16T10:00:00Z',
|
|
updatedAt: '2026-09-16T10:00:00Z',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function renderPage() {
|
|
return render(
|
|
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>
|
|
<ItemsPage />
|
|
</SWRConfig>,
|
|
)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(itemsApi.listItems).mockResolvedValue([])
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('ItemsPage states', () => {
|
|
it('shows a sign-in card when the API answers 401 (OIDC enabled)', async () => {
|
|
vi.mocked(itemsApi.listItems).mockRejectedValue(
|
|
new ApiError(401, 'unauthorized', 'sign in via /api/auth/login'),
|
|
)
|
|
|
|
renderPage()
|
|
|
|
expect(await screen.findByText('Sign in required')).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument()
|
|
expect(screen.queryByText('Cannot reach the Go API')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows the backend-down card with a retry when the API is unreachable', async () => {
|
|
vi.mocked(itemsApi.listItems).mockRejectedValue(
|
|
new ApiError(0, 'network', 'Could not reach the API (fetch failed)'),
|
|
)
|
|
|
|
renderPage()
|
|
|
|
expect(await screen.findByText('Cannot reach the Go API')).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /Retry/ })).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders the empty state and creates an item through the dialog', async () => {
|
|
vi.mocked(itemsApi.createItem).mockResolvedValue(item({ title: 'From dialog' }))
|
|
|
|
renderPage()
|
|
|
|
// Empty state with its own CTA (the toolbar has a second "New item").
|
|
expect(await screen.findByText('No items yet')).toBeInTheDocument()
|
|
const cta = screen.getAllByRole('button', { name: /New item/ }).at(-1)!
|
|
fireEvent.click(cta)
|
|
|
|
// Dialog: fill the form and submit.
|
|
expect(await screen.findByRole('dialog')).toBeInTheDocument()
|
|
fireEvent.change(screen.getByLabelText(/Title/), { target: { value: 'From dialog' } })
|
|
fireEvent.click(screen.getByRole('button', { name: /Create item/ }))
|
|
|
|
await waitFor(() =>
|
|
expect(itemsApi.createItem).toHaveBeenCalledWith({ title: 'From dialog', description: undefined }),
|
|
)
|
|
// Success closes the dialog and revalidates the list.
|
|
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
|
expect(itemsApi.listItems).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|