feat: deliver first finance preview with setup wizard
Release / release (push) Failing after 18s

This commit is contained in:
Qiufeng
2026-08-17 14:08:40 +08:00
parent 47403fd337
commit c8b0151769
558 changed files with 140774 additions and 3 deletions
@@ -0,0 +1,354 @@
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`));
});