import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const projectId = '01M00000000000000000000001'; const formId = '01M00000000000000000000002'; const receivableId = '01M00000000000000000000009'; const accountingEventId = '01M00000000000000000000013'; const voucherId = '01M00000000000000000000014'; const permissions = [ 'project:project:view', 'source:form:view', 'contractcost:ledger:view', 'receivable:receipt:view', 'receivable:invoice:view', 'payment:request:view', 'accounting:event:view', 'accounting:voucher:view', 'archive:package:view', 'archive:file:view', ]; function envelope(data: unknown, totalElements = Array.isArray(data) ? data.length : 0) { return { data, meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 }, requestId: '01M00000000000000000000099', }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000003', username: 'finance', displayName: '财务测试用户', departmentName: '财务部', mustChangePassword: false, }, roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }], activeRole: 'FINANCE_MANAGER', permissions, }; } function project() { return { publicId: projectId, businessNo: 'P-DRILL-001', name: '穿透验证项目', description: '', company: { publicId: '01M00000000000000000000004', name: '测试公司' }, customer: null, owner: { publicId: '01M00000000000000000000005', name: '项目负责人' }, contractAmount: '100.00', receivedAmount: '50.00', paidAmount: '20.00', currency: 'CNY', stage: 'EXECUTION', financeStatus: 'IN_PROGRESS', archiveStatus: 'PREPARING', status: 'ACTIVE', startDate: '2026-08-01', endDate: null, version: 1, createdAt: '2026-08-01T00:00:00Z', updatedAt: '2026-08-15T00:00:00Z', overview: { contractAmount: '100.00', receivedAmount: '50.00', paidAmount: '20.00', availableBalance: '30.00', sourceFormCount: 1, contractCount: 0, receiptCount: 0, invoiceCount: 0, paymentCount: 1, progressCount: 0, peopleCount: 0, accountingCount: 1, voucherCount: 1, pendingTaskCount: 0, fileCount: 1, activeRiskCount: 0, documentCompleteness: 80, }, riskFlags: [], timeline: [], allowedActions: [], }; } async function fulfillApi(route: Route) { const request = route.request(); const url = new URL(request.url()); const pathname = url.pathname; if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(session()) }); if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(session().user) }); if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(session().roles) }); if (pathname === `/api/v1/projects/${projectId}`) return route.fulfill({ json: envelope(project()) }); if (pathname === `/api/v1/projects/${projectId}/forms`) { return route.fulfill({ json: envelope( [ { publicId: formId, businessNo: 'OA01-DRILL-001', formType: 'OA-01', formName: '项目立项表', applicantName: '申请人', status: 'APPROVED', businessDate: '2026-08-01', updatedAt: '2026-08-15T00:00:00Z', }, ], 1, ), }); } if (pathname === `/api/v1/projects/${projectId}/payments`) { return route.fulfill({ json: envelope( [ { publicId: '01M00000000000000000000006', businessNo: 'PAY-DRILL-001', supplier: { publicId: '01M00000000000000000000007', name: '测试供应商' }, requestedAmount: '20.00', purpose: '项目付款', status: 'OFFLINE_PENDING', requestedDate: '2026-08-15', }, ], 1, ), }); } if (pathname === `/api/v1/projects/${projectId}/timeline`) { const current = Number(url.searchParams.get('page') || '1'); const item = { publicId: current === 1 ? '01M00000000000000000000011' : '01M00000000000000000000012', actionCode: current === 1 ? 'PROJECT_CREATE' : 'PROJECT_UPDATE', resultCode: 'SUCCESS', reason: current === 1 ? '第一页操作记录' : '第二页操作记录', username: '项目操作人', activeRole: 'PROJECT_MANAGER', occurredAt: current === 1 ? '2026-08-15T08:00:00Z' : '2026-08-14T08:00:00Z', }; return route.fulfill({ json: { data: [item], meta: { page: current, size: 20, totalElements: 21, totalPages: 2 }, requestId: '01M00000000000000000000098', }, }); } if (pathname === `/api/v1/projects/${projectId}/receipts`) { if (url.searchParams.get('view') === 'receivables') { return route.fulfill({ json: envelope( [ { publicId: receivableId, businessNo: 'REC-DRILL-001', counterparty: { publicId: '01M00000000000000000000010', name: '测试客户' }, businessDate: '2026-08-15', amount: '50.00', settledAmount: '20.00', outstandingAmount: '30.00', currency: 'CNY', status: 'PARTIALLY_SETTLED', }, ], 1, ), }); } return route.fulfill({ json: envelope([]) }); } if (pathname === `/api/v1/projects/${projectId}/archives`) { if (url.searchParams.get('view') === 'files') { return route.fulfill({ json: envelope( [ { publicId: '01M00000000000000000000008', displayName: '付款审批单.pdf', originalType: 'PAYMENT_APPROVAL', archiveStatus: 'ACTIVE', uploadedAt: '2026-08-15T00:00:00Z', }, ], 1, ), }); } return route.fulfill({ json: envelope([]) }); } if (pathname === `/api/v1/projects/${projectId}/accounting`) { if (url.searchParams.get('view') === 'vouchers') { return route.fulfill({ json: envelope( [ { publicId: voucherId, businessNo: 'VCH-DRILL-001', eventPublicId: accountingEventId, eventBusinessNo: 'EV-DRILL-001', period: '2026-08', businessDate: '2026-08-15', debitTotal: '50.00', creditTotal: '50.00', status: 'COMPLETED', updatedAt: '2026-08-16T09:00:00Z', }, ], 1, ), }); } return route.fulfill({ json: envelope( [ { publicId: accountingEventId, businessNo: 'EV-DRILL-001', sourceType: 'RECEIPT', sourcePublicId: receivableId, sourceBusinessNo: 'REC-DRILL-001', voucherPublicId: voucherId, voucherStatus: 'COMPLETED', period: '2026-08', businessDate: '2026-08-15', amount: '50.00', status: 'COMPLETED', createdAt: '2026-08-15T09:00:00Z', }, ], 1, ), }); } if (pathname === '/api/v1/accounting/vouchers') { return route.fulfill({ json: envelope( [ { publicId: voucherId, businessNo: 'VCH-DRILL-001', eventPublicId: accountingEventId, eventBusinessNo: 'EV-DRILL-001', company: { publicId: '01M00000000000000000000004', code: 'GS-DRILL', name: '测试公司' }, project: { publicId: projectId, code: 'P-DRILL-001', name: '穿透验证项目' }, period: '2026-08', businessDate: '2026-08-15', summary: '项目记账完成结果', debitTotal: '50.00', creditTotal: '50.00', balanceStatus: 'BALANCED', status: 'COMPLETED', resultCycleNo: 0, evidenceFileIds: [], version: 5, createdAt: '2026-08-15T09:00:00Z', updatedAt: '2026-08-16T09:00:00Z', allowedActions: [], }, ], 1, ), }); } if (request.method() === 'GET') return route.fulfill({ json: envelope([]) }); return route.fulfill({ status: 204 }); } async function openProject(page: Page, tab: string) { await page.route('**/api/v1/**', fulfillApi); await page.goto(`/projects/${projectId}?tab=${tab}`); await expect(page.getByRole('heading', { name: '穿透验证项目', exact: true })).toBeVisible(); } test('source form row opens the exact PAGE-10 document and keeps the project return path', async ({ page }) => { await openProject(page, 'forms'); await page.getByRole('link', { name: '查看OA01-DRILL-001', exact: true }).click(); await expect(page).toHaveURL(new RegExp(`/forms/OA-01/${formId}\\?`)); const url = new URL(page.url()); expect(url.searchParams.get('from')).toBe(`/projects/${projectId}?tab=forms`); }); test('payment row opens PAGE-15 with project, business number and status tab', async ({ page }) => { await openProject(page, 'payments'); await page.getByRole('link', { name: '查看PAY-DRILL-001', exact: true }).click(); await expect(page).toHaveURL(/\/finance\/payments\?/); const url = new URL(page.url()); expect(url.searchParams.get('projectId')).toBe(projectId); expect(url.searchParams.get('keyword')).toBe('PAY-DRILL-001'); expect(url.searchParams.get('tab')).toBe('offline'); }); test('receivable row opens the PAGE-14 receivable ledger instead of the receipt tab', async ({ page }) => { await openProject(page, 'receipts'); await page.getByRole('link', { name: '查看REC-DRILL-001', exact: true }).click(); await expect(page).toHaveURL(/\/finance\/receipts-invoices\?/); const url = new URL(page.url()); expect(url.searchParams.get('projectId')).toBe(projectId); expect(url.searchParams.get('keyword')).toBe('REC-DRILL-001'); expect(url.searchParams.get('tab')).toBe('receivables'); }); test('archive file row opens PAGE-18 with project and exact file name', async ({ page }) => { await openProject(page, 'archives'); await page.getByRole('link', { name: '查看付款审批单.pdf', exact: true }).click(); await expect(page).toHaveURL(/\/archives\/files\?/); const url = new URL(page.url()); expect(url.searchParams.get('resource')).toBe('files'); expect(url.searchParams.get('projectId')).toBe(projectId); expect(url.searchParams.get('fileName')).toBe('付款审批单.pdf'); }); test('completed voucher remains visible in the project lifecycle and opens its exact PAGE-16 tab', async ({ page }) => { await openProject(page, 'accounting'); await expect(page.getByRole('link', { name: '查看VCH-DRILL-001', exact: true })).toBeVisible(); await page.getByRole('link', { name: '查看VCH-DRILL-001', exact: true }).click(); await expect(page).toHaveURL(/\/finance\/accounting\?/); const url = new URL(page.url()); expect(url.searchParams.get('projectId')).toBe(projectId); expect(url.searchParams.get('keyword')).toBe('VCH-DRILL-001'); expect(url.searchParams.get('tab')).toBe('completed'); expect(url.searchParams.get('from')).toBe(`/projects/${projectId}?tab=accounting`); }); test('accounting project link returns to PAGE-08 and preserves the accounting return path', async ({ page }) => { await page.route('**/api/v1/**', fulfillApi); await page.goto('/finance/accounting?tab=completed'); await expect(page.getByText('VCH-DRILL-001', { exact: true })).toBeVisible(); await page.getByRole('link', { name: 'P-DRILL-001 穿透验证项目', exact: true }).click(); await expect(page).toHaveURL(new RegExp(`/projects/${projectId}\\?`)); const url = new URL(page.url()); expect(url.searchParams.get('tab')).toBe('accounting'); expect(url.searchParams.get('from')).toContain('/finance/accounting'); await expect(page.getByRole('heading', { name: '穿透验证项目', exact: true })).toBeVisible(); }); test('timeline uses the paged PAGE-08 subresource beyond the first 20 records', async ({ page }) => { await openProject(page, 'timeline'); await expect(page.getByText('共 21 条操作记录', { exact: true })).toBeVisible(); await expect(page.getByText('第一页操作记录', { exact: true })).toBeVisible(); await page.locator('.t-pagination__number').filter({ hasText: /^2$/ }).click(); await expect(page.getByText('第二页操作记录', { exact: true })).toBeVisible(); await expect(page).toHaveURL(new RegExp(`/projects/${projectId}\\?tab=timeline`)); });