Files
kaidi/frontend/e2e/project-archives.e2e.ts
2026-08-17 14:08:40 +08:00

294 lines
11 KiB
TypeScript

import type { Page, Route } from '@playwright/test';
import { expect, test } from '@playwright/test';
const packageId = '01M00000000000000000000301';
const createdPackageId = '01M00000000000000000000302';
const companyId = '01M00000000000000000000303';
const projectId = '01M00000000000000000000304';
const itemId = '01M00000000000000000000305';
interface ArchiveFixtureOptions {
permissions?: string[];
packageActions?: string[];
listQueries?: URL[];
createRequests?: Array<Record<string, unknown>>;
checkRequests?: Array<Record<string, unknown>>;
}
function archivePackage(allowedActions: string[] = []) {
return {
publicId: packageId,
businessNo: 'ARC-E2E-001',
company: { publicId: companyId, businessNo: 'COMP-E2E-001', name: '凯迪档案测试公司' },
project: { publicId: projectId, businessNo: 'P-E2E-001', name: '档案验收项目' },
packageVersion: 1,
rootPackagePublicId: packageId,
supersedesPackagePublicId: null,
revisionReason: null,
ruleVersion: 'ARCHIVE-RULE-V1',
completeness: '80.00',
missingCount: 1,
retentionYears: 10,
retentionExpiresOn: '2026-10-31',
retentionStatus: 'DUE_SOON',
physicalLocation: 'A-01-03',
frozen: false,
status: 'PREPARING',
manifestSha256: 'a'.repeat(64),
submittedByName: null,
reviewedByName: null,
submittedAt: null,
archivedAt: null,
updatedAt: '2026-08-15T08:00:00Z',
version: 3,
allowedActions,
items: [
{
publicId: itemId,
itemType: 'CONTRACT',
itemName: '合同文件',
required: true,
status: 'MISSING',
missingReason: '尚未上传合同文件',
allowedActions: ['UPLOAD'],
},
],
objectSnapshots: [],
actions: [
{
sequenceNo: 1,
actionCode: 'GENERATE',
fromStatus: null,
toStatus: 'PREPARING',
opinion: '生成项目档案包',
actorName: '财务归档员',
occurredAt: '2026-08-15T08:00:00Z',
manifestSha256: 'a'.repeat(64),
},
],
};
}
function session(permissions: string[]) {
return {
authenticated: true,
user: {
publicId: '01M00000000000000000000306',
username: 'archive-e2e',
displayName: '财务归档员',
departmentName: '财务部',
mustChangePassword: false,
},
roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }],
activeRole: 'FINANCE_MANAGER',
permissions,
};
}
async function installArchiveFixture(page: Page, options: ArchiveFixtureOptions = {}) {
const userSession = session(options.permissions || ['archive:package:view']);
let currentPackage = archivePackage(options.packageActions || []);
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: '01M00000000000000000000307' } });
return;
}
if (pathname === '/api/v1/auth/profile') {
await route.fulfill({ json: { data: userSession.user, requestId: '01M00000000000000000000308' } });
return;
}
if (pathname === '/api/v1/auth/roles') {
await route.fulfill({ json: { data: userSession.roles, requestId: '01M00000000000000000000309' } });
return;
}
if (pathname === '/api/v1/auth/csrf') {
await route.fulfill({
headers: { 'set-cookie': 'XSRF-TOKEN=archive-e2e-csrf; Path=/' },
json: { data: { token: 'archive-e2e-csrf' }, requestId: '01M00000000000000000000310' },
});
return;
}
if (request.method() === 'GET' && pathname === '/api/v1/archives/packages') {
options.listQueries?.push(new URL(request.url()));
await route.fulfill({
json: {
data: [currentPackage],
meta: { page: 1, size: 20, totalElements: 1, totalPages: 1 },
requestId: '01M00000000000000000000311',
},
});
return;
}
if (request.method() === 'GET' && pathname === `/api/v1/archives/packages/${currentPackage.publicId}`) {
await route.fulfill({ json: { data: currentPackage, requestId: '01M00000000000000000000312' } });
return;
}
if (request.method() === 'POST' && pathname === '/api/v1/archives/packages') {
options.createRequests?.push(request.postDataJSON() as Record<string, unknown>);
currentPackage = { ...archivePackage(['CHECK']), publicId: createdPackageId, businessNo: 'ARC-E2E-002' };
await route.fulfill({ json: { data: currentPackage, requestId: '01M00000000000000000000313' } });
return;
}
if (request.method() === 'POST' && pathname === `/api/v1/archives/packages/${currentPackage.publicId}/check`) {
options.checkRequests?.push(request.postDataJSON() as Record<string, unknown>);
currentPackage = {
...currentPackage,
status: 'CHECKED',
version: currentPackage.version + 1,
allowedActions: ['SUBMIT'],
};
await route.fulfill({ json: { data: currentPackage, requestId: '01M00000000000000000000314' } });
return;
}
if (request.method() === 'GET') {
await route.fulfill({
json: {
data: [],
meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 },
requestId: '01M00000000000000000000315',
},
});
return;
}
await route.fulfill({ status: 204 });
});
}
test('PAGE-17 keeps list filters in the URL and reset removes optional conditions', async ({ page }) => {
const listQueries: URL[] = [];
await installArchiveFixture(page, { listQueries });
await page.goto(
`/archives/projects?view=archived&projectId=${projectId}&status=ARCHIVED&frozen=true&sort=archivedAt%2Cdesc`,
);
await expect.poll(() => listQueries.length).toBeGreaterThan(0);
const initialQuery = listQueries.at(-1)?.searchParams;
expect(initialQuery?.get('view')).toBe('archived');
expect(initialQuery?.get('projectId')).toBe(projectId);
expect(initialQuery?.get('status')).toBe('ARCHIVED');
expect(initialQuery?.get('frozen')).toBe('true');
expect(initialQuery?.get('sort')).toBe('archivedAt,desc');
await expect
.poll(() => {
const url = new URL(page.url());
return {
pathname: url.pathname,
view: url.searchParams.get('view'),
projectId: url.searchParams.get('projectId'),
status: url.searchParams.get('status'),
frozen: url.searchParams.get('frozen'),
sort: url.searchParams.get('sort'),
};
})
.toEqual({
pathname: '/archives/projects',
view: 'archived',
projectId,
status: 'ARCHIVED',
frozen: 'true',
sort: 'archivedAt,desc',
});
await expect(page.getByText('档案验收项目', { exact: true })).toBeVisible();
await expect(page.getByText('即将到期', { exact: true })).toBeVisible();
await page.getByRole('button', { name: '重置', exact: true }).click();
await expect.poll(() => listQueries.length).toBeGreaterThan(1);
const resetQuery = listQueries.at(-1)?.searchParams;
expect(resetQuery?.get('view')).toBe('archived');
expect(resetQuery?.has('projectId')).toBe(false);
expect(resetQuery?.has('status')).toBe(false);
expect(resetQuery?.has('frozen')).toBe(false);
expect(resetQuery?.get('sort')).toBe('updatedAt,desc');
await expect
.poll(() => {
const url = new URL(page.url());
return {
pathname: url.pathname,
view: url.searchParams.get('view'),
projectId: url.searchParams.get('projectId'),
status: url.searchParams.get('status'),
frozen: url.searchParams.get('frozen'),
sort: url.searchParams.get('sort'),
};
})
.toEqual({
pathname: '/archives/projects',
view: 'archived',
projectId: null,
status: null,
frozen: null,
sort: 'updatedAt,desc',
});
});
test('PAGE-17 hides archive generation without create permission and opens scoped detail', async ({ page }) => {
await installArchiveFixture(page);
await page.goto('/archives/projects');
await expect(page.getByRole('heading', { name: '项目档案包', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '生成档案包', exact: true })).toHaveCount(0);
await page.getByText('ARC-E2E-001', { exact: true }).click();
const drawer = page.locator('.t-drawer').filter({ hasText: 'ARC-E2E-001' });
await expect(drawer.getByText('保管到期', { exact: true })).toBeVisible();
await expect(
drawer.getByText('档案将于 2026-10-31 到期。系统仅提示,不会自动删除或销毁。', { exact: true }),
).toBeVisible();
await expect(drawer.getByRole('button', { name: '检查', exact: true })).toHaveCount(0);
});
test('PAGE-17 validates and sends the archive package creation payload', async ({ page }) => {
const createRequests: Array<Record<string, unknown>> = [];
await installArchiveFixture(page, {
permissions: ['archive:package:view', 'archive:package:create'],
createRequests,
});
await page.goto('/archives/projects');
await page.getByRole('button', { name: '生成档案包', 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();
expect(createRequests).toHaveLength(0);
await dialog.getByPlaceholder('请输入项目公开编号', { exact: true }).fill(projectId);
await dialog.getByPlaceholder('可选,例如 A-01-03', { exact: true }).fill('B-02-08');
await dialog.getByRole('button', { name: '生成', exact: true }).click();
await expect.poll(() => createRequests.length).toBe(1);
expect(createRequests[0]).toEqual({ projectId, retentionYears: 10, physicalLocation: 'B-02-08' });
await expect(page.getByText('档案包已生成', { exact: true })).toBeVisible();
await expect(dialog).toBeHidden();
await expect(page.getByText('ARC-E2E-002', { exact: true })).toBeVisible();
});
test('PAGE-17 executes only server-provided package actions and refreshes detail', async ({ page }) => {
const checkRequests: Array<Record<string, unknown>> = [];
await installArchiveFixture(page, {
permissions: ['archive:package:view', 'archive:package:submit'],
packageActions: ['CHECK'],
checkRequests,
});
await page.goto('/archives/projects');
await page.getByText('ARC-E2E-001', { exact: true }).click();
const drawer = page.locator('.t-drawer').filter({ hasText: 'ARC-E2E-001' });
await drawer.getByRole('button', { name: '检查', exact: true }).click();
await expect(drawer.getByRole('button', { name: '提交审核', exact: true })).toHaveCount(0);
const dialog = page.locator('.t-dialog').filter({ hasText: '检查' });
await dialog.getByRole('button', { name: '确认检查', exact: true }).click();
await expect.poll(() => checkRequests.length).toBe(1);
expect(checkRequests[0]).toEqual({ version: 3 });
await expect(page.getByText('检查已完成', { exact: true })).toBeVisible();
await expect(dialog).toBeHidden();
await expect(drawer.getByRole('button', { name: '提交审核', exact: true })).toBeVisible();
await expect(drawer.getByRole('button', { name: '检查', exact: true })).toHaveCount(0);
});