92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
||
import { createHash } from 'node:crypto';
|
||
import { readFileSync } from 'node:fs';
|
||
|
||
// 排产导出模板下载的浏览器验收:只在显式指定授权工作簿时运行,指向隔离后端。
|
||
const SOURCE = process.env.ROUND87_SOURCE;
|
||
const REQUIRED = process.env.ROUND87_REQUIRED === '1';
|
||
const API_TARGET = process.env.E2E_API_TARGET || 'http://127.0.0.1:18787';
|
||
const apiUrl = new URL(API_TARGET);
|
||
if (!['127.0.0.1', 'localhost', '[::1]'].includes(apiUrl.hostname)
|
||
|| apiUrl.protocol !== 'http:' || !apiUrl.port
|
||
|| ['8000', '8003', '5173'].includes(apiUrl.port)) {
|
||
throw new Error('Schedule template download acceptance requires an isolated loopback backend.');
|
||
}
|
||
|
||
async function checkIsolation(page: Page): Promise<void> {
|
||
const direct = await page.request.get(`${API_TARGET}/__e2e__/health`);
|
||
expect(direct.ok(), 'start tests/e2e/round87_masterdata_server.py first').toBeTruthy();
|
||
expect(await direct.json()).toMatchObject({ isolated: true, sourceUnchanged: true, productionCode: true });
|
||
const proxied = await page.request.get('/api/__e2e__/health');
|
||
expect(proxied.ok(), 'frontend proxy must target the isolated Round 87 host').toBeTruthy();
|
||
}
|
||
|
||
async function createProject(page: Page): Promise<void> {
|
||
await page.locator('.workspace-sidebar:not(.guest-sidebar) button[title="新建项目"]').click();
|
||
const dialog = page.getByRole('dialog', { name: '新建项目', exact: true });
|
||
await expect(dialog).toBeVisible();
|
||
await dialog.locator('input').first().fill(`排产模板下载-${Date.now()}`);
|
||
const created = page.waitForResponse(response =>
|
||
new URL(response.url()).pathname === '/api/projects' && response.request().method() === 'POST');
|
||
await dialog.getByRole('button', { name: '创建', exact: true }).click();
|
||
expect((await created).ok()).toBeTruthy();
|
||
await expect(dialog).toBeHidden();
|
||
await expect(page.locator('.composer textarea')).toBeVisible();
|
||
}
|
||
|
||
test('flex workbench downloads the contract-driven schedule template', async ({ page }) => {
|
||
test.skip(!SOURCE && !REQUIRED, 'Set ROUND87_SOURCE to the explicitly authorized original workbook.');
|
||
test.setTimeout(300_000);
|
||
page.setDefaultTimeout(25_000);
|
||
await checkIsolation(page);
|
||
const login = await page.request.post('/api/auth/login', {
|
||
data: { tenantName: 'schedule-template-download', username: 'planner', password: 'test' },
|
||
});
|
||
expect(login.ok(), await login.text()).toBeTruthy();
|
||
|
||
const errors: string[] = [];
|
||
page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
|
||
page.on('console', message => {
|
||
if (message.type() === 'error') errors.push(`console: ${message.text()}`);
|
||
});
|
||
page.on('response', response => {
|
||
if (response.status() >= 500 && new URL(response.url()).pathname.startsWith('/api/')) {
|
||
errors.push(`HTTP ${response.status()}: ${response.url()}`);
|
||
}
|
||
});
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer textarea')).toBeVisible({ timeout: 60_000 });
|
||
await createProject(page);
|
||
await page.locator('.right-rail:not(.guest-right-rail) button[data-tip="柔性工作台"]').click();
|
||
const panel = page.locator('.flex-bench');
|
||
await expect(panel).toBeVisible();
|
||
|
||
// antd 图标会进入可访问名称(download 排产模板),这里按文本匹配避免版本差异。
|
||
const button = panel.getByRole('button', { name: /排产模板/ });
|
||
await button.scrollIntoViewIfNeeded();
|
||
const [download, response] = await Promise.all([
|
||
page.waitForEvent('download'),
|
||
page.waitForResponse(actual =>
|
||
new URL(actual.url()).pathname === '/api/reports/schedule-template'),
|
||
button.click(),
|
||
]);
|
||
expect(response.status()).toBe(200);
|
||
expect(response.headers()['x-aps-template-id']).toBe('schedule-plan.v1');
|
||
expect(response.headers()['x-aps-contract-digest']).toMatch(/^[0-9a-f]{64}$/);
|
||
expect(download.suggestedFilename()).toMatch(/\.xlsx$/i);
|
||
const path = await download.path();
|
||
expect(path).toBeTruthy();
|
||
const browserBytes = readFileSync(path!);
|
||
expect(browserBytes.subarray(0, 2).toString()).toBe('PK');
|
||
expect(browserBytes.length).toBeGreaterThan(4096);
|
||
|
||
// 模板不含业务行:UI 下载必须与鉴权接口逐字节一致,且同一合同重复下载稳定。
|
||
const apiTemplate = await page.request.get('/api/reports/schedule-template');
|
||
expect(apiTemplate.ok(), await apiTemplate.text()).toBeTruthy();
|
||
const hash = (value: Buffer) => createHash('sha256').update(value).digest('hex');
|
||
expect(hash(browserBytes)).toBe(hash(await apiTemplate.body()));
|
||
await expect(panel.locator('.ant-alert')).toContainText('已下载');
|
||
expect(errors, 'browser errors collected during schedule template download').toEqual([]);
|
||
});
|