import { Buffer } from 'node:buffer'; import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const documentId = '01M00000000000000000000230'; const companyId = '01M00000000000000000000231'; const projectId = '01M00000000000000000000232'; const nestedFileId = '01M00000000000000000000247'; function fields() { return [ { code: 'projectName', label: '项目名称', sectionCode: 'BASE', sectionName: '项目基础信息', dataType: 'TEXT', required: true, repeatable: false, maxLength: 200, options: [], sensitivity: 'NORMAL', sortOrder: 10, }, { code: 'budgetDetails', label: '预算明细', sectionCode: 'BUDGET', sectionName: '预算信息', dataType: 'TABLE', required: true, repeatable: true, options: [ { code: 'costType', label: '费用类型' }, { code: 'amount', label: '金额', dataType: 'DECIMAL' }, ], sensitivity: 'INTERNAL', sortOrder: 20, }, { code: 'supportingFiles', label: '立项附件', sectionCode: 'ATTACHMENT', sectionName: '附件信息', dataType: 'FILE', required: true, repeatable: true, options: [], sensitivity: 'INTERNAL', sortOrder: 30, }, { code: 'attachmentMatrix', label: '条件附件矩阵', sectionCode: 'ATTACHMENT', sectionName: '附件信息', dataType: 'TABLE', required: true, repeatable: true, options: [ { code: 'attachmentType', label: '附件类型', dataType: 'TEXT', required: true, maxLength: 100 }, { code: 'required', label: '是否必需', dataType: 'BOOLEAN', required: true }, { code: 'fileId', label: '文件', dataType: 'FILE', required: false }, { code: 'notApplicableReason', label: '不涉及原因', dataType: 'LONG_TEXT', required: false, maxLength: 500 }, ], sensitivity: 'INTERNAL', sortOrder: 40, }, ]; } function summary(overrides: Record = {}) { return { publicId: documentId, businessNo: 'OA01-E2E-PREVIEW', formType: 'OA-01', formName: '项目立项申请', templateVersion: 1, sourceSystem: 'MANUAL', sourceNo: 'SRC-OA01-PREVIEW', sourceVersion: '1', company: { publicId: companyId, name: '凯迪测试公司' }, project: { publicId: projectId, name: '预览验收项目' }, applicantName: '项目经办人', businessDate: '2026-08-15', status: 'DRAFT', currentNodeCode: null, currentNodeName: null, version: 1, createdByName: '项目经办人', createdAt: '2026-08-15T08:00:00Z', updatedAt: '2026-08-15T08:00:00Z', allowedActions: ['EDIT', 'VALIDATE', 'SUBMIT', 'VOID'], ...overrides, }; } function validation(valid: boolean) { return { documentId, version: 1, valid, canSubmit: valid, errors: valid ? [] : [{ level: 'ERROR', sectionCode: 'BASE', fieldCode: 'projectName', message: '项目名称为必填项' }], warnings: [], validatedAt: '2026-08-15T08:01:00Z', }; } function detail(valid: boolean, submitted = false, valuesOverride: Record = {}) { return { summary: summary( submitted ? { status: 'PROJECT_REVIEW', currentNodeCode: 'PROJECT_REVIEW', currentNodeName: '项目复核', version: 2, allowedActions: ['WITHDRAW', 'VOID'], } : {}, ), documentVersion: 2, snapshotSha256: 'b'.repeat(64), values: { projectName: '预览验收项目', budgetDetails: [{ costType: '材料', amount: 1000 }], supportingFiles: ['01M00000000000000000000233'], attachmentMatrix: [{ attachmentType: '发票及验收资料', required: false, fileId: '' }], ...valuesOverride, }, fields: fields(), approvals: [], validation: validation(valid), }; } function template() { return { publicId: '01M00000000000000000000234', formType: 'OA-01', templateVersion: 1, name: '项目立项申请', groupCode: 'PROJECT_PREPARATION', groupName: '项目准备', purpose: '形成项目基础资料', initiatorRoles: ['PROJECT_MANAGER'], status: 'ACTIVE', publishedAt: '2026-08-15T08:00:00Z', myDraftCount: 1, myPendingCount: 0, canCreate: true, fields: fields(), }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000235', username: 'editor-e2e', displayName: '项目经办人', departmentName: '项目部', mustChangePassword: false, }, roles: [{ code: 'PROJECT_MANAGER', name: '项目管理人员', workbenchRoute: '/workbench/project' }], activeRole: 'PROJECT_MANAGER', permissions: ['source:template:view', 'source:form:view', 'source:form:edit', 'source:form:submit'], }; } async function installEditorFixture( page: Page, validateRequests: Array>, submitRequests: Array>, updateRequests: Array> = [], uploadRequests: string[] = [], uploadGate?: Promise, ) { const userSession = session(); let isValid = false; let isSubmitted = false; let currentValues: Record = detail(false).values; await page.route('**/api/v1/**', async (route: Route) => { const request = route.request(); const pathname = new URL(request.url()).pathname; if (pathname === '/api/v1/auth/session') { await route.fulfill({ json: { data: userSession, requestId: '01M00000000000000000000236' } }); return; } if (pathname === '/api/v1/auth/profile') { await route.fulfill({ json: { data: userSession.user, requestId: '01M00000000000000000000237' } }); return; } if (pathname === '/api/v1/auth/roles') { await route.fulfill({ json: { data: userSession.roles, requestId: '01M00000000000000000000238' } }); return; } if (pathname === '/api/v1/auth/csrf') { await route.fulfill({ headers: { 'set-cookie': 'XSRF-TOKEN=editor-e2e-csrf; Path=/' }, json: { data: { token: 'editor-e2e-csrf' }, requestId: '01M00000000000000000000239' }, }); return; } if (request.method() === 'GET' && pathname === '/api/v1/source/templates') { await route.fulfill({ json: { data: [template()], requestId: '01M00000000000000000000240' } }); return; } if (request.method() === 'GET' && pathname === '/api/v1/source/templates/OA-01/versions/1') { await route.fulfill({ json: { data: template(), requestId: '01M00000000000000000000241' } }); return; } if (request.method() === 'GET' && pathname === '/api/v1/source/references') { await route.fulfill({ json: { data: { companies: [{ publicId: companyId, name: '凯迪测试公司' }], projects: [{ publicId: projectId, name: '预览验收项目' }], suppliers: [], customers: [], contracts: [], costCategories: [], }, requestId: '01M00000000000000000000242', }, }); return; } if (request.method() === 'GET' && pathname === `/api/v1/source/forms/${documentId}`) { await route.fulfill({ json: { data: detail(isValid, isSubmitted, currentValues), requestId: '01M00000000000000000000243' }, }); return; } if (request.method() === 'PATCH' && pathname === `/api/v1/source/forms/${documentId}`) { const payload = request.postDataJSON() as Record; updateRequests.push(payload); currentValues = payload.values as Record; await route.fulfill({ json: { data: detail(false, false, currentValues), requestId: '01M00000000000000000000248' }, }); return; } if (request.method() === 'POST' && pathname === '/api/v1/files') { uploadRequests.push((await request.headerValue('content-type')) || ''); if (uploadGate) await uploadGate; await route.fulfill({ json: { data: { publicId: nestedFileId, originalName: 'matrix-evidence.pdf', mediaType: 'application/pdf', extension: 'pdf', sizeBytes: 24, sha256: 'd'.repeat(64), scanStatus: 'AVAILABLE', createdAt: '2026-08-15T08:02:00Z', }, requestId: '01M00000000000000000000249', }, }); return; } if (request.method() === 'POST' && pathname === `/api/v1/source/forms/${documentId}/validate`) { validateRequests.push((request.postDataJSON() || {}) as Record); isValid = true; await route.fulfill({ json: { data: validation(true), requestId: '01M00000000000000000000244' } }); return; } if (request.method() === 'POST' && pathname === `/api/v1/source/forms/${documentId}/submit`) { submitRequests.push(request.postDataJSON() as Record); isSubmitted = true; await route.fulfill({ json: { data: detail(true, true, currentValues), requestId: '01M00000000000000000000245' }, }); return; } if (request.method() === 'GET') { await route.fulfill({ json: { data: [], meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 }, requestId: '01M00000000000000000000246', }, }); return; } await route.fulfill({ status: 204 }); }); } test('PAGE-10 validates, previews the saved snapshot and confirms submission', async ({ page }) => { const validateRequests: Array> = []; const submitRequests: Array> = []; await installEditorFixture(page, validateRequests, submitRequests); await page.goto(`/forms/OA-01/${documentId}`); await expect(page.getByRole('heading', { name: '项目立项申请', exact: true })).toBeVisible(); await page.getByRole('button', { name: '执行校验', exact: true }).click(); await expect(page.getByRole('button', { name: '预览并提交', exact: true })).toBeVisible(); await page.getByRole('button', { name: '预览并提交', exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '提交预览' }); await expect(dialog.getByText('OA01-E2E-PREVIEW', { exact: true })).toBeVisible(); await expect(dialog.getByText('预览验收项目', { exact: true }).first()).toBeVisible(); await expect(dialog.getByText('第 1 行:费用类型:材料;金额:1000', { exact: true })).toBeVisible(); await expect(dialog.getByText('项目复核', { exact: true })).toBeVisible(); await expect(submitRequests).toHaveLength(0); await dialog.getByRole('button', { name: '确认提交', exact: true }).click(); await expect.poll(() => submitRequests.length).toBe(1); expect(submitRequests[0]).toEqual({ version: 1 }); expect(validateRequests).toHaveLength(3); await expect(dialog).toBeHidden(); await expect(page.getByText('已提交审批', { exact: true })).toBeVisible(); }); test('PAGE-10 saves current edits before validating the server snapshot', async ({ page }) => { const updateRequests: Array> = []; const validateRequests: Array> = []; await installEditorFixture(page, validateRequests, [], updateRequests); await page.goto(`/forms/OA-01/${documentId}`); const projectNameInput = page.locator('.field-section').first().locator('input').first(); await projectNameInput.fill('已更新的预览验收项目'); await page.getByRole('button', { name: '执行校验', exact: true }).click(); await expect.poll(() => updateRequests.length).toBe(1); await expect.poll(() => validateRequests.length).toBe(1); expect((updateRequests[0].values as Record).projectName).toBe('已更新的预览验收项目'); }); test('PAGE-10 edits boolean matrix cells and uploads nested evidence files', async ({ page }) => { const updateRequests: Array> = []; const uploadRequests: string[] = []; await installEditorFixture(page, [], [], updateRequests, uploadRequests); await page.goto(`/forms/OA-01/${documentId}`); const matrixRow = page.locator('.detail-row').filter({ hasText: '是否必需' }); await expect(matrixRow).toBeVisible(); const requiredSwitch = matrixRow.locator('.t-switch'); await expect(requiredSwitch).toContainText('否'); await requiredSwitch.click(); await expect(requiredSwitch).toContainText('是'); await matrixRow.locator('input[type="file"]').setInputFiles({ name: 'matrix-evidence.pdf', mimeType: 'application/pdf', buffer: Buffer.from('nested matrix evidence'), }); await matrixRow.getByRole('button', { name: '上传附件', exact: true }).click(); await expect.poll(() => uploadRequests.length).toBe(1); expect(uploadRequests[0]).toContain('multipart/form-data'); await expect(matrixRow.getByText(nestedFileId, { exact: true })).toBeVisible(); await page.getByRole('button', { name: '保存草稿', exact: true }).first().click(); await expect.poll(() => updateRequests.length).toBe(1); const savedValues = updateRequests[0].values as Record; expect(updateRequests[0]).not.toHaveProperty('formType'); expect(updateRequests[0]).not.toHaveProperty('templateVersion'); expect(updateRequests[0]).not.toHaveProperty('sourceSystem'); expect(savedValues.attachmentMatrix).toEqual([ { attachmentType: '发票及验收资料', required: true, fileId: nestedFileId, }, ]); }); test('PAGE-10 keeps concurrent upload states isolated and blocks stale draft saves', async ({ page }) => { const updateRequests: Array> = []; const uploadRequests: string[] = []; let releaseUploads: () => void = () => undefined; const uploadGate = new Promise((resolve) => { releaseUploads = resolve; }); await installEditorFixture(page, [], [], updateRequests, uploadRequests, uploadGate); await page.goto(`/forms/OA-01/${documentId}`); const topLevelFile = page.locator('.file-field').first(); const matrixRow = page.locator('.detail-row').filter({ hasText: '是否必需' }); await topLevelFile.locator('input[type="file"]').setInputFiles({ name: 'project-support.pdf', mimeType: 'application/pdf', buffer: Buffer.from('project support'), }); await matrixRow.locator('input[type="file"]').setInputFiles({ name: 'matrix-evidence.pdf', mimeType: 'application/pdf', buffer: Buffer.from('nested matrix evidence'), }); const topLevelUpload = topLevelFile.getByRole('button', { name: '上传文件', exact: true }); const matrixUpload = matrixRow.getByRole('button', { name: '上传附件', exact: true }); await topLevelUpload.click(); await matrixUpload.click(); await expect.poll(() => uploadRequests.length).toBe(2); await expect(topLevelUpload).toBeDisabled(); await expect(matrixUpload).toBeDisabled(); await expect(page.getByRole('button', { name: '保存草稿', exact: true }).first()).toBeDisabled(); expect(updateRequests).toHaveLength(0); releaseUploads(); await expect(matrixRow.getByText(nestedFileId, { exact: true })).toBeVisible(); await expect(topLevelFile.getByText(nestedFileId, { exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: '保存草稿', exact: true }).first()).toBeEnabled(); await page.getByRole('button', { name: '保存草稿', exact: true }).first().click(); await expect.poll(() => updateRequests.length).toBe(1); expect(updateRequests[0]).not.toHaveProperty('formType'); expect(updateRequests[0]).not.toHaveProperty('templateVersion'); expect(updateRequests[0]).not.toHaveProperty('sourceSystem'); });