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