import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const taskId = '01M00000000000000000000190'; const instanceId = '01M00000000000000000000191'; const formId = '01M00000000000000000000192'; const companyId = '01M00000000000000000000193'; const projectId = '01M00000000000000000000194'; interface TaskFixtureOptions { view?: 'TODO' | 'CREATED'; taskActions?: string[]; formActions?: string[]; approveRequests?: Array>; returnRequests?: Array>; withdrawRequests?: Array>; voidRequests?: Array>; detailRequests?: string[]; detailTaskVersion?: number; detailField?: { code: string; label: string; dataType?: string }; taskQueries?: URL[]; } function sourceForm(allowedActions: string[] = []) { return { publicId: formId, businessNo: 'OA05-E2E-001', formType: 'OA-05', formName: '收款确认单', templateVersion: 2, sourceSystem: 'ONLINE', sourceNo: 'SRC-E2E-001', sourceVersion: '1', company: { publicId: companyId, name: '凯迪测试公司' }, project: { publicId: projectId, name: '审批验收项目' }, applicantName: '项目申请人', businessDate: '2026-08-15', status: 'PROJECT_REVIEW', currentNodeCode: 'PROJECT_REVIEW', currentNodeName: '项目复核', version: 1, createdByName: '项目申请人', createdAt: '2026-08-15T08:00:00Z', updatedAt: '2026-08-15T08:00:00Z', allowedActions, }; } function workflowTask(taskActions: string[], formActions: string[]) { return { publicId: taskId, instancePublicId: instanceId, nodeCode: 'PROJECT_REVIEW', nodeName: '项目复核', sequenceNo: 1, status: 'PENDING', version: 0, assigneeName: '项目审批人', arrivedAt: '2026-08-15T08:01:00Z', dueAt: '2026-08-18T08:01:00Z', updatedAt: '2026-08-15T08:01:00Z', keyAmount: '1000.00', form: sourceForm(formActions), allowedActions: taskActions, }; } function taskDetail( taskActions: string[], formActions: string[], taskVersion = 0, detailField: { code: string; label: string; dataType?: string } = { code: 'receiptAmount', label: '收款金额', dataType: 'DECIMAL', }, ) { const task = workflowTask(taskActions, formActions); task.version = taskVersion; return { task, form: { summary: task.form, documentVersion: 1, snapshotSha256: 'a'.repeat(64), values: { receiptAmount: '1000.00' }, fields: [ { code: detailField.code, label: detailField.label, sectionCode: 'base', sectionName: '基本信息', dataType: detailField.dataType || 'DECIMAL', required: true, repeatable: false, options: [], sensitivity: 'NORMAL', sortOrder: 1, }, ], approvals: [], validation: { documentId: formId, version: 1, valid: true, canSubmit: false, errors: [], warnings: [], validatedAt: '2026-08-15T08:00:30Z', }, }, timeline: [], }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000195', username: 'task-e2e', displayName: '审批验收用户', departmentName: '项目部', mustChangePassword: false, }, roles: [{ code: 'PROJECT_MANAGER', name: '项目管理人员', workbenchRoute: '/workbench/project' }], activeRole: 'PROJECT_MANAGER', permissions: ['workflow:task:view', 'workflow:task:approve', 'source:form:withdraw', 'source:form:void'], }; } async function installTaskFixture(page: Page, options: TaskFixtureOptions = {}) { const userSession = session(); let taskActions = options.taskActions || []; let formActions = options.formActions || []; const currentTaskDetail = () => taskDetail(taskActions, formActions, options.detailTaskVersion, options.detailField); await page.route('**/api/v1/**', async (route: Route) => { const request = route.request(); const pathname = new URL(request.url()).pathname; if (pathname === '/api/v1/auth/session') { await route.fulfill({ json: { data: userSession, requestId: '01M00000000000000000000196' } }); return; } if (pathname === '/api/v1/auth/profile') { await route.fulfill({ json: { data: userSession.user, requestId: '01M00000000000000000000197' } }); return; } if (pathname === '/api/v1/auth/roles') { await route.fulfill({ json: { data: userSession.roles, requestId: '01M00000000000000000000198' } }); return; } if (pathname === '/api/v1/auth/csrf') { await route.fulfill({ headers: { 'set-cookie': 'XSRF-TOKEN=task-e2e-csrf; Path=/' }, json: { data: { token: 'task-e2e-csrf' }, requestId: '01M00000000000000000000199' }, }); return; } if (request.method() === 'GET' && pathname === '/api/v1/source/references') { await route.fulfill({ json: { data: { companies: [{ publicId: companyId, name: '凯迪测试公司' }], projects: [{ publicId: projectId, name: '审批验收项目' }], suppliers: [], customers: [], contracts: [], costCategories: [], }, requestId: '01M00000000000000000000206', }, }); return; } if (request.method() === 'GET' && pathname === '/api/v1/workflow/tasks') { options.taskQueries?.push(new URL(request.url())); await route.fulfill({ json: { data: [workflowTask(taskActions, formActions)], meta: { page: 1, size: 20, totalElements: 1, totalPages: 1 }, requestId: '01M00000000000000000000200', }, }); return; } if (request.method() === 'GET' && pathname === `/api/v1/workflow/tasks/${taskId}`) { options.detailRequests?.push(taskId); await route.fulfill({ json: { data: currentTaskDetail(), requestId: '01M00000000000000000000201' }, }); return; } if (request.method() === 'POST' && pathname === `/api/v1/workflow/tasks/${taskId}/approve`) { options.approveRequests?.push(request.postDataJSON() as Record); taskActions = []; await route.fulfill({ json: { data: currentTaskDetail(), requestId: '01M00000000000000000000202' }, }); return; } if (request.method() === 'POST' && pathname === `/api/v1/workflow/tasks/${taskId}/return`) { options.returnRequests?.push(request.postDataJSON() as Record); taskActions = []; await route.fulfill({ json: { data: currentTaskDetail(), requestId: '01M00000000000000000000207' }, }); return; } if (request.method() === 'POST' && pathname === `/api/v1/source/forms/${formId}/withdraw`) { options.withdrawRequests?.push(request.postDataJSON() as Record); formActions = []; await route.fulfill({ json: { data: currentTaskDetail().form, requestId: '01M00000000000000000000203' }, }); return; } if (request.method() === 'POST' && pathname === `/api/v1/source/forms/${formId}/void`) { options.voidRequests?.push(request.postDataJSON() as Record); formActions = []; await route.fulfill({ json: { data: currentTaskDetail().form, requestId: '01M00000000000000000000204' }, }); return; } if (request.method() === 'GET') { await route.fulfill({ json: { data: [], meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 }, requestId: '01M00000000000000000000205', }, }); return; } await route.fulfill({ status: 204 }); }); } test('PAGE-11 applies project, arrival date, overdue and sort filters and renders the required columns', async ({ page, }) => { const taskQueries: URL[] = []; await installTaskFixture(page, { taskQueries }); await page.goto( `/tasks?view=TODO&projectId=${projectId}&arrivedDateFrom=2026-08-15&arrivedDateTo=2026-08-16&overdue=true&sort=dueAt%2Casc`, ); await expect.poll(() => taskQueries.length).toBeGreaterThan(0); const initialQuery = taskQueries.at(-1)?.searchParams; expect(initialQuery?.get('projectId')).toBe(projectId); expect(initialQuery?.get('arrivedDateFrom')).toBe('2026-08-15'); expect(initialQuery?.get('arrivedDateTo')).toBe('2026-08-16'); expect(initialQuery?.get('overdue')).toBe('true'); expect(initialQuery?.get('sort')).toBe('dueAt,asc'); const table = page.locator('.t-table'); await expect(table.getByText('审批验收项目', { exact: true })).toBeVisible(); await expect(table.getByText('项目申请人', { exact: true })).toBeVisible(); await expect(table.getByText('1,000.00 CNY', { exact: true })).toBeVisible(); await expect(table.getByText('当前节点', { exact: true })).toBeVisible(); await page.getByRole('button', { name: '重置', exact: true }).click(); await expect.poll(() => taskQueries.length).toBeGreaterThan(1); const resetQuery = taskQueries.at(-1)?.searchParams; expect(resetQuery?.has('projectId')).toBe(false); expect(resetQuery?.has('arrivedDateFrom')).toBe(false); expect(resetQuery?.has('arrivedDateTo')).toBe(false); expect(resetQuery?.has('overdue')).toBe(false); expect(resetQuery?.get('sort')).toBe('arrivedAt,asc'); }); test('PAGE-11 requires and sends the approver opinion', async ({ page }) => { const approveRequests: Array> = []; await installTaskFixture(page, { taskActions: ['APPROVE', 'RETURN', 'REJECT'], approveRequests }); await page.goto('/tasks?view=TODO'); await expect(page.getByRole('heading', { name: '待办审批', exact: true })).toBeVisible(); await page.getByText('通过', { exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '通过审批' }); await dialog.getByRole('button', { name: '确认通过', exact: true }).click(); await expect(dialog.getByText('请填写审批意见', { exact: true })).toBeVisible(); await dialog.locator('textarea').fill('资料与金额核对无误,同意通过'); await dialog.getByRole('button', { name: '确认通过', exact: true }).click(); await expect.poll(() => approveRequests.length).toBe(1); expect(approveRequests[0]).toEqual({ version: 0, opinion: '资料与金额核对无误,同意通过', returnPoints: [], }); await expect(dialog).toBeHidden(); await expect(page.getByText('审批已通过', { exact: true })).toBeVisible(); }); test('PAGE-11 loads fresh detail and sends selected return points from the list action', async ({ page }) => { const detailRequests: string[] = []; const returnRequests: Array> = []; await installTaskFixture(page, { taskActions: ['RETURN'], detailRequests, detailTaskVersion: 7, detailField: { code: 'supportingFile', label: '补充附件', dataType: 'FILE' }, returnRequests, }); await page.goto('/tasks?view=TODO'); await page.getByText('退回', { exact: true }).click(); await expect.poll(() => detailRequests.length).toBe(1); const dialog = page.locator('.t-dialog').filter({ hasText: '退回补件' }); const returnPoint = dialog.locator('label.t-checkbox').filter({ hasText: '补充附件(附件)' }); await expect(returnPoint).toBeVisible(); await returnPoint.click(); await dialog.locator('textarea').fill('请补充可核验的原始附件'); await dialog.getByRole('button', { name: '确认退回', exact: true }).click(); await expect.poll(() => returnRequests.length).toBe(1); expect(returnRequests[0]).toEqual({ version: 7, opinion: '请补充可核验的原始附件', returnPoints: ['supportingFile'], }); await expect.poll(() => detailRequests.length).toBe(2); await expect(page.getByText('已退回补件', { exact: true })).toBeVisible(); }); test('PAGE-11 exposes and sends withdraw only from form allowed actions', async ({ page }) => { const withdrawRequests: Array> = []; await installTaskFixture(page, { view: 'CREATED', formActions: ['WITHDRAW', 'VOID'], withdrawRequests, }); await page.goto('/tasks?view=CREATED'); await expect(page.getByText('撤回', { exact: true })).toBeVisible(); await expect(page.getByText('作废', { exact: true })).toBeVisible(); await page.getByText('撤回', { exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '撤回申请' }); await dialog.locator('textarea').fill('申请内容需要补充后重新提交'); await dialog.getByRole('button', { name: '确认撤回', exact: true }).click(); await expect.poll(() => withdrawRequests.length).toBe(1); expect(withdrawRequests[0]).toEqual({ version: 1, opinion: '申请内容需要补充后重新提交' }); await expect(page.getByText('申请已撤回', { exact: true })).toBeVisible(); }); test('PAGE-11 sends void with an explicit reason', async ({ page }) => { const voidRequests: Array> = []; await installTaskFixture(page, { view: 'CREATED', formActions: ['VOID'], voidRequests }); await page.goto('/tasks?view=CREATED'); await expect(page.getByText('撤回', { exact: true })).toHaveCount(0); await page.getByText('作废', { exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '作废申请' }); await dialog.locator('textarea').fill('该申请已由新版本替代'); await dialog.getByRole('button', { name: '确认作废', exact: true }).click(); await expect.poll(() => voidRequests.length).toBe(1); expect(voidRequests[0]).toEqual({ version: 1, opinion: '该申请已由新版本替代' }); await expect(page.getByText('申请已作废', { exact: true })).toBeVisible(); });