Compare commits
2 Commits
8014dcd602
...
38ebd2bbd1
| Author | SHA1 | Date | |
|---|---|---|---|
| 38ebd2bbd1 | |||
| 83c9cd8fb7 |
525
src/test/proposal-essential.test.tsx
Normal file
525
src/test/proposal-essential.test.tsx
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ProposalDetailPage from '@/pages/ProposalDetailPage'
|
||||||
|
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||||
|
|
||||||
|
const mockGet = vi.fn()
|
||||||
|
const mockPost = vi.fn()
|
||||||
|
const mockPatch = vi.fn()
|
||||||
|
const mockDelete = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('@/services/api', () => ({
|
||||||
|
default: {
|
||||||
|
get: (...args: any[]) => mockGet(...args),
|
||||||
|
post: (...args: any[]) => mockPost(...args),
|
||||||
|
patch: (...args: any[]) => mockPatch(...args),
|
||||||
|
delete: (...args: any[]) => mockDelete(...args),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockNavigate = vi.fn()
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = await vi.importActual('react-router-dom')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
useParams: () => ({ id: '1' }),
|
||||||
|
useSearchParams: () => [new URLSearchParams('?project_id=1'), vi.fn()],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
type Essential = {
|
||||||
|
id: number
|
||||||
|
essential_code: string
|
||||||
|
proposal_id: number
|
||||||
|
type: 'feature' | 'improvement' | 'refactor'
|
||||||
|
title: string
|
||||||
|
description: string | null
|
||||||
|
created_by_id: number | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type Proposal = {
|
||||||
|
id: number
|
||||||
|
proposal_code: string | null
|
||||||
|
title: string
|
||||||
|
description: string | null
|
||||||
|
status: 'open' | 'accepted' | 'rejected'
|
||||||
|
project_id: number
|
||||||
|
created_by_id: number | null
|
||||||
|
created_by_username: string | null
|
||||||
|
feat_task_id: string | null
|
||||||
|
essentials: Essential[] | null
|
||||||
|
generated_tasks: any[] | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type Milestone = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
status: 'open' | 'freeze' | 'undergoing' | 'completed' | 'closed'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockProposal: Proposal = {
|
||||||
|
id: 1,
|
||||||
|
proposal_code: 'PROP-001',
|
||||||
|
title: 'Test Proposal',
|
||||||
|
description: 'Test description',
|
||||||
|
status: 'open',
|
||||||
|
project_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
created_by_username: 'admin',
|
||||||
|
feat_task_id: null,
|
||||||
|
essentials: [],
|
||||||
|
generated_tasks: null,
|
||||||
|
created_at: '2026-03-01T00:00:00Z',
|
||||||
|
updated_at: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockEssentials: Essential[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
essential_code: 'ESS-001',
|
||||||
|
proposal_id: 1,
|
||||||
|
type: 'feature',
|
||||||
|
title: 'Feature Essential',
|
||||||
|
description: 'A feature essential',
|
||||||
|
created_by_id: 1,
|
||||||
|
created_at: '2026-03-01T00:00:00Z',
|
||||||
|
updated_at: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
essential_code: 'ESS-002',
|
||||||
|
proposal_id: 1,
|
||||||
|
type: 'improvement',
|
||||||
|
title: 'Improvement Essential',
|
||||||
|
description: null,
|
||||||
|
created_by_id: 1,
|
||||||
|
created_at: '2026-03-01T00:00:00Z',
|
||||||
|
updated_at: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockMilestones: Milestone[] = [
|
||||||
|
{ id: 1, title: 'Milestone 1', status: 'open' },
|
||||||
|
{ id: 2, title: 'Milestone 2', status: 'open' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function setupApi(options?: {
|
||||||
|
proposal?: Proposal
|
||||||
|
essentials?: Essential[]
|
||||||
|
milestones?: Milestone[]
|
||||||
|
error?: string
|
||||||
|
}) {
|
||||||
|
const proposal = options?.proposal ?? mockProposal
|
||||||
|
const essentials = options?.essentials ?? mockEssentials
|
||||||
|
const milestones = options?.milestones ?? mockMilestones
|
||||||
|
|
||||||
|
mockGet.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('/proposals/1') && !url.includes('/essentials')) {
|
||||||
|
if (options?.error) {
|
||||||
|
return Promise.reject({ response: { data: { detail: options.error } } })
|
||||||
|
}
|
||||||
|
return Promise.resolve({ data: proposal })
|
||||||
|
}
|
||||||
|
if (url.includes('/essentials')) {
|
||||||
|
return Promise.resolve({ data: essentials })
|
||||||
|
}
|
||||||
|
if (url.includes('/milestones')) {
|
||||||
|
return Promise.resolve({ data: milestones })
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`Unhandled GET ${url}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
mockPost.mockResolvedValue({ data: {} })
|
||||||
|
mockPatch.mockResolvedValue({ data: {} })
|
||||||
|
mockDelete.mockResolvedValue({ data: {} })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ProposalDetailPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
setupApi()
|
||||||
|
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Essential List Display', () => {
|
||||||
|
it('renders essentials section with count', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Essentials (2)')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays essential cards with type badges', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Feature Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('Improvement Essential')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('feature')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('improvement')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays essential codes', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('ESS-001')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('ESS-002')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays essential descriptions when available', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('A feature essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state when no essentials', async () => {
|
||||||
|
setupApi({ essentials: [] })
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Essentials (0)')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText(/No essentials yet/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/Add one to define deliverables/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows loading state for essentials', async () => {
|
||||||
|
mockGet.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('/proposals/1') && !url.includes('/essentials')) {
|
||||||
|
return Promise.resolve({ data: mockProposal })
|
||||||
|
}
|
||||||
|
if (url.includes('/essentials')) {
|
||||||
|
return new Promise(() => {}) // Never resolve
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`Unhandled GET ${url}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Loading/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Essential Create/Edit Forms', () => {
|
||||||
|
it('opens create essential modal', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('+ New Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('+ New Essential'))
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: 'New Essential' })).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText('Essential title')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Feature')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Improvement')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Refactor')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates new essential', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('+ New Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('+ New Essential'))
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByPlaceholderText('Essential title'), 'New Test Essential')
|
||||||
|
fireEvent.change(screen.getAllByRole('combobox')[0], { target: { value: 'refactor' } })
|
||||||
|
await userEvent.type(screen.getByPlaceholderText('Description (optional)'), 'Test description')
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'/projects/1/proposals/1/essentials',
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'New Test Essential',
|
||||||
|
type: 'refactor',
|
||||||
|
description: 'Test description',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens edit essential modal with pre-filled data', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Feature Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByRole('button', { name: 'Edit' })
|
||||||
|
await userEvent.click(editButtons[0])
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: 'Edit Essential' })).toBeInTheDocument()
|
||||||
|
|
||||||
|
const titleInput = screen.getByDisplayValue('Feature Essential')
|
||||||
|
expect(titleInput).toBeInTheDocument()
|
||||||
|
|
||||||
|
const descriptionInput = screen.getByDisplayValue('A feature essential')
|
||||||
|
expect(descriptionInput).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates essential', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Feature Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByRole('button', { name: 'Edit' })
|
||||||
|
await userEvent.click(editButtons[0])
|
||||||
|
|
||||||
|
const titleInput = screen.getByDisplayValue('Feature Essential')
|
||||||
|
await userEvent.clear(titleInput)
|
||||||
|
await userEvent.type(titleInput, 'Updated Essential Title')
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPatch).toHaveBeenCalledWith(
|
||||||
|
'/projects/1/proposals/1/essentials/1',
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'Updated Essential Title',
|
||||||
|
type: 'feature',
|
||||||
|
description: 'A feature essential',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes essential after confirmation', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Feature Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' })
|
||||||
|
await userEvent.click(deleteButtons[0])
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete this Essential?')
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockDelete).toHaveBeenCalledWith('/projects/1/proposals/1/essentials/1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables save button when title is empty', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('+ New Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('+ New Essential'))
|
||||||
|
|
||||||
|
const saveButton = screen.getByRole('button', { name: 'Save' })
|
||||||
|
expect(saveButton).toBeDisabled()
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByPlaceholderText('Essential title'), 'Some Title')
|
||||||
|
expect(saveButton).toBeEnabled()
|
||||||
|
|
||||||
|
await userEvent.clear(screen.getByPlaceholderText('Essential title'))
|
||||||
|
expect(saveButton).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Accept with Milestone Selection', () => {
|
||||||
|
it('opens accept modal with milestone selector', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
const acceptButton = await screen.findByRole('button', { name: /accept/i })
|
||||||
|
await userEvent.click(acceptButton)
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: 'Accept Proposal' })).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/milestone to generate story tasks/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables confirm button without milestone selection', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
const acceptButton = await screen.findByRole('button', { name: /accept/i })
|
||||||
|
await userEvent.click(acceptButton)
|
||||||
|
|
||||||
|
const confirmButton = screen.getByRole('button', { name: 'Confirm Accept' })
|
||||||
|
expect(confirmButton).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enables confirm button after milestone selection', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
const acceptButton = await screen.findByRole('button', { name: /accept/i })
|
||||||
|
await userEvent.click(acceptButton)
|
||||||
|
|
||||||
|
const select = screen.getByRole('combobox')
|
||||||
|
fireEvent.change(select, { target: { value: '1' } })
|
||||||
|
|
||||||
|
const confirmButton = screen.getByRole('button', { name: 'Confirm Accept' })
|
||||||
|
expect(confirmButton).toBeEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls accept API with selected milestone', async () => {
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
const acceptButton = await screen.findByRole('button', { name: /accept/i })
|
||||||
|
await userEvent.click(acceptButton)
|
||||||
|
|
||||||
|
const select = screen.getByRole('combobox')
|
||||||
|
fireEvent.change(select, { target: { value: '1' } })
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Confirm Accept' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'/projects/1/proposals/1/accept',
|
||||||
|
{ milestone_id: 1 }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows warning when no open milestones available', async () => {
|
||||||
|
setupApi({ milestones: [] })
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
const acceptButton = await screen.findByRole('button', { name: /accept/i })
|
||||||
|
await userEvent.click(acceptButton)
|
||||||
|
|
||||||
|
expect(screen.getByText('No open milestones available.')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays generated tasks after accept', async () => {
|
||||||
|
const acceptedProposal: Proposal = {
|
||||||
|
...mockProposal,
|
||||||
|
status: 'accepted',
|
||||||
|
generated_tasks: [
|
||||||
|
{ task_id: 1, task_code: 'TASK-001', title: 'Story Task 1', task_type: 'story', task_subtype: 'feature' },
|
||||||
|
{ task_id: 2, task_code: 'TASK-002', title: 'Story Task 2', task_type: 'story', task_subtype: 'improvement' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
setupApi({ proposal: acceptedProposal })
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Generated Tasks')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('Story Task 1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Story Task 2')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('story/feature')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('story/improvement')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Story Creation Restriction UI', () => {
|
||||||
|
it('hides essential controls for accepted proposal', async () => {
|
||||||
|
const acceptedProposal: Proposal = {
|
||||||
|
...mockProposal,
|
||||||
|
status: 'accepted',
|
||||||
|
essentials: mockEssentials,
|
||||||
|
}
|
||||||
|
setupApi({ proposal: acceptedProposal })
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await screen.findByText('Feature Essential')
|
||||||
|
expect(screen.queryByText('+ New Essential')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides edit/delete buttons for essentials when proposal is accepted', async () => {
|
||||||
|
const acceptedProposal: Proposal = {
|
||||||
|
...mockProposal,
|
||||||
|
status: 'accepted',
|
||||||
|
essentials: mockEssentials,
|
||||||
|
}
|
||||||
|
setupApi({ proposal: acceptedProposal })
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Feature Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteButtons = screen.queryAllByRole('button', { name: 'Delete' })
|
||||||
|
expect(deleteButtons.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides edit/delete buttons for rejected proposal', async () => {
|
||||||
|
const rejectedProposal: Proposal = {
|
||||||
|
...mockProposal,
|
||||||
|
status: 'rejected',
|
||||||
|
essentials: mockEssentials,
|
||||||
|
}
|
||||||
|
setupApi({ proposal: rejectedProposal })
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await screen.findByRole('button', { name: /reopen/i })
|
||||||
|
expect(screen.queryByText('+ New Essential')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
it('displays error message on essential create failure', async () => {
|
||||||
|
mockPost.mockRejectedValue({ response: { data: { detail: 'Failed to create essential' } } })
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('+ New Essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('+ New Essential'))
|
||||||
|
await userEvent.type(screen.getByPlaceholderText('Essential title'), 'Test')
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Failed to create essential')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays error message on accept failure', async () => {
|
||||||
|
mockPost.mockRejectedValue({ response: { data: { detail: 'Accept failed: milestone required' } } })
|
||||||
|
|
||||||
|
render(<ProposalDetailPage />)
|
||||||
|
|
||||||
|
const acceptButton = await screen.findByRole('button', { name: /accept/i })
|
||||||
|
await userEvent.click(acceptButton)
|
||||||
|
fireEvent.change(screen.getByRole('combobox'), { target: { value: '1' } })
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Confirm Accept' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Accept failed: milestone required')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user