aps-agent/apps/web/e2e/schedule-template-download....

92 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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([]);
});