mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
// FE-W4ATB-001 to FE-W4ATB-010
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render'
|
|
import { useTripStore } from '../../store/tripStore'
|
|
|
|
const listTemplates = vi.fn(async (_tripId: number | string) => ({ templates: [] as unknown[] }))
|
|
const applyTemplate = vi.fn(async (_tripId: number | string, _id: number, _vis: string) => ({ items: [] as unknown[], count: 0 }))
|
|
const toast = { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }
|
|
|
|
vi.mock('../../api/client', () => ({
|
|
packingApi: {
|
|
listTemplates: (tripId: number | string) => listTemplates(tripId),
|
|
applyTemplate: (tripId: number | string, id: number, vis: string) => applyTemplate(tripId, id, vis),
|
|
},
|
|
}))
|
|
vi.mock('../shared/Toast', () => ({ useToast: () => toast }))
|
|
|
|
import ApplyTemplateButton from './ApplyTemplateButton'
|
|
|
|
const TEMPLATES = [
|
|
{ id: 1, name: 'Beach trip', item_count: 12 },
|
|
{ id: 2, name: 'City break', item_count: 7 },
|
|
]
|
|
|
|
function renderButton(visibility: 'common' | 'personal' = 'common') {
|
|
return render(<ApplyTemplateButton tripId={4} visibility={visibility} style={{ gap: 6 }} />)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
listTemplates.mockReset()
|
|
listTemplates.mockResolvedValue({ templates: TEMPLATES })
|
|
applyTemplate.mockReset()
|
|
applyTemplate.mockResolvedValue({ items: [], count: 0 })
|
|
Object.values(toast).forEach(f => f.mockClear())
|
|
useTripStore.setState({ packingItems: [] })
|
|
})
|
|
|
|
describe('ApplyTemplateButton', () => {
|
|
it('FE-W4ATB-001: renders nothing while no template exists', async () => {
|
|
listTemplates.mockResolvedValue({ templates: [] })
|
|
const { container } = renderButton()
|
|
|
|
await waitFor(() => expect(listTemplates).toHaveBeenCalledWith(4))
|
|
expect(container).toBeEmptyDOMElement()
|
|
})
|
|
|
|
it('FE-W4ATB-002: stays hidden when the template list fails to load', async () => {
|
|
listTemplates.mockRejectedValue(new Error('offline'))
|
|
const { container } = renderButton()
|
|
|
|
await waitFor(() => expect(listTemplates).toHaveBeenCalled())
|
|
expect(container).toBeEmptyDOMElement()
|
|
})
|
|
|
|
it('FE-W4ATB-003: tolerates a response without a templates array', async () => {
|
|
listTemplates.mockResolvedValue({} as { templates: unknown[] })
|
|
const { container } = renderButton()
|
|
|
|
await waitFor(() => expect(listTemplates).toHaveBeenCalled())
|
|
expect(container).toBeEmptyDOMElement()
|
|
})
|
|
|
|
it('FE-W4ATB-004: shows the trigger once templates arrive', async () => {
|
|
renderButton()
|
|
|
|
expect(await screen.findByRole('button')).toHaveTextContent(/apply template/i)
|
|
})
|
|
|
|
it('FE-W4ATB-005: the trigger toggles the template menu', async () => {
|
|
renderButton()
|
|
const trigger = await screen.findByRole('button')
|
|
|
|
fireEvent.click(trigger)
|
|
expect(screen.getByText('Beach trip')).toBeInTheDocument()
|
|
expect(screen.getByText(/^12 /)).toBeInTheDocument()
|
|
|
|
fireEvent.click(trigger)
|
|
expect(screen.queryByText('Beach trip')).toBeNull()
|
|
})
|
|
|
|
it('FE-W4ATB-006: a mousedown outside closes the menu', async () => {
|
|
renderButton()
|
|
fireEvent.click(await screen.findByRole('button'))
|
|
expect(screen.getByText('City break')).toBeInTheDocument()
|
|
|
|
fireEvent.mouseDown(document.body)
|
|
expect(screen.queryByText('City break')).toBeNull()
|
|
})
|
|
|
|
it('FE-W4ATB-007: a mousedown inside keeps the menu open', async () => {
|
|
renderButton()
|
|
fireEvent.click(await screen.findByRole('button'))
|
|
|
|
fireEvent.mouseDown(screen.getByText('Beach trip'))
|
|
|
|
expect(screen.getByText('Beach trip')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-W4ATB-008: applying a template appends its items to the trip store', async () => {
|
|
applyTemplate.mockResolvedValue({ items: [{ id: 90 }, { id: 91 }], count: 2 })
|
|
useTripStore.setState({ packingItems: [{ id: 1 }] as never })
|
|
renderButton('personal')
|
|
|
|
fireEvent.click(await screen.findByRole('button'))
|
|
fireEvent.click(screen.getByText('Beach trip'))
|
|
|
|
await waitFor(() => expect(toast.success).toHaveBeenCalledWith(expect.stringContaining('2')))
|
|
expect(applyTemplate).toHaveBeenCalledWith(4, 1, 'personal')
|
|
expect(useTripStore.getState().packingItems.map(i => i.id)).toEqual([1, 90, 91])
|
|
expect(screen.queryByText('Beach trip')).toBeNull()
|
|
})
|
|
|
|
it('FE-W4ATB-009: tolerates a response without an items array', async () => {
|
|
applyTemplate.mockResolvedValue({ count: 0 } as { items: unknown[]; count: number })
|
|
renderButton()
|
|
|
|
fireEvent.click(await screen.findByRole('button'))
|
|
fireEvent.click(screen.getByText('City break'))
|
|
|
|
await waitFor(() => expect(toast.success).toHaveBeenCalled())
|
|
expect(useTripStore.getState().packingItems).toEqual([])
|
|
})
|
|
|
|
it('FE-W4ATB-010: a failing apply toasts an error and keeps the menu open', async () => {
|
|
applyTemplate.mockRejectedValue(new Error('500'))
|
|
renderButton()
|
|
|
|
fireEvent.click(await screen.findByRole('button'))
|
|
fireEvent.click(screen.getByText('Beach trip'))
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalled())
|
|
expect(toast.success).not.toHaveBeenCalled()
|
|
expect(screen.getByText('Beach trip')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-W4ATB-011: honours a caller className on the trigger', async () => {
|
|
render(<ApplyTemplateButton tripId={4} visibility="common" style={{}} className="btn-ghost" />)
|
|
|
|
expect(await screen.findByRole('button')).toHaveClass('btn-ghost')
|
|
})
|
|
})
|