Files
kaidi/frontend/e2e/master-data-departments.e2e.ts
2026-08-17 14:08:40 +08:00

376 lines
16 KiB
TypeScript

import type { Page, Route } from '@playwright/test';
import { expect, test } from '@playwright/test';
const departmentId = '01M00000000000000000000220';
const legacyMappingId = '01M00000000000000000000250';
interface DepartmentFixtureOptions {
createRequests: Array<Record<string, unknown>>;
submitRequests: Array<Record<string, unknown>>;
reviewRequests: Array<Record<string, unknown>>;
disableRequests: Array<Record<string, unknown>>;
versionRequests: string[];
legacyCreateRequests: Array<Record<string, unknown>>;
legacyDisableRequests: Array<Record<string, unknown>>;
legacyListRequests: string[];
}
function session() {
return {
authenticated: true,
user: {
publicId: '01M00000000000000000000221',
username: 'masterdata-e2e',
displayName: '主数据经办人',
departmentName: '财务部',
mustChangePassword: false,
},
roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }],
activeRole: 'FINANCE_MANAGER',
permissions: [
'masterdata:company:view',
'masterdata:dictionary:view',
'masterdata:dictionary:create',
'masterdata:dictionary:edit',
'masterdata:dictionary:submit',
'masterdata:dictionary:review',
'masterdata:dictionary:disable',
],
};
}
function department(status: string, version: number, allowedActions: string[]) {
return {
publicId: departmentId,
resource: 'departments',
code: 'DEPT-E2E-001',
name: '财务管理部',
parent: null,
accountType: null,
auxiliaryRequired: null,
rate: null,
status,
version,
createdAt: '2026-08-15T08:00:00Z',
updatedAt: '2026-08-15T08:00:00Z',
allowedActions,
};
}
function departmentSnapshot(value: ReturnType<typeof department>) {
const { allowedActions: _allowedActions, ...snapshot } = value;
return snapshot;
}
function legacyMapping(status: 'ACTIVE' | 'DISABLED', version: number, allowedActions: string[]) {
return {
publicId: legacyMappingId,
resource: 'departments',
sourceSystem: 'OLD_ERP',
legacyCode: 'DEPT-LEGACY-001',
targetPublicId: departmentId,
status,
version,
createdAt: '2026-08-15T09:00:00Z',
updatedAt: '2026-08-15T09:00:00Z',
allowedActions,
};
}
async function installDepartmentFixture(page: Page, options: DepartmentFixtureOptions) {
const userSession = session();
let currentDepartment: ReturnType<typeof department> | null = null;
const versionHistory: Array<Record<string, unknown>> = [];
let currentLegacyMapping: ReturnType<typeof legacyMapping> | null = null;
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: '01M00000000000000000000222' } });
return;
}
if (pathname === '/api/v1/auth/profile') {
await route.fulfill({ json: { data: userSession.user, requestId: '01M00000000000000000000223' } });
return;
}
if (pathname === '/api/v1/auth/roles') {
await route.fulfill({ json: { data: userSession.roles, requestId: '01M00000000000000000000224' } });
return;
}
if (pathname === '/api/v1/auth/csrf') {
await route.fulfill({
headers: { 'set-cookie': 'XSRF-TOKEN=masterdata-e2e-csrf; Path=/' },
json: { data: { token: 'masterdata-e2e-csrf' }, requestId: '01M00000000000000000000225' },
});
return;
}
if (request.method() === 'GET' && pathname === '/api/v1/masterdata/references') {
await route.fulfill({
json: {
data: { companies: [], projects: [], counterparties: [], costCategories: [] },
requestId: '01M00000000000000000000226',
},
});
return;
}
if (request.method() === 'GET' && pathname === '/api/v1/masterdata/companies') {
await route.fulfill({
json: {
data: [],
meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 },
requestId: '01M00000000000000000000227',
},
});
return;
}
if (request.method() === 'GET' && pathname === '/api/v1/masterdata/departments') {
await route.fulfill({
json: {
data: currentDepartment ? [currentDepartment] : [],
meta: { page: 1, size: 20, totalElements: currentDepartment ? 1 : 0, totalPages: currentDepartment ? 1 : 0 },
requestId: '01M00000000000000000000228',
},
});
return;
}
if (request.method() === 'GET' && pathname === `/api/v1/masterdata/departments/${departmentId}`) {
await route.fulfill({
json: { data: currentDepartment, requestId: '01M00000000000000000000229' },
});
return;
}
if (request.method() === 'GET' && pathname === `/api/v1/masterdata/departments/${departmentId}/versions`) {
options.versionRequests.push(new URL(request.url()).search);
await route.fulfill({
json: {
data: versionHistory,
meta: { page: 1, size: 20, totalElements: versionHistory.length, totalPages: 1 },
requestId: '01M00000000000000000000235',
},
});
return;
}
if (
request.method() === 'GET' &&
pathname === `/api/v1/masterdata/departments/${departmentId}/legacy-identifiers`
) {
options.legacyListRequests.push(new URL(request.url()).search);
await route.fulfill({
json: {
data: currentLegacyMapping ? [currentLegacyMapping] : [],
meta: {
page: 1,
size: 20,
totalElements: currentLegacyMapping ? 1 : 0,
totalPages: currentLegacyMapping ? 1 : 0,
allowedActions: ['CREATE'],
},
requestId: '01M00000000000000000000251',
},
});
return;
}
if (
request.method() === 'POST' &&
pathname === `/api/v1/masterdata/departments/${departmentId}/legacy-identifiers`
) {
options.legacyCreateRequests.push(request.postDataJSON() as Record<string, unknown>);
currentLegacyMapping = legacyMapping('ACTIVE', 0, ['DISABLE']);
await route.fulfill({
json: { data: currentLegacyMapping, requestId: '01M00000000000000000000252' },
});
return;
}
if (
request.method() === 'POST' &&
pathname === `/api/v1/masterdata/departments/${departmentId}/legacy-identifiers/${legacyMappingId}/disable`
) {
options.legacyDisableRequests.push(request.postDataJSON() as Record<string, unknown>);
currentLegacyMapping = legacyMapping('DISABLED', 1, []);
await route.fulfill({
json: { data: currentLegacyMapping, requestId: '01M00000000000000000000253' },
});
return;
}
if (request.method() === 'POST' && pathname === '/api/v1/masterdata/departments') {
options.createRequests.push(request.postDataJSON() as Record<string, unknown>);
currentDepartment = department('DRAFT', 0, ['EDIT', 'SUBMIT']);
versionHistory.unshift({
auditId: '01M00000000000000000000240',
version: 0,
status: 'DRAFT',
actionCode: 'MASTERDATA_DICTIONARY_CREATE',
actorName: 'masterdata-e2e',
changedAt: '2026-08-15T08:00:00Z',
before: null,
after: departmentSnapshot(currentDepartment),
});
await route.fulfill({
json: { data: currentDepartment, requestId: '01M00000000000000000000230' },
});
return;
}
if (request.method() === 'POST' && pathname === `/api/v1/masterdata/departments/${departmentId}/submit`) {
options.submitRequests.push(request.postDataJSON() as Record<string, unknown>);
const before = currentDepartment ? departmentSnapshot(currentDepartment) : null;
currentDepartment = department('REVIEWING', 1, ['REVIEW']);
versionHistory.unshift({
auditId: '01M00000000000000000000241',
version: 1,
status: 'REVIEWING',
actionCode: 'MASTERDATA_DICTIONARY_SUBMIT',
actorName: 'masterdata-e2e',
changedAt: '2026-08-15T08:10:00Z',
before,
after: departmentSnapshot(currentDepartment),
});
await route.fulfill({
json: { data: currentDepartment, requestId: '01M00000000000000000000231' },
});
return;
}
if (request.method() === 'POST' && pathname === `/api/v1/masterdata/departments/${departmentId}/review`) {
options.reviewRequests.push(request.postDataJSON() as Record<string, unknown>);
const before = currentDepartment ? departmentSnapshot(currentDepartment) : null;
currentDepartment = department('ACTIVE', 2, ['DISABLE']);
versionHistory.unshift({
auditId: '01M00000000000000000000242',
version: 2,
status: 'ACTIVE',
actionCode: 'MASTERDATA_DICTIONARY_REVIEW',
actorName: 'masterdata-reviewer',
changedAt: '2026-08-15T08:20:00Z',
before,
after: departmentSnapshot(currentDepartment),
});
await route.fulfill({
json: { data: currentDepartment, requestId: '01M00000000000000000000232' },
});
return;
}
if (request.method() === 'POST' && pathname === `/api/v1/masterdata/departments/${departmentId}/disable`) {
options.disableRequests.push(request.postDataJSON() as Record<string, unknown>);
const before = currentDepartment ? departmentSnapshot(currentDepartment) : null;
currentDepartment = department('DISABLED', 3, []);
versionHistory.unshift({
auditId: '01M00000000000000000000243',
version: 3,
status: 'DISABLED',
actionCode: 'MASTERDATA_DICTIONARY_DISABLE',
actorName: 'masterdata-reviewer',
changedAt: '2026-08-15T08:30:00Z',
before,
after: departmentSnapshot(currentDepartment),
});
await route.fulfill({
json: { data: currentDepartment, requestId: '01M00000000000000000000233' },
});
return;
}
if (request.method() === 'GET') {
await route.fulfill({
json: {
data: [],
meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 },
requestId: '01M00000000000000000000234',
},
});
return;
}
await route.fulfill({ status: 404 });
});
}
test('PAGE-12 creates and advances a department using server allowed actions', async ({ page }) => {
const createRequests: Array<Record<string, unknown>> = [];
const submitRequests: Array<Record<string, unknown>> = [];
const reviewRequests: Array<Record<string, unknown>> = [];
const disableRequests: Array<Record<string, unknown>> = [];
const versionRequests: string[] = [];
const legacyCreateRequests: Array<Record<string, unknown>> = [];
const legacyDisableRequests: Array<Record<string, unknown>> = [];
const legacyListRequests: string[] = [];
await installDepartmentFixture(page, {
createRequests,
submitRequests,
reviewRequests,
disableRequests,
versionRequests,
legacyCreateRequests,
legacyDisableRequests,
legacyListRequests,
});
await page.goto('/finance/master-data');
await page.locator('.resource-tabs').getByText('部门', { exact: true }).click();
await page.locator('.page-header').getByRole('button', { name: '新建部门', exact: true }).click();
const createDialog = page.locator('.t-dialog').filter({ hasText: '新建部门' });
await createDialog.locator('.t-form-item__code input').fill('DEPT-E2E-001');
await createDialog.locator('.t-form-item__name input').fill('财务管理部');
await createDialog.getByRole('button', { name: '创建部门', exact: true }).click();
await expect.poll(() => createRequests.length).toBe(1);
expect(createRequests[0]).toEqual({ code: 'DEPT-E2E-001', name: '财务管理部' });
await expect(page.getByText('草稿', { exact: true })).toBeVisible();
await page.getByText('提交', { exact: true }).click();
const submitDialog = page.locator('.t-dialog').filter({ hasText: '提交复核' });
await submitDialog.getByRole('button', { name: '确认提交', exact: true }).click();
await expect.poll(() => submitRequests.length).toBe(1);
expect(submitRequests[0]).toEqual({ version: 0 });
await expect(page.getByText('复核中', { exact: true })).toBeVisible();
await page.getByText('复核', { exact: true }).click();
const reviewDialog = page.locator('.t-dialog').filter({ hasText: '复核主数据' });
await reviewDialog.locator('textarea').fill('部门资料核对无误');
await reviewDialog.getByRole('button', { name: '确认复核', exact: true }).click();
await expect.poll(() => reviewRequests.length).toBe(1);
expect(reviewRequests[0]).toEqual({ version: 1, decision: 'APPROVE', opinion: '部门资料核对无误' });
await expect(page.getByText('已生效', { exact: true })).toBeVisible();
await page.getByText('停用', { exact: true }).click();
const disableDialog = page.locator('.t-dialog').filter({ hasText: '停用主数据' });
await disableDialog.getByRole('button', { name: '确认停用', exact: true }).click();
await expect.poll(() => disableRequests.length).toBe(1);
expect(disableRequests[0]).toEqual({ version: 2 });
await expect(page.locator('.table-panel').getByText('已停用', { exact: true })).toBeVisible();
await expect(page.getByText('停用', { exact: true })).toHaveCount(0);
await page.getByText('版本记录', { exact: true }).click();
const historyDialog = page.locator('.t-dialog').filter({ hasText: '部门版本记录' });
await expect(historyDialog).toBeVisible();
await expect.poll(() => versionRequests.length).toBe(1);
expect(versionRequests[0]).toContain('page=1');
expect(versionRequests[0]).toContain('size=20');
await expect(historyDialog.locator('.history-section tbody tr')).toHaveCount(4);
const statusDifference = historyDialog.locator('.comparison-section tbody tr').filter({ hasText: '状态' });
await expect(statusDifference).toContainText('草稿');
await expect(statusDifference).toContainText('已停用');
await expect(historyDialog.locator('.comparison-section tbody tr').filter({ hasText: '版本' })).toContainText('v3');
await historyDialog.getByRole('button', { name: '关闭', exact: true }).click();
await page.getByText('查看', { exact: true }).click();
const detailDialog = page.locator('.t-dialog').filter({ hasText: '部门详情' });
await detailDialog.getByRole('button', { name: '旧编号', exact: true }).click();
const legacyDialog = page.locator('.t-dialog').filter({ hasText: '部门旧编号映射' });
await expect(legacyDialog).toBeVisible();
await expect.poll(() => legacyListRequests.length).toBe(1);
await legacyDialog.locator('.t-form-item__sourceSystem input').fill('OLD_ERP');
await legacyDialog.locator('.t-form-item__legacyCode input').fill('DEPT-LEGACY-001');
await legacyDialog.getByRole('button', { name: '新增映射', exact: true }).click();
await expect.poll(() => legacyCreateRequests.length).toBe(1);
expect(legacyCreateRequests[0]).toEqual({ sourceSystem: 'OLD_ERP', legacyCode: 'DEPT-LEGACY-001' });
await expect(legacyDialog.getByText('DEPT-LEGACY-001', { exact: true })).toBeVisible();
await legacyDialog.getByText('停用', { exact: true }).click();
const legacyDisableDialog = page.locator('.t-dialog').filter({ hasText: '停用旧编号映射' });
await legacyDisableDialog.getByRole('button', { name: '停用', exact: true }).click();
await expect.poll(() => legacyDisableRequests.length).toBe(1);
expect(legacyDisableRequests[0]).toEqual({ version: 0 });
await expect(legacyDialog.getByText('已停用', { exact: true })).toBeVisible();
await expect(legacyDialog.getByText('停用', { exact: true })).toHaveCount(0);
});