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