355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
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`));
|
|
});
|