This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
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<string, unknown> = {}) {
|
||||
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<string, unknown> = {}) {
|
||||
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<Record<string, unknown>>,
|
||||
submitRequests: Array<Record<string, unknown>>,
|
||||
updateRequests: Array<Record<string, unknown>> = [],
|
||||
uploadRequests: string[] = [],
|
||||
uploadGate?: Promise<void>,
|
||||
) {
|
||||
const userSession = session();
|
||||
let isValid = false;
|
||||
let isSubmitted = false;
|
||||
let currentValues: Record<string, unknown> = 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<string, unknown>;
|
||||
updateRequests.push(payload);
|
||||
currentValues = payload.values as Record<string, unknown>;
|
||||
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<string, unknown>);
|
||||
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<string, unknown>);
|
||||
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<Record<string, unknown>> = [];
|
||||
const submitRequests: Array<Record<string, unknown>> = [];
|
||||
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<Record<string, unknown>> = [];
|
||||
const validateRequests: Array<Record<string, unknown>> = [];
|
||||
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<string, unknown>).projectName).toBe('已更新的预览验收项目');
|
||||
});
|
||||
|
||||
test('PAGE-10 edits boolean matrix cells and uploads nested evidence files', async ({ page }) => {
|
||||
const updateRequests: Array<Record<string, unknown>> = [];
|
||||
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<string, unknown>;
|
||||
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<Record<string, unknown>> = [];
|
||||
const uploadRequests: string[] = [];
|
||||
let releaseUploads: () => void = () => undefined;
|
||||
const uploadGate = new Promise<void>((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');
|
||||
});
|
||||
Reference in New Issue
Block a user