250 lines
11 KiB
TypeScript
250 lines
11 KiB
TypeScript
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();
|
|
});
|