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
+249
View File
@@ -0,0 +1,249 @@
import { Buffer } from 'node:buffer';
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import type { Page, Route } from '@playwright/test';
import { expect, test } from '@playwright/test';
const companyId = '01M00000000000000000000501';
const customerId = '01M00000000000000000000502';
const ownerId = '01M00000000000000000000503';
const projectId = '01M00000000000000000000504';
const importedProjectId = '01M00000000000000000000505';
const sourceFileId = '01M00000000000000000000506';
const importId = '01M00000000000000000000507';
const sourceFileSha256 = 'a'.repeat(64);
function envelope(data: unknown, totalElements?: number) {
return {
data,
...(typeof totalElements === 'number'
? { meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 } }
: {}),
requestId: '01M00000000000000000000599',
};
}
function session() {
return {
authenticated: true,
user: {
publicId: '01M00000000000000000000508',
username: 'project-e2e',
displayName: '项目管理员',
departmentName: '项目部',
mustChangePassword: false,
},
roles: [{ code: 'PROJECT_MANAGER', name: '项目管理人员', workbenchRoute: '/workbench/project' }],
activeRole: 'PROJECT_MANAGER',
permissions: ['project:project:view', 'project:project:create', 'project:project:export', 'file:upload'],
};
}
function project(publicId = projectId, businessNo = 'XM-E2E-007', name = '筛选项目验收') {
return {
publicId,
businessNo,
name,
company: { publicId: companyId, name: '凯迪测试公司' },
customer: { publicId: customerId, name: '测试客户' },
owner: { publicId: ownerId, name: '项目负责人' },
contractAmount: '1000.00',
receivedAmount: '800.00',
paidAmount: '300.00',
currency: 'CNY',
stage: 'EXECUTION',
financeStatus: 'IN_PROGRESS',
archiveStatus: 'PREPARING',
status: 'ACTIVE',
pendingTaskCount: 2,
documentCompleteness: 75,
startDate: '2026-08-01',
endDate: '2026-12-31',
version: 1,
updatedAt: '2026-08-16T08:00:00Z',
allowedActions: ['EDIT'],
};
}
interface FixtureOptions {
projectQueries: URLSearchParams[];
exportRequests: Array<Record<string, unknown>>;
importRequests: Array<Record<string, unknown>>;
confirmRequests: Array<Record<string, unknown>>;
commandHeaders: Array<Record<string, string>>;
}
async function installFixture(page: Page, options: FixtureOptions) {
const userSession = session();
let projects = [project()];
const exportContent = '项目编号,项目名称\nXM-E2E-007,筛选项目验收\n';
const exportSha256 = createHash('sha256').update(exportContent).digest('hex');
await page.route('**/api/v1/**', async (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(userSession) });
if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(userSession.user) });
if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(userSession.roles) });
if (pathname === '/api/v1/auth/csrf') {
return route.fulfill({
headers: { 'set-cookie': 'XSRF-TOKEN=project-e2e-csrf; Path=/' },
json: envelope({ token: 'project-e2e-csrf' }),
});
}
if (request.method() === 'GET' && pathname === '/api/v1/projects/references') {
return route.fulfill({
json: envelope({
companies: [{ publicId: companyId, name: '凯迪测试公司' }],
customers: [{ publicId: customerId, name: '测试客户' }],
owners: [{ publicId: ownerId, name: '项目负责人' }],
}),
});
}
if (request.method() === 'GET' && pathname === '/api/v1/projects') {
options.projectQueries.push(new URLSearchParams(url.searchParams));
return route.fulfill({ json: envelope(projects, projects.length) });
}
if (request.method() === 'POST' && pathname === '/api/v1/projects/exports') {
options.exportRequests.push(request.postDataJSON() as Record<string, unknown>);
options.commandHeaders.push(request.headers());
return route.fulfill({
json: envelope({
exportId: 'EXP-E2E-007',
rowCount: 1,
sha256: exportSha256,
filterHash: 'b'.repeat(64),
columns: ['businessNo', 'name'],
fileName: '项目导出-E2E.csv',
content: exportContent,
}),
});
}
if (request.method() === 'POST' && pathname === '/api/v1/files') {
return route.fulfill({
json: envelope({
publicId: sourceFileId,
originalName: 'projects.csv',
mediaType: 'text/csv',
extension: 'csv',
sizeBytes: 160,
sha256: sourceFileSha256,
scanStatus: 'AVAILABLE',
createdAt: '2026-08-16T08:10:00Z',
}),
});
}
if (request.method() === 'POST' && pathname === '/api/v1/projects/imports') {
options.importRequests.push(request.postDataJSON() as Record<string, unknown>);
options.commandHeaders.push(request.headers());
return route.fulfill({
json: envelope({
publicId: importId,
batchNo: 'IMP-E2E-007',
sourceFileId,
fileSha256: sourceFileSha256,
totalRows: 1,
validRows: 1,
errorRows: 0,
status: 'READY',
version: 0,
createdAt: '2026-08-16T08:11:00Z',
updatedAt: '2026-08-16T08:11:00Z',
issues: [],
}),
});
}
if (request.method() === 'POST' && pathname === `/api/v1/projects/imports/${importId}/confirm`) {
options.confirmRequests.push(request.postDataJSON() as Record<string, unknown>);
options.commandHeaders.push(request.headers());
const imported = project(importedProjectId, 'XM-IMP-007', '筛选项目批量导入');
projects = [project(), imported];
return route.fulfill({ json: envelope([imported]) });
}
if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) });
return route.fulfill({ status: 204 });
});
}
test('PAGE-07 filters, verifies an export, and confirms a validated CSV import', async ({ page }) => {
const projectQueries: URLSearchParams[] = [];
const exportRequests: Array<Record<string, unknown>> = [];
const importRequests: Array<Record<string, unknown>> = [];
const confirmRequests: Array<Record<string, unknown>> = [];
const commandHeaders: Array<Record<string, string>> = [];
await installFixture(page, { projectQueries, exportRequests, importRequests, confirmRequests, commandHeaders });
await page.goto('/projects');
await expect(page.getByRole('heading', { name: '项目列表', exact: true })).toBeVisible();
await expect(page.getByText('XM-E2E-007', { exact: true }).first()).toBeVisible();
await page.getByPlaceholder('项目编号或名称', { exact: true }).fill('筛选项目');
await page.getByPlaceholder('全部公司', { exact: true }).click();
await page.locator('.t-select-option').filter({ hasText: '凯迪测试公司' }).click();
await page.getByPlaceholder('全部阶段', { exact: true }).click();
await page.locator('.t-select-option').filter({ hasText: '执行' }).click();
await page.getByRole('textbox', { name: '全部状态', exact: true }).nth(2).click();
await page.locator('.t-select-option').filter({ hasText: '进行中' }).click();
await page.getByRole('button', { name: '查询', exact: true }).click();
await expect.poll(() => projectQueries.length).toBeGreaterThan(1);
const filteredQuery = projectQueries.at(-1)!;
expect(filteredQuery.get('keyword')).toBe('筛选项目');
expect(filteredQuery.get('companyId')).toBe(companyId);
expect(filteredQuery.get('stage')).toBe('EXECUTION');
expect(filteredQuery.get('status')).toBe('ACTIVE');
expect(filteredQuery.get('sort')).toBe('updatedAt,desc');
expect(filteredQuery.get('page')).toBe('1');
expect(filteredQuery.get('size')).toBe('20');
await expect(page).toHaveURL(/keyword=%E7%AD%9B%E9%80%89%E9%A1%B9%E7%9B%AE/);
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: '导出当前结果', exact: true }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('项目导出-E2E.csv');
const downloadPath = await download.path();
expect(downloadPath).toBeTruthy();
expect(await readFile(downloadPath!, 'utf8')).toBe('项目编号,项目名称\nXM-E2E-007,筛选项目验收\n');
await expect.poll(() => exportRequests.length).toBe(1);
expect(exportRequests[0]).toMatchObject({
keyword: '筛选项目',
companyId,
stage: 'EXECUTION',
status: 'ACTIVE',
sort: 'updatedAt,desc',
});
expect(exportRequests[0]).not.toHaveProperty('page');
expect(exportRequests[0]).not.toHaveProperty('size');
expect(exportRequests[0].columns).toHaveLength(11);
expect(commandHeaders[0]['x-xsrf-token']).toBe('project-e2e-csrf');
const initialListRequests = projectQueries.length;
await page.getByRole('button', { name: '导入项目', exact: true }).click();
const dialog = page.locator('.t-dialog').filter({ hasText: '批量导入项目' });
await dialog.locator('input[type="file"]').setInputFiles({
name: 'projects.csv',
mimeType: 'text/csv',
buffer: Buffer.from(
'项目编号,项目名称,公司名称或公共ID,负责人名称或公共ID,币种\nXM-IMP-007,筛选项目批量导入,凯迪测试公司,项目负责人,CNY\n',
),
});
await dialog.getByRole('button', { name: '上传并校验', exact: true }).click();
await expect(dialog.getByText(/批次 IMP-E2E-007:共 1 行,有效 1 行,错误 0 行。/)).toBeVisible();
await expect.poll(() => importRequests.length).toBe(1);
expect(importRequests[0]).toEqual({ sourceFileId, fileSha256: sourceFileSha256 });
expect(commandHeaders[1]['x-xsrf-token']).toBe('project-e2e-csrf');
expect(commandHeaders[1]['idempotency-key']).toBeTruthy();
await dialog.getByRole('button', { name: '确认创建 1 个项目', exact: true }).click();
await expect.poll(() => confirmRequests.length).toBe(1);
expect(confirmRequests[0]).toEqual({ version: 0 });
expect(commandHeaders[2]['x-xsrf-token']).toBe('project-e2e-csrf');
expect(commandHeaders[2]['idempotency-key']).toBeTruthy();
expect(commandHeaders[2]['idempotency-key']).not.toBe(commandHeaders[1]['idempotency-key']);
await expect(dialog).toBeHidden();
await expect.poll(() => projectQueries.length).toBeGreaterThan(initialListRequests);
await expect(page.getByText('XM-IMP-007', { exact: true }).first()).toBeVisible();
await expect(page.getByText('已创建 1 个项目草稿', { exact: true })).toBeVisible();
});