Files
2026-10-19 16:04:17 +02:00

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)
})
})