2026-08-20 11:39:21 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 核心用户路径 E2E 冒烟(矩阵 118 · 前端 E2E)
|
|
|
|
|
|
// 登录(JMS)→ 跑一版排产(柔性)→ 结果块出现 → 视口切换(世界对比/柔性工作台)→ 命令面板
|
2026-09-08 00:07:26 +08:00
|
|
|
|
// 全部为中文断言;依赖隔离后端 + JMS 网络可达,登录参数必须由环境变量注入。
|
2026-08-20 11:39:21 +08:00
|
|
|
|
// 设置 APS_E2E_SKIP_JMS=1 时跳过本文件(CI 无外网场景),只保留访客冒烟。
|
|
|
|
|
|
// 注意:默认 preview(生产构建)模式运行;dev 模式 StrictMode 结果捕获缺陷已于 round-42
|
|
|
|
|
|
// 修复并验证(E2E_MODE=dev 附加冒烟),preview 仍为默认门禁。
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
import { test, expect, type Page } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
|
|
const SKIP_JMS = process.env.APS_E2E_SKIP_JMS === '1';
|
|
|
|
|
|
|
|
|
|
|
|
const JMS = {
|
2026-09-08 00:07:26 +08:00
|
|
|
|
tenant: process.env.E2E_JMS_TENANT?.trim() || '',
|
|
|
|
|
|
username: process.env.E2E_JMS_USER?.trim() || '',
|
|
|
|
|
|
password: process.env.E2E_JMS_PASSWORD || '',
|
2026-08-20 11:39:21 +08:00
|
|
|
|
};
|
2026-09-08 00:07:26 +08:00
|
|
|
|
const missingJmsConfig = Object.entries(JMS).filter(([, value]) => !value).map(([key]) => key);
|
|
|
|
|
|
if (!SKIP_JMS && missingJmsConfig.length) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`[e2e] JMS 登录参数未配置:${missingJmsConfig.join(', ')};` +
|
|
|
|
|
|
'请设置 E2E_JMS_TENANT/E2E_JMS_USER/E2E_JMS_PASSWORD,或设置 APS_E2E_SKIP_JMS=1。',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-08-20 11:39:21 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
// 成功 → 弹窗关闭;若后端要求图形验证码则报清晰错误(本地 jms 未开启)
|
|
|
|
|
|
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 });
|
|
|
|
|
|
await expect(page.locator('.sidebar-brand')).toContainText('工业智核');
|
2026-09-08 00:07:26 +08:00
|
|
|
|
await expect(page.locator('.sidebar-account-area')).toContainText(JMS.username);
|
2026-08-20 11:39:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
test.describe('核心用户路径(JMS)', () => {
|
|
|
|
|
|
test.skip(SKIP_JMS, 'APS_E2E_SKIP_JMS=1:跳过需要外网 JMS 的核心路径');
|
|
|
|
|
|
|
|
|
|
|
|
test('登录 → 跑一版排产 → 结果块 → 视口切换 → 命令面板', async ({ page }) => {
|
|
|
|
|
|
await login(page);
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 跑一版排产(柔性,当前种子数据可直接出结果) ----------
|
|
|
|
|
|
const composer = page.locator('textarea[placeholder^="输入排产指令"]');
|
|
|
|
|
|
await composer.fill('跑一版柔性排产');
|
|
|
|
|
|
await composer.press('Enter');
|
|
|
|
|
|
|
|
|
|
|
|
// 智能体回复出现(SSE 流式正文非空,中文断言)
|
|
|
|
|
|
await expect(page.locator('.msg.agent .bubble')).toContainText(/.+/, { timeout: 240_000 });
|
|
|
|
|
|
// 结果块出现:柔性排产结果卡(中文标题断言)
|
|
|
|
|
|
const resultBlock = page.locator('.flex-block-title', { hasText: '柔性排产' });
|
|
|
|
|
|
await expect(resultBlock).toBeVisible({ timeout: 240_000 });
|
2026-09-17 00:08:13 +08:00
|
|
|
|
// 结果入口可用:右侧“排产结果/再点收起”按钮(preview 模式下结果捕获后自动打开视口)
|
2026-08-20 11:39:21 +08:00
|
|
|
|
const resultButton = page.locator(
|
2026-09-17 00:08:13 +08:00
|
|
|
|
'.right-rail button[data-tip="排产结果"], .right-rail button[data-tip="再点收起"]',
|
2026-08-20 11:39:21 +08:00
|
|
|
|
);
|
|
|
|
|
|
await expect(resultButton).toBeEnabled({ timeout: 120_000 });
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 视口切换 ----------
|
2026-09-17 00:08:13 +08:00
|
|
|
|
// 结果捕获成功后视口已自动打开;未打开则点击“排产结果”兜底
|
2026-08-20 11:39:21 +08:00
|
|
|
|
const vpToolbar = page.locator('.vp-toolbar');
|
|
|
|
|
|
await expect(vpToolbar).toBeVisible({ timeout: 30_000 }).catch(async () => {
|
|
|
|
|
|
await resultButton.click();
|
|
|
|
|
|
await expect(vpToolbar).toBeVisible({ timeout: 30_000 });
|
|
|
|
|
|
});
|
2026-09-17 00:08:13 +08:00
|
|
|
|
// 页签存在:版本对比(收在「更多分析」菜单内)
|
|
|
|
|
|
await page.locator('.vp-more summary').click();
|
|
|
|
|
|
const diffTab = page.locator('.vp-more-menu button', { hasText: '版本对比' });
|
2026-08-20 11:39:21 +08:00
|
|
|
|
await expect(diffTab).toBeVisible();
|
|
|
|
|
|
await diffTab.click();
|
|
|
|
|
|
await expect(page.locator('.world-diff')).toBeVisible();
|
|
|
|
|
|
// 右侧工具:柔性工作台(任务要求“柔性工作台页签存在”)
|
|
|
|
|
|
const flexButton = page.locator('.right-rail button[data-tip="柔性工作台"]');
|
|
|
|
|
|
await expect(flexButton).toBeVisible();
|
|
|
|
|
|
await flexButton.click();
|
|
|
|
|
|
await expect(page.locator('.flex-bench')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 命令面板 ----------
|
|
|
|
|
|
await page.keyboard.press('Control+K');
|
|
|
|
|
|
const paletteInput = page.locator('.palette input');
|
|
|
|
|
|
await expect(paletteInput).toBeVisible();
|
|
|
|
|
|
// 未过滤时面板条目可用(柔性工作台面板项)
|
|
|
|
|
|
await expect(page.locator('.palette-item', { hasText: '柔性工作台' })).toBeVisible();
|
2026-09-17 00:08:13 +08:00
|
|
|
|
// 中文过滤:对比 → 版本对比视图命令
|
|
|
|
|
|
await paletteInput.fill('对比');
|
|
|
|
|
|
await expect(page.locator('.palette-item', { hasText: '版本对比' })).toBeVisible();
|
2026-08-20 11:39:21 +08:00
|
|
|
|
// 中文过滤:柔性 → 柔性工作台面板项
|
|
|
|
|
|
await paletteInput.fill('柔性');
|
|
|
|
|
|
await expect(page.locator('.palette-item', { hasText: '柔性工作台' })).toBeVisible();
|
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
|
await expect(page.locator('.palette')).toBeHidden();
|
|
|
|
|
|
});
|
2026-09-08 00:07:26 +08:00
|
|
|
|
});
|