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>; importRequests: Array>; confirmRequests: Array>; commandHeaders: Array>; } 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); 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); 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); 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> = []; const importRequests: Array> = []; const confirmRequests: Array> = []; const commandHeaders: Array> = []; 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(); });