import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const departmentId = '01M00000000000000000000220'; const legacyMappingId = '01M00000000000000000000250'; interface DepartmentFixtureOptions { createRequests: Array>; submitRequests: Array>; reviewRequests: Array>; disableRequests: Array>; versionRequests: string[]; legacyCreateRequests: Array>; legacyDisableRequests: Array>; 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) { 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 | null = null; const versionHistory: Array> = []; let currentLegacyMapping: ReturnType | 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); 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); 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); 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); 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); 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); 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> = []; const submitRequests: Array> = []; const reviewRequests: Array> = []; const disableRequests: Array> = []; const versionRequests: string[] = []; const legacyCreateRequests: Array> = []; const legacyDisableRequests: Array> = []; 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); });