import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const borrowId = '01M00000000000000000000401'; const fileId = '01M00000000000000000000402'; const projectId = '01M00000000000000000000403'; function envelope(data: unknown, totalElements?: number) { return { data, ...(typeof totalElements === 'number' ? { meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 } } : {}), requestId: '01M00000000000000000000499', }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000404', username: 'archive-e2e', displayName: '资料管理员', departmentName: '档案部', mustChangePassword: false, }, roles: [{ code: 'ARCHIVE_MANAGER', name: '资料管理员', workbenchRoute: '/workbench/archive' }], activeRole: 'ARCHIVE_MANAGER', permissions: ['archive:file:view', 'archive:borrow:approve', 'archive:borrow:reject', 'archive:borrow:return'], }; } function borrow(overrides: Record = {}) { return { publicId: borrowId, businessNo: 'BOR-E2E-018', filePublicId: fileId, fileName: '项目合同原件.pdf', projectPublicId: projectId, projectName: '档案借阅验收项目', purpose: '项目审计核对原件', dueAt: '2099-12-31T12:00:00Z', status: 'APPLIED', applicantName: '财务申请员', approvedByName: null, approvedAt: null, reviewOpinion: null, returnCondition: null, returnedByName: null, returnedAt: null, version: 0, allowedActions: ['APPROVE', 'REJECT'], ...overrides, }; } interface FixtureOptions { approveRequests: Array>; returnRequests: Array>; commandHeaders: Array>; borrowQueries: URLSearchParams[]; } async function installFixture(page: Page, options: FixtureOptions) { const userSession = session(); let currentBorrow = borrow(); await page.route('**/api/v1/**', async (route: Route) => { const request = route.request(); const url = new URL(request.url()); const pathname = url.pathname; if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(userSession) }); if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(userSession.user) }); if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(userSession.roles) }); if (pathname === '/api/v1/auth/csrf') { return route.fulfill({ headers: { 'set-cookie': 'XSRF-TOKEN=archive-e2e-csrf; Path=/' }, json: envelope({ token: 'archive-e2e-csrf' }), }); } if (request.method() === 'GET' && pathname === '/api/v1/archives/borrows') { options.borrowQueries.push(new URLSearchParams(url.searchParams)); return route.fulfill({ json: envelope([currentBorrow], 1) }); } if (request.method() === 'POST' && pathname === `/api/v1/archives/borrows/${borrowId}/approve`) { options.approveRequests.push(request.postDataJSON() as Record); options.commandHeaders.push(request.headers()); currentBorrow = borrow({ status: 'BORROWED', approvedByName: '资料管理员', approvedAt: '2026-08-16T09:00:00Z', reviewOpinion: '用途和期限符合要求', version: 1, allowedActions: ['RETURN'], }); return route.fulfill({ json: envelope(currentBorrow) }); } if (request.method() === 'POST' && pathname === `/api/v1/archives/borrows/${borrowId}/return`) { const payload = request.postDataJSON() as Record; options.returnRequests.push(payload); options.commandHeaders.push(request.headers()); currentBorrow = borrow({ status: 'RETURNED', approvedByName: '资料管理员', approvedAt: '2026-08-16T09:00:00Z', reviewOpinion: '用途和期限符合要求', returnCondition: payload.returnCondition, returnedByName: '资料管理员', returnedAt: '2026-08-16T10:00:00Z', version: 2, allowedActions: [], }); return route.fulfill({ json: envelope(currentBorrow) }); } if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) }); return route.fulfill({ status: 204 }); }); } test('PAGE-18 approves and returns an archive borrow with versioned commands', async ({ page }) => { const approveRequests: Array> = []; const returnRequests: Array> = []; const commandHeaders: Array> = []; const borrowQueries: URLSearchParams[] = []; await installFixture(page, { approveRequests, returnRequests, commandHeaders, borrowQueries }); await page.goto('/archives/files?resource=borrows'); await expect(page.getByRole('heading', { name: '文件检索与借阅', exact: true })).toBeVisible(); await expect(page.getByText('BOR-E2E-018', { exact: true }).first()).toBeVisible(); await expect.poll(() => borrowQueries.length).toBeGreaterThan(0); expect(borrowQueries[0].get('page')).toBe('1'); expect(borrowQueries[0].get('size')).toBe('20'); const initialListRequests = borrowQueries.length; await page.getByRole('button', { name: '批准', exact: true }).click(); const approveDialog = page.locator('.t-dialog').filter({ hasText: '批准借阅' }); await approveDialog.getByRole('button', { name: '确认', exact: true }).click(); await expect(approveDialog.getByText('请填写至少 2 个字符的处理意见', { exact: true })).toBeVisible(); expect(approveRequests).toHaveLength(0); await approveDialog.locator('textarea').fill('用途和期限符合要求'); await approveDialog.getByRole('button', { name: '确认', exact: true }).click(); await expect.poll(() => approveRequests.length).toBe(1); expect(approveRequests[0]).toEqual({ version: 0, opinion: '用途和期限符合要求', }); expect(commandHeaders[0]['x-xsrf-token']).toBe('archive-e2e-csrf'); expect(commandHeaders[0]['idempotency-key']).toBeTruthy(); await expect(approveDialog).toBeHidden(); await expect.poll(() => borrowQueries.length).toBeGreaterThan(initialListRequests); await expect(page.getByText('借出中', { exact: true })).toBeVisible(); const afterApprovalListRequests = borrowQueries.length; await page.getByRole('button', { name: '登记归还', exact: true }).click(); const returnDialog = page.locator('.t-dialog').filter({ hasText: '登记归还' }); await returnDialog.getByRole('button', { name: '确认', exact: true }).click(); await expect(returnDialog.getByText('请填写至少 2 个字符的处理意见', { exact: true })).toBeVisible(); expect(returnRequests).toHaveLength(0); await returnDialog.locator('textarea').fill('纸质原件完好,页数一致'); await returnDialog.getByRole('button', { name: '确认', exact: true }).click(); await expect.poll(() => returnRequests.length).toBe(1); expect(returnRequests[0]).toEqual({ version: 1, returnCondition: '纸质原件完好,页数一致', }); expect(commandHeaders[1]['x-xsrf-token']).toBe('archive-e2e-csrf'); expect(commandHeaders[1]['idempotency-key']).toBeTruthy(); await expect(returnDialog).toBeHidden(); await expect.poll(() => borrowQueries.length).toBeGreaterThan(afterApprovalListRequests); await expect(page.getByText('已归还', { exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: '登记归还', exact: true })).toHaveCount(0); });