294 lines
11 KiB
TypeScript
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);
|
|
});
|