376 lines
16 KiB
TypeScript
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);
|
|
});
|