175 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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