This commit is contained in:
@@ -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`));
|
||||
});
|
||||
Reference in New Issue
Block a user