This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
import type { Page, Route } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const taskId = '01M00000000000000000000190';
|
||||
const instanceId = '01M00000000000000000000191';
|
||||
const formId = '01M00000000000000000000192';
|
||||
const companyId = '01M00000000000000000000193';
|
||||
const projectId = '01M00000000000000000000194';
|
||||
|
||||
interface TaskFixtureOptions {
|
||||
view?: 'TODO' | 'CREATED';
|
||||
taskActions?: string[];
|
||||
formActions?: string[];
|
||||
approveRequests?: Array<Record<string, unknown>>;
|
||||
returnRequests?: Array<Record<string, unknown>>;
|
||||
withdrawRequests?: Array<Record<string, unknown>>;
|
||||
voidRequests?: Array<Record<string, unknown>>;
|
||||
detailRequests?: string[];
|
||||
detailTaskVersion?: number;
|
||||
detailField?: { code: string; label: string; dataType?: string };
|
||||
taskQueries?: URL[];
|
||||
}
|
||||
|
||||
function sourceForm(allowedActions: string[] = []) {
|
||||
return {
|
||||
publicId: formId,
|
||||
businessNo: 'OA05-E2E-001',
|
||||
formType: 'OA-05',
|
||||
formName: '收款确认单',
|
||||
templateVersion: 2,
|
||||
sourceSystem: 'ONLINE',
|
||||
sourceNo: 'SRC-E2E-001',
|
||||
sourceVersion: '1',
|
||||
company: { publicId: companyId, name: '凯迪测试公司' },
|
||||
project: { publicId: projectId, name: '审批验收项目' },
|
||||
applicantName: '项目申请人',
|
||||
businessDate: '2026-08-15',
|
||||
status: 'PROJECT_REVIEW',
|
||||
currentNodeCode: 'PROJECT_REVIEW',
|
||||
currentNodeName: '项目复核',
|
||||
version: 1,
|
||||
createdByName: '项目申请人',
|
||||
createdAt: '2026-08-15T08:00:00Z',
|
||||
updatedAt: '2026-08-15T08:00:00Z',
|
||||
allowedActions,
|
||||
};
|
||||
}
|
||||
|
||||
function workflowTask(taskActions: string[], formActions: string[]) {
|
||||
return {
|
||||
publicId: taskId,
|
||||
instancePublicId: instanceId,
|
||||
nodeCode: 'PROJECT_REVIEW',
|
||||
nodeName: '项目复核',
|
||||
sequenceNo: 1,
|
||||
status: 'PENDING',
|
||||
version: 0,
|
||||
assigneeName: '项目审批人',
|
||||
arrivedAt: '2026-08-15T08:01:00Z',
|
||||
dueAt: '2026-08-18T08:01:00Z',
|
||||
updatedAt: '2026-08-15T08:01:00Z',
|
||||
keyAmount: '1000.00',
|
||||
form: sourceForm(formActions),
|
||||
allowedActions: taskActions,
|
||||
};
|
||||
}
|
||||
|
||||
function taskDetail(
|
||||
taskActions: string[],
|
||||
formActions: string[],
|
||||
taskVersion = 0,
|
||||
detailField: { code: string; label: string; dataType?: string } = {
|
||||
code: 'receiptAmount',
|
||||
label: '收款金额',
|
||||
dataType: 'DECIMAL',
|
||||
},
|
||||
) {
|
||||
const task = workflowTask(taskActions, formActions);
|
||||
task.version = taskVersion;
|
||||
return {
|
||||
task,
|
||||
form: {
|
||||
summary: task.form,
|
||||
documentVersion: 1,
|
||||
snapshotSha256: 'a'.repeat(64),
|
||||
values: { receiptAmount: '1000.00' },
|
||||
fields: [
|
||||
{
|
||||
code: detailField.code,
|
||||
label: detailField.label,
|
||||
sectionCode: 'base',
|
||||
sectionName: '基本信息',
|
||||
dataType: detailField.dataType || 'DECIMAL',
|
||||
required: true,
|
||||
repeatable: false,
|
||||
options: [],
|
||||
sensitivity: 'NORMAL',
|
||||
sortOrder: 1,
|
||||
},
|
||||
],
|
||||
approvals: [],
|
||||
validation: {
|
||||
documentId: formId,
|
||||
version: 1,
|
||||
valid: true,
|
||||
canSubmit: false,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
validatedAt: '2026-08-15T08:00:30Z',
|
||||
},
|
||||
},
|
||||
timeline: [],
|
||||
};
|
||||
}
|
||||
|
||||
function session() {
|
||||
return {
|
||||
authenticated: true,
|
||||
user: {
|
||||
publicId: '01M00000000000000000000195',
|
||||
username: 'task-e2e',
|
||||
displayName: '审批验收用户',
|
||||
departmentName: '项目部',
|
||||
mustChangePassword: false,
|
||||
},
|
||||
roles: [{ code: 'PROJECT_MANAGER', name: '项目管理人员', workbenchRoute: '/workbench/project' }],
|
||||
activeRole: 'PROJECT_MANAGER',
|
||||
permissions: ['workflow:task:view', 'workflow:task:approve', 'source:form:withdraw', 'source:form:void'],
|
||||
};
|
||||
}
|
||||
|
||||
async function installTaskFixture(page: Page, options: TaskFixtureOptions = {}) {
|
||||
const userSession = session();
|
||||
let taskActions = options.taskActions || [];
|
||||
let formActions = options.formActions || [];
|
||||
const currentTaskDetail = () => taskDetail(taskActions, formActions, options.detailTaskVersion, options.detailField);
|
||||
|
||||
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: '01M00000000000000000000196' } });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/auth/profile') {
|
||||
await route.fulfill({ json: { data: userSession.user, requestId: '01M00000000000000000000197' } });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/auth/roles') {
|
||||
await route.fulfill({ json: { data: userSession.roles, requestId: '01M00000000000000000000198' } });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/auth/csrf') {
|
||||
await route.fulfill({
|
||||
headers: { 'set-cookie': 'XSRF-TOKEN=task-e2e-csrf; Path=/' },
|
||||
json: { data: { token: 'task-e2e-csrf' }, requestId: '01M00000000000000000000199' },
|
||||
});
|
||||
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: '01M00000000000000000000206',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/workflow/tasks') {
|
||||
options.taskQueries?.push(new URL(request.url()));
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: [workflowTask(taskActions, formActions)],
|
||||
meta: { page: 1, size: 20, totalElements: 1, totalPages: 1 },
|
||||
requestId: '01M00000000000000000000200',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === `/api/v1/workflow/tasks/${taskId}`) {
|
||||
options.detailRequests?.push(taskId);
|
||||
await route.fulfill({
|
||||
json: { data: currentTaskDetail(), requestId: '01M00000000000000000000201' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/workflow/tasks/${taskId}/approve`) {
|
||||
options.approveRequests?.push(request.postDataJSON() as Record<string, unknown>);
|
||||
taskActions = [];
|
||||
await route.fulfill({
|
||||
json: { data: currentTaskDetail(), requestId: '01M00000000000000000000202' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/workflow/tasks/${taskId}/return`) {
|
||||
options.returnRequests?.push(request.postDataJSON() as Record<string, unknown>);
|
||||
taskActions = [];
|
||||
await route.fulfill({
|
||||
json: { data: currentTaskDetail(), requestId: '01M00000000000000000000207' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/source/forms/${formId}/withdraw`) {
|
||||
options.withdrawRequests?.push(request.postDataJSON() as Record<string, unknown>);
|
||||
formActions = [];
|
||||
await route.fulfill({
|
||||
json: { data: currentTaskDetail().form, requestId: '01M00000000000000000000203' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/source/forms/${formId}/void`) {
|
||||
options.voidRequests?.push(request.postDataJSON() as Record<string, unknown>);
|
||||
formActions = [];
|
||||
await route.fulfill({
|
||||
json: { data: currentTaskDetail().form, requestId: '01M00000000000000000000204' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET') {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: [],
|
||||
meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 },
|
||||
requestId: '01M00000000000000000000205',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.fulfill({ status: 204 });
|
||||
});
|
||||
}
|
||||
|
||||
test('PAGE-11 applies project, arrival date, overdue and sort filters and renders the required columns', async ({
|
||||
page,
|
||||
}) => {
|
||||
const taskQueries: URL[] = [];
|
||||
await installTaskFixture(page, { taskQueries });
|
||||
|
||||
await page.goto(
|
||||
`/tasks?view=TODO&projectId=${projectId}&arrivedDateFrom=2026-08-15&arrivedDateTo=2026-08-16&overdue=true&sort=dueAt%2Casc`,
|
||||
);
|
||||
|
||||
await expect.poll(() => taskQueries.length).toBeGreaterThan(0);
|
||||
const initialQuery = taskQueries.at(-1)?.searchParams;
|
||||
expect(initialQuery?.get('projectId')).toBe(projectId);
|
||||
expect(initialQuery?.get('arrivedDateFrom')).toBe('2026-08-15');
|
||||
expect(initialQuery?.get('arrivedDateTo')).toBe('2026-08-16');
|
||||
expect(initialQuery?.get('overdue')).toBe('true');
|
||||
expect(initialQuery?.get('sort')).toBe('dueAt,asc');
|
||||
|
||||
const table = page.locator('.t-table');
|
||||
await expect(table.getByText('审批验收项目', { exact: true })).toBeVisible();
|
||||
await expect(table.getByText('项目申请人', { exact: true })).toBeVisible();
|
||||
await expect(table.getByText('1,000.00 CNY', { exact: true })).toBeVisible();
|
||||
await expect(table.getByText('当前节点', { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '重置', exact: true }).click();
|
||||
await expect.poll(() => taskQueries.length).toBeGreaterThan(1);
|
||||
const resetQuery = taskQueries.at(-1)?.searchParams;
|
||||
expect(resetQuery?.has('projectId')).toBe(false);
|
||||
expect(resetQuery?.has('arrivedDateFrom')).toBe(false);
|
||||
expect(resetQuery?.has('arrivedDateTo')).toBe(false);
|
||||
expect(resetQuery?.has('overdue')).toBe(false);
|
||||
expect(resetQuery?.get('sort')).toBe('arrivedAt,asc');
|
||||
});
|
||||
|
||||
test('PAGE-11 requires and sends the approver opinion', async ({ page }) => {
|
||||
const approveRequests: Array<Record<string, unknown>> = [];
|
||||
await installTaskFixture(page, { taskActions: ['APPROVE', 'RETURN', 'REJECT'], approveRequests });
|
||||
|
||||
await page.goto('/tasks?view=TODO');
|
||||
await expect(page.getByRole('heading', { name: '待办审批', exact: true })).toBeVisible();
|
||||
await page.getByText('通过', { exact: true }).click();
|
||||
|
||||
const dialog = page.locator('.t-dialog').filter({ hasText: '通过审批' });
|
||||
await dialog.getByRole('button', { name: '确认通过', exact: true }).click();
|
||||
await expect(dialog.getByText('请填写审批意见', { exact: true })).toBeVisible();
|
||||
await dialog.locator('textarea').fill('资料与金额核对无误,同意通过');
|
||||
await dialog.getByRole('button', { name: '确认通过', exact: true }).click();
|
||||
|
||||
await expect.poll(() => approveRequests.length).toBe(1);
|
||||
expect(approveRequests[0]).toEqual({
|
||||
version: 0,
|
||||
opinion: '资料与金额核对无误,同意通过',
|
||||
returnPoints: [],
|
||||
});
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect(page.getByText('审批已通过', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-11 loads fresh detail and sends selected return points from the list action', async ({ page }) => {
|
||||
const detailRequests: string[] = [];
|
||||
const returnRequests: Array<Record<string, unknown>> = [];
|
||||
await installTaskFixture(page, {
|
||||
taskActions: ['RETURN'],
|
||||
detailRequests,
|
||||
detailTaskVersion: 7,
|
||||
detailField: { code: 'supportingFile', label: '补充附件', dataType: 'FILE' },
|
||||
returnRequests,
|
||||
});
|
||||
|
||||
await page.goto('/tasks?view=TODO');
|
||||
await page.getByText('退回', { exact: true }).click();
|
||||
|
||||
await expect.poll(() => detailRequests.length).toBe(1);
|
||||
const dialog = page.locator('.t-dialog').filter({ hasText: '退回补件' });
|
||||
const returnPoint = dialog.locator('label.t-checkbox').filter({ hasText: '补充附件(附件)' });
|
||||
await expect(returnPoint).toBeVisible();
|
||||
await returnPoint.click();
|
||||
await dialog.locator('textarea').fill('请补充可核验的原始附件');
|
||||
await dialog.getByRole('button', { name: '确认退回', exact: true }).click();
|
||||
|
||||
await expect.poll(() => returnRequests.length).toBe(1);
|
||||
expect(returnRequests[0]).toEqual({
|
||||
version: 7,
|
||||
opinion: '请补充可核验的原始附件',
|
||||
returnPoints: ['supportingFile'],
|
||||
});
|
||||
await expect.poll(() => detailRequests.length).toBe(2);
|
||||
await expect(page.getByText('已退回补件', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-11 exposes and sends withdraw only from form allowed actions', async ({ page }) => {
|
||||
const withdrawRequests: Array<Record<string, unknown>> = [];
|
||||
await installTaskFixture(page, {
|
||||
view: 'CREATED',
|
||||
formActions: ['WITHDRAW', 'VOID'],
|
||||
withdrawRequests,
|
||||
});
|
||||
|
||||
await page.goto('/tasks?view=CREATED');
|
||||
await expect(page.getByText('撤回', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('作废', { exact: true })).toBeVisible();
|
||||
await page.getByText('撤回', { exact: true }).click();
|
||||
|
||||
const dialog = page.locator('.t-dialog').filter({ hasText: '撤回申请' });
|
||||
await dialog.locator('textarea').fill('申请内容需要补充后重新提交');
|
||||
await dialog.getByRole('button', { name: '确认撤回', exact: true }).click();
|
||||
|
||||
await expect.poll(() => withdrawRequests.length).toBe(1);
|
||||
expect(withdrawRequests[0]).toEqual({ version: 1, opinion: '申请内容需要补充后重新提交' });
|
||||
await expect(page.getByText('申请已撤回', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-11 sends void with an explicit reason', async ({ page }) => {
|
||||
const voidRequests: Array<Record<string, unknown>> = [];
|
||||
await installTaskFixture(page, { view: 'CREATED', formActions: ['VOID'], voidRequests });
|
||||
|
||||
await page.goto('/tasks?view=CREATED');
|
||||
await expect(page.getByText('撤回', { exact: true })).toHaveCount(0);
|
||||
await page.getByText('作废', { exact: true }).click();
|
||||
|
||||
const dialog = page.locator('.t-dialog').filter({ hasText: '作废申请' });
|
||||
await dialog.locator('textarea').fill('该申请已由新版本替代');
|
||||
await dialog.getByRole('button', { name: '确认作废', exact: true }).click();
|
||||
|
||||
await expect.poll(() => voidRequests.length).toBe(1);
|
||||
expect(voidRequests[0]).toEqual({ version: 1, opinion: '该申请已由新版本替代' });
|
||||
await expect(page.getByText('申请已作废', { exact: true })).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user