175 lines
8.0 KiB
TypeScript
175 lines
8.0 KiB
TypeScript
import { act, render, screen, waitFor } from '@testing-library/react'
|
||
import userEvent from '@testing-library/user-event'
|
||
|
||
import TaskDetailChangeRequestPage from './page'
|
||
|
||
const originalFetch = global.fetch
|
||
|
||
const createJsonResponse = <T,>(body: T, status = 200) => ({
|
||
ok: status >= 200 && status < 300,
|
||
status,
|
||
json: async () => body,
|
||
})
|
||
|
||
const requestRecord = (status = 'pending_manager_approval', applyError: string | null = null) => ({
|
||
success: true,
|
||
data: { id: 'request-1', status, applyError, canDecide: status === 'pending_manager_approval',
|
||
managerName: '测试主管', requesterName: '测试提交人', material: { productModel: '测试素材', materialType: '任务-详情' },
|
||
operations: [{ kind: 'delete', assetId: 'asset-1' }],
|
||
},
|
||
})
|
||
|
||
jest.mock('next/navigation', () => ({
|
||
useParams: () => ({ id: 'request-1' }),
|
||
}))
|
||
|
||
describe('TaskDetailChangeRequestPage', () => {
|
||
afterEach(() => { jest.useRealTimers() })
|
||
afterAll(() => {
|
||
global.fetch = originalFetch
|
||
})
|
||
|
||
test('loads request details and allows approver to approve', async () => {
|
||
const user = userEvent.setup()
|
||
let approved = false
|
||
const fetchMock = jest.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||
const url = String(input)
|
||
if (url === '/api/task-detail-change-requests/request-1') {
|
||
return createJsonResponse({
|
||
success: true,
|
||
data: {
|
||
id: 'request-1',
|
||
status: approved ? 'applied' : 'pending_manager_approval',
|
||
canDecide: !approved,
|
||
managerName: '王主管',
|
||
requesterName: '小李',
|
||
expiresAt: '2026-09-01T12:00:00.000Z',
|
||
decidedAt: approved ? '2026-08-28T10:30:00.000Z' : null,
|
||
material: {
|
||
productModel: 'GBSFY-SJZ0024详情',
|
||
materialType: '任务-详情',
|
||
taskNo: 'TASK-001',
|
||
},
|
||
operations: [
|
||
{ kind: 'append', group: 'main', fileName: 'append-main-1.jpg' },
|
||
{ kind: 'insert', group: 'detail', afterAssetId: 'a1', beforeAssetId: 'a2', fileName: 'insert-detail.jpg' },
|
||
],
|
||
},
|
||
})
|
||
}
|
||
|
||
if (url === '/api/task-detail-change-requests/request-1/decision') {
|
||
expect(init).toMatchObject({
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ decision: 'approve' }),
|
||
})
|
||
|
||
approved = true
|
||
return createJsonResponse({ success: true, data: { status: 'applied' } })
|
||
}
|
||
|
||
return createJsonResponse({ error: 'unexpected request' }, 500)
|
||
})
|
||
global.fetch = fetchMock as jest.Mock
|
||
|
||
render(<TaskDetailChangeRequestPage />)
|
||
|
||
expect(await screen.findByRole('heading', { name: '审批详情' })).toBeInTheDocument()
|
||
expect(await screen.findByText('GBSFY-SJZ0024详情')).toBeInTheDocument()
|
||
expect(screen.getByText('小李')).toBeInTheDocument()
|
||
expect(screen.getAllByText('待主管确认').length).toBeGreaterThan(0)
|
||
expect(screen.getByText('追加到主图末尾')).toBeInTheDocument()
|
||
expect(screen.getByText('插入到详情图接缝(a1 → a2)')).toBeInTheDocument()
|
||
|
||
await user.click(screen.getByRole('button', { name: '批准' }))
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getAllByText('已通过').length).toBeGreaterThan(0)
|
||
})
|
||
expect(screen.getByText('处理时间')).toBeInTheDocument()
|
||
expect(screen.queryByRole('button', { name: '批准' })).not.toBeInTheDocument()
|
||
expect(screen.queryByRole('button', { name: '否决' })).not.toBeInTheDocument()
|
||
})
|
||
|
||
test('applying is shown as approved and processing, with read-only refresh instead of decision buttons', async () => {
|
||
const user = userEvent.setup()
|
||
const fetchMock = jest.fn(async () => createJsonResponse(requestRecord('applying')))
|
||
global.fetch = fetchMock as jest.Mock
|
||
render(<TaskDetailChangeRequestPage />)
|
||
expect(await screen.findByText(/主管已批准,正在应用图片变更/)).toBeInTheDocument()
|
||
expect(screen.queryByRole('button', { name: '批准' })).not.toBeInTheDocument()
|
||
await user.click(screen.getByRole('button', { name: '刷新状态' }))
|
||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||
expect(fetchMock.mock.calls.every((call: unknown[]) => !(call[1] as RequestInit)?.method)).toBe(true)
|
||
})
|
||
|
||
test('failed decision refreshes persisted state and retains the specific apply error', async () => {
|
||
const user = userEvent.setup()
|
||
let submitted = false
|
||
const reason = '插图位置不存在或已被本次批量操作改动,请刷新后重试'
|
||
global.fetch = jest.fn(async (_input, init?: RequestInit) => {
|
||
if (init?.method === 'POST') {
|
||
submitted = true
|
||
return createJsonResponse({ error: reason }, 409)
|
||
}
|
||
return createJsonResponse(requestRecord(submitted ? 'apply_failed' : 'pending_manager_approval', submitted ? reason : null))
|
||
}) as jest.Mock
|
||
render(<TaskDetailChangeRequestPage />)
|
||
await user.click(await screen.findByRole('button', { name: '批准' }))
|
||
expect(await screen.findByRole('alert')).toHaveTextContent(reason)
|
||
expect(screen.getAllByText('应用失败')).toHaveLength(2)
|
||
expect(screen.getByRole('alert')).toHaveTextContent('本单不能重复批准')
|
||
await user.click(screen.getByRole('button', { name: '刷新状态' }))
|
||
expect(screen.getByRole('alert')).toHaveTextContent(reason)
|
||
expect(screen.queryByRole('button', { name: '批准' })).not.toBeInTheDocument()
|
||
})
|
||
|
||
test('a slow POST can be reconciled by read-only refresh without sending another approval', async () => {
|
||
const user = userEvent.setup()
|
||
let status = 'pending_manager_approval'
|
||
let finish!: (response: ReturnType<typeof createJsonResponse>) => void
|
||
const fetchMock = jest.fn((_input, init?: RequestInit) => {
|
||
if (init?.method === 'POST') {
|
||
status = 'applying'
|
||
return new Promise(resolve => { finish = resolve })
|
||
}
|
||
return Promise.resolve(createJsonResponse(requestRecord(status)))
|
||
})
|
||
global.fetch = fetchMock as jest.Mock
|
||
render(<TaskDetailChangeRequestPage />)
|
||
await user.click(await screen.findByRole('button', { name: '批准' }))
|
||
expect(screen.getByText(/正在提交审批并处理图片变更/)).toBeInTheDocument()
|
||
await user.click(screen.getByRole('button', { name: '刷新状态' }))
|
||
expect(await screen.findByText(/主管已批准,正在应用图片变更/)).toBeInTheDocument()
|
||
expect(fetchMock.mock.calls.filter(([,init]) => init?.method === 'POST')).toHaveLength(1)
|
||
status = 'applied'
|
||
await act(async () => { finish(createJsonResponse({ success: true })); await Promise.resolve() })
|
||
expect(await screen.findAllByText('已通过')).toHaveLength(2)
|
||
})
|
||
|
||
test('a timed-out POST is not treated as failure of the application and later success clears the timeout hint', async () => {
|
||
jest.useFakeTimers()
|
||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime })
|
||
let status = 'pending_manager_approval'
|
||
const fetchMock = jest.fn((_input, init?: RequestInit) => {
|
||
if (init?.method === 'POST') {
|
||
status = 'applying'
|
||
return new Promise((_resolve, reject) => init.signal?.addEventListener('abort', () => reject(new DOMException('timeout', 'AbortError'))))
|
||
}
|
||
return Promise.resolve(createJsonResponse(requestRecord(status)))
|
||
})
|
||
global.fetch = fetchMock as jest.Mock
|
||
render(<TaskDetailChangeRequestPage />)
|
||
await user.click(await screen.findByRole('button', { name: '批准' }))
|
||
await act(async () => { await jest.advanceTimersByTimeAsync(30_100) })
|
||
expect(screen.getByRole('alert')).toHaveTextContent('请求未返回结果')
|
||
expect(screen.getByText(/主管已批准,正在应用图片变更/)).toBeInTheDocument()
|
||
expect(fetchMock.mock.calls.filter(([, init]) => init?.method === 'POST')).toHaveLength(1)
|
||
status = 'applied'
|
||
await act(async () => { await jest.advanceTimersByTimeAsync(3100) })
|
||
expect(screen.getAllByText('已通过')).toHaveLength(2)
|
||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||
})
|
||
})
|