242 lines
10 KiB
TypeScript
242 lines
10 KiB
TypeScript
// ============================================================
|
||
// 工程图纸 DXF Web E2E(R71.1):上传 → 解析 → 候选 → P2 暂存 → 确认落库
|
||
// 依赖隔离后端(docs/development/e2e.md 的 8100 启动方式)与 JMS 登录。
|
||
// APS_E2E_SKIP_DRAWING=1 时跳过,供无图纸样例/无外部环境的 CI 场景使用。
|
||
// ============================================================
|
||
import { test, expect, type Page } from '@playwright/test';
|
||
|
||
const SKIP_DRAWING = process.env.APS_E2E_SKIP_DRAWING === '1';
|
||
|
||
const JMS = {
|
||
tenant: process.env.E2E_JMS_TENANT || 'platform',
|
||
username: process.env.E2E_JMS_USER || 'zhangzhen',
|
||
password: process.env.E2E_JMS_PASSWORD || 'sz@zz@1116',
|
||
};
|
||
|
||
function minimalDxf(text: string): string {
|
||
return [
|
||
'0', 'SECTION', '2', 'HEADER', '0', 'ENDSEC',
|
||
'0', 'SECTION', '2', 'ENTITIES',
|
||
'0', 'TEXT', '8', '0', '10', '0.0', '20', '0.0', '40', '2.5', '1', text,
|
||
'0', 'ENDSEC', '0', 'EOF', '',
|
||
].join('\n');
|
||
}
|
||
|
||
function minimalPdf(text: string): Buffer {
|
||
const lines = [text];
|
||
const content = `BT /F1 10 Tf 72 760 Td 14 TL\n${lines.map(line => `(${line}) Tj T*\n`).join('')}ET`;
|
||
const objects = [
|
||
'<< /Type /Catalog /Pages 2 0 R >>',
|
||
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
||
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
|
||
`<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`,
|
||
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
|
||
];
|
||
let body = '%PDF-1.4\n';
|
||
const offsets: number[] = [];
|
||
objects.forEach((obj, index) => {
|
||
offsets.push(Buffer.byteLength(body, 'latin1'));
|
||
body += `${index + 1} 0 obj\n${obj}\nendobj\n`;
|
||
});
|
||
const xrefPos = Buffer.byteLength(body, 'latin1');
|
||
body += `xref\n0 ${objects.length + 1}\n`;
|
||
body += '0000000000 65535 f \n';
|
||
offsets.forEach(offset => { body += `${String(offset).padStart(10, '0')} 00000 n \n`; });
|
||
body += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefPos}\n%%EOF\n`;
|
||
return Buffer.from(body, 'latin1');
|
||
}
|
||
|
||
async function login(page: Page): Promise<void> {
|
||
await page.goto('/');
|
||
const trigger = page.locator('.guest-login-trigger');
|
||
await expect(trigger).toBeEnabled({ timeout: 30_000 });
|
||
await trigger.click();
|
||
const dialog = page.locator('.auth-login-dialog');
|
||
await expect(dialog).toBeVisible();
|
||
await dialog.locator('input[placeholder="请输入企业名称"]').fill(JMS.tenant);
|
||
await dialog.locator('input[placeholder="请输入用户名"]').fill(JMS.username);
|
||
await dialog.locator('input[placeholder="请输入密码"]').fill(JMS.password);
|
||
await dialog.locator('.login-submit').click();
|
||
await expect(dialog).toBeHidden({ timeout: 30_000 }).catch(async (error) => {
|
||
if (await dialog.locator('.login-captcha-field').count()) {
|
||
throw new Error('登录要求图形验证码,E2E 冒烟无法自动处理(请用预置测试账号或关闭验证码)');
|
||
}
|
||
throw error;
|
||
});
|
||
await expect(page.locator('textarea[placeholder^="输入排产指令"]')).toBeVisible({ timeout: 60_000 });
|
||
}
|
||
|
||
async function createProjectViaUi(page: Page, name: string): Promise<string> {
|
||
await page.locator('.sidebar-mini-btn[title="新建项目"]').click();
|
||
const dialog = page.locator('[role="dialog"][aria-label="新建项目"]');
|
||
await expect(dialog).toBeVisible();
|
||
await dialog.locator('.dlg-input').first().fill(name);
|
||
const createRespPromise = page.waitForResponse(
|
||
resp => resp.url().endsWith('/api/projects') && resp.request().method() === 'POST',
|
||
);
|
||
await dialog.locator('.btn-primary').click();
|
||
await expect(dialog).toBeHidden({ timeout: 30_000 });
|
||
const createResp = await createRespPromise;
|
||
expect(createResp.ok()).toBeTruthy();
|
||
const created = await createResp.json() as { project: { id: string } };
|
||
const projectId = created.project.id;
|
||
const workspace = await (await page.request.get('/api/workspace')).json();
|
||
const projectSession = workspace.sessions.find(
|
||
(row: { projectId: string }) => row.projectId === projectId,
|
||
);
|
||
expect(projectSession).toBeTruthy();
|
||
const selected = await page.request.put('/api/workspace', {
|
||
data: {
|
||
projects: [],
|
||
sessions: [],
|
||
files: [],
|
||
activeProjectId: projectId,
|
||
activeSessionId: projectSession.id,
|
||
},
|
||
});
|
||
expect(selected.ok()).toBeTruthy();
|
||
await page.reload();
|
||
await expect(page.locator('textarea[placeholder^="输入排产指令"]')).toBeVisible({ timeout: 60_000 });
|
||
const projectItem = page.locator('.sidebar-project', { hasText: name }).first();
|
||
await expect(projectItem).toBeVisible();
|
||
await expect(projectItem).toHaveClass(/active/);
|
||
await expect.poll(async () => {
|
||
const snap = await (await page.request.get('/api/workspace')).json();
|
||
return snap.activeProjectId;
|
||
}).toBe(projectId);
|
||
return projectId;
|
||
}
|
||
|
||
async function uploadViaApi(
|
||
page: Page,
|
||
projectId: string,
|
||
files: { name: string; text: string }[],
|
||
): Promise<void> {
|
||
const form = new FormData();
|
||
for (const file of files) {
|
||
form.append(
|
||
'files',
|
||
new Blob([Buffer.from(minimalDxf(file.text), 'utf8')], { type: 'image/vnd.dxf' }),
|
||
file.name,
|
||
);
|
||
}
|
||
const resp = await page.request.post(`/api/projects/${projectId}/files/upload`, {
|
||
multipart: form,
|
||
});
|
||
expect(resp.ok()).toBeTruthy();
|
||
}
|
||
|
||
async function openDrawingPanel(page: Page): Promise<void> {
|
||
await page.locator('.right-rail button[data-tip="工程图纸"]').click();
|
||
await expect(page.getByText('工程图纸工作区')).toBeVisible({ timeout: 30_000 });
|
||
}
|
||
|
||
test.describe('工程图纸 DXF 链路', () => {
|
||
test.skip(SKIP_DRAWING, 'APS_E2E_SKIP_DRAWING=1:跳过图纸 E2E');
|
||
|
||
test('浏览器上传 DXF → 自动解析 → 候选块出现', async ({ page }) => {
|
||
await login(page);
|
||
const projectId = await createProjectViaUi(page, `E2E 图纸上传 ${Date.now()}`);
|
||
await openDrawingPanel(page);
|
||
await expect(page.locator('button[aria-label="全屏查看图纸"]')).toBeVisible();
|
||
|
||
const batchRespPromise = page.waitForResponse(
|
||
resp => resp.url().includes('/api/drawings/inspect-batch'),
|
||
);
|
||
await page.locator('.drawing-upload-input').setInputFiles({
|
||
name: 'E2E-001-a.dxf',
|
||
mimeType: 'image/vnd.dxf',
|
||
buffer: Buffer.from(minimalDxf('E2E-DXF-ONE'), 'utf8'),
|
||
});
|
||
const batchResp = await batchRespPromise;
|
||
expect(batchResp.ok()).toBeTruthy();
|
||
|
||
await expect(page.locator('.drawing-list-item', { hasText: 'E2E-001' })).toBeVisible({ timeout: 60_000 });
|
||
await expect(page.locator('.ant-tabs-tab', { hasText: '物料 1' }).first()).toBeVisible({ timeout: 30_000 });
|
||
|
||
const listResp = await page.request.get(`/api/projects/${projectId}/drawings`);
|
||
expect(listResp.ok()).toBeTruthy();
|
||
const drawings = (await listResp.json()).drawings;
|
||
expect(drawings.some(row => row.fileName === 'E2E-001-a.dxf' && row.status === 'PARSED')).toBeTruthy();
|
||
});
|
||
|
||
test('浏览器上传 PDF → 自动解析 → 候选块出现', async ({ page }) => {
|
||
await login(page);
|
||
const projectId = await createProjectViaUi(page, `E2E PDF 上传 ${Date.now()}`);
|
||
await openDrawingPanel(page);
|
||
|
||
const batchRespPromise = page.waitForResponse(
|
||
resp => resp.url().includes('/api/drawings/inspect-batch'),
|
||
);
|
||
await page.locator('.drawing-upload-input').setInputFiles({
|
||
name: 'PDF-001-a.pdf',
|
||
mimeType: 'application/pdf',
|
||
buffer: minimalPdf('PDF-001-a\nMATERIAL: STEEL'),
|
||
});
|
||
const batchResp = await batchRespPromise;
|
||
expect(batchResp.ok()).toBeTruthy();
|
||
|
||
await expect(page.locator('.drawing-list-item', { hasText: 'PDF-001' })).toBeVisible({ timeout: 60_000 });
|
||
|
||
const listResp = await page.request.get(`/api/projects/${projectId}/drawings`);
|
||
expect(listResp.ok()).toBeTruthy();
|
||
const drawings = (await listResp.json()).drawings;
|
||
expect(drawings.some(row => row.fileName === 'PDF-001-a.pdf' && row.status === 'PARSED')).toBeTruthy();
|
||
});
|
||
|
||
test('项目文件批量解析 → P2 暂存 → 确认落库', async ({ page }) => {
|
||
await login(page);
|
||
const projectId = await createProjectViaUi(page, `E2E 图纸批量确认 ${Date.now()}`);
|
||
const first = 'E2E-001-a.dxf';
|
||
const second = 'E2E-002-a.dxf';
|
||
await uploadViaApi(page, projectId, [
|
||
{ name: first, text: 'E2E-DXF-ONE' },
|
||
{ name: second, text: 'E2E-DXF-TWO' },
|
||
]);
|
||
await openDrawingPanel(page);
|
||
|
||
await page.locator('.drawing-file-select').click();
|
||
for (const name of [first, second]) {
|
||
await expect(page.locator('.ant-select-dropdown .ant-select-item-option', { hasText: name }).first()).toBeVisible();
|
||
}
|
||
await page.keyboard.press('Escape');
|
||
|
||
const batchRespPromise = page.waitForResponse(
|
||
resp => resp.url().includes('/api/drawings/inspect-batch'),
|
||
);
|
||
await page.locator('.drawing-parse-all').click();
|
||
const batchResp = await batchRespPromise;
|
||
expect(batchResp.ok()).toBeTruthy();
|
||
|
||
await expect(page.locator('.drawing-list-item', { hasText: 'E2E-001' })).toBeVisible({ timeout: 60_000 });
|
||
await expect(page.locator('.drawing-list-item', { hasText: 'E2E-002' })).toBeVisible({ timeout: 30_000 });
|
||
|
||
await page.locator('.ant-tabs-tab', { hasText: '物料 1' }).first().click();
|
||
const stageRespPromise = page.waitForResponse(
|
||
resp => {
|
||
const path = new URL(resp.url()).pathname;
|
||
return path.includes('/api/drawings/') && path.endsWith('/stage');
|
||
},
|
||
);
|
||
await page.locator('.ant-table-tbody .ant-checkbox-input').first().check();
|
||
await page.locator('button:has-text("P2 暂存")').first().click();
|
||
const stageResp = await stageRespPromise;
|
||
expect(stageResp.ok()).toBeTruthy();
|
||
const staged = await stageResp.json();
|
||
expect(staged.confirmId).toBeTruthy();
|
||
expect(staged.masterCommitted).toBe(false);
|
||
|
||
const confirmResp = await page.request.post('/api/actions/confirm', {
|
||
data: { confirmId: staged.confirmId, approve: true },
|
||
});
|
||
expect(confirmResp.ok()).toBeTruthy();
|
||
const confirmBody = await confirmResp.json();
|
||
expect(confirmBody.message).toContain('Drawing review applied');
|
||
|
||
const listResp = await page.request.get(`/api/projects/${projectId}/drawings`);
|
||
expect(listResp.ok()).toBeTruthy();
|
||
const drawings = (await listResp.json()).drawings;
|
||
expect(drawings.some(row => row.fileName === first && row.status === 'COMMITTED')).toBeTruthy();
|
||
});
|
||
});
|