- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化 - Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab - 四档响应式(桌面/笔记本/平板/手机)、深浅主题 - 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测 - 真实凭据已脱敏,运行时配置在 backend/.env(不入 git) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
7.2 KiB
JavaScript
147 lines
7.2 KiB
JavaScript
// deploy/m3_e2e.js —— M3 综合验收(PM 使用,在远端运行)
|
||
// 覆盖:KG 自由浏览(真实 Neo4j)、抽取任务 tab(真实 8000)、三档视口布局、主题切换
|
||
const { chromium } = require('playwright');
|
||
|
||
const BASE = 'http://127.0.0.1:5180';
|
||
const OUT_DIR = `${__dirname}/../artifacts/m3`;
|
||
const fs = require('fs');
|
||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
|
||
const results = [];
|
||
function check(name, ok, extra = '') {
|
||
results.push({ name, ok });
|
||
console.log(`${ok ? ' ✅' : ' ❌'} ${name}${extra ? ' —— ' + extra : ''}`);
|
||
}
|
||
|
||
async function waitFor(fn, timeoutMs, intervalMs = 700, label = '') {
|
||
const start = Date.now();
|
||
while (Date.now() - start < timeoutMs) {
|
||
try {
|
||
const v = await fn();
|
||
if (v) return v;
|
||
} catch {}
|
||
await new Promise((r) => setTimeout(r, intervalMs));
|
||
}
|
||
throw new Error(`等待超时(${timeoutMs}ms): ${label}`);
|
||
}
|
||
|
||
(async () => {
|
||
const browser = await chromium.launch();
|
||
|
||
// ============ 桌面档:KG 浏览 + 抽取 ============
|
||
{
|
||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||
const page = await ctx.newPage();
|
||
const errors = [];
|
||
page.on('pageerror', (e) => errors.push(e.message));
|
||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||
await waitFor(() => page.locator('.app-shell').count(), 15000, 500, 'AppShell');
|
||
|
||
// KG tab:实体搜索 + 邻域
|
||
await page.click('.bench-tab:has-text("知识图谱")');
|
||
await waitFor(() => page.locator('.bench-body input[type="text"], .bench-body input').count(), 10000, 500, 'KG 搜索框');
|
||
await page.fill('.bench-body input', 'OsSPL14');
|
||
await page.press('.bench-body input', 'Enter');
|
||
await waitFor(async () => {
|
||
const n = await page.locator('.bench-body .kg-result, .bench-body [class*="result"], .bench-body [class*="entity"]').count();
|
||
return n > 0;
|
||
}, 20000, 800, '实体搜索结果');
|
||
check('KG:实体搜索有结果(真实 Neo4j fulltext 替代)', true);
|
||
await page.screenshot({ path: `${OUT_DIR}/01-kg-search.png` });
|
||
|
||
// 邻域渲染(点击第一个结果或通过已选中实体)
|
||
const clicked = await page.evaluate(async () => {
|
||
const items = document.querySelectorAll('.bench-body [class*="result"] button, .bench-body [class*="entity"] button, .bench-body [class*="result"] [class*="item"]');
|
||
if (items[0]) { items[0].click(); return true; }
|
||
return false;
|
||
});
|
||
await waitFor(async () => {
|
||
const svg = await page.locator('.bench-body svg').count();
|
||
const nodes = await page.locator('.bench-body svg circle, .bench-body svg [class*="node"]').count();
|
||
return svg > 0 && nodes > 0;
|
||
}, 30000, 800, 'KG 图谱渲染');
|
||
check('KG:邻域图谱渲染(节点+边)', true);
|
||
await page.screenshot({ path: `${OUT_DIR}/02-kg-graph.png` });
|
||
|
||
// 抽取任务 tab
|
||
await page.click('.bench-tab:has-text("提取任务")');
|
||
await waitFor(async () => {
|
||
const n = await page.locator('.bench-body [class*="job"], .bench-body [class*="prog"]').count();
|
||
return n > 0;
|
||
}, 20000, 800, '抽取任务列表');
|
||
check('抽取:真实任务列表渲染', true);
|
||
const jobText = await page.locator('.bench-body').textContent();
|
||
check('抽取:含真实任务名或状态', /MinerU|completed|failed|running|queued|wang|Pecan/i.test(jobText));
|
||
await page.screenshot({ path: `${OUT_DIR}/03-extraction.png` });
|
||
|
||
check('桌面档无 JS 错误', errors.length === 0, errors.slice(0, 2).join('|'));
|
||
await ctx.close();
|
||
}
|
||
|
||
// ============ 平板档:抽屉布局 ============
|
||
{
|
||
const ctx = await browser.newContext({ viewport: { width: 768, height: 1024 } });
|
||
const page = await ctx.newPage();
|
||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||
await waitFor(() => page.locator('.app-shell.drawers-mode').count(), 15000, 500, 'drawers-mode');
|
||
check('平板:drawers-mode 生效(单栏)', true);
|
||
// 汉堡开左抽屉
|
||
const hamburger = await page.locator('.hamburger-btn').count();
|
||
check('平板:顶栏汉堡按钮出现', hamburger > 0);
|
||
if (hamburger > 0) {
|
||
await page.click('.hamburger-btn');
|
||
await waitFor(() => page.locator('.drawer').count(), 5000, 400, '左抽屉');
|
||
const drawerVisible = await page.locator('.drawer').first().isVisible();
|
||
check('平板:左抽屉打开', drawerVisible);
|
||
await page.screenshot({ path: `${OUT_DIR}/04-tablet-drawer.png` });
|
||
// Esc 关闭(抽屉仍挂载但移出屏幕——断言 open class 而非 isHidden)
|
||
await page.keyboard.press('Escape');
|
||
await page.waitForTimeout(500);
|
||
const openClass = await page.locator('.drawer.left').first().evaluate((el) => el.classList.contains('open')).catch(() => false);
|
||
check('平板:Esc 关闭抽屉', !openClass);
|
||
}
|
||
await ctx.close();
|
||
}
|
||
|
||
// ============ 手机档:全屏覆盖层 + 主题 ============
|
||
{
|
||
const ctx = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
||
const page = await ctx.newPage();
|
||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||
await waitFor(() => page.locator('.app-shell.mobile').count(), 15000, 500, 'mobile 档');
|
||
check('手机:mobile 档生效', true);
|
||
const sb = await page.locator('.status-bar').isHidden().catch(() => true);
|
||
check('手机:底栏隐藏(FR-RESP-004)', sb);
|
||
const benchBtn = await page.locator('.bench-btn').count();
|
||
check('手机:工作台按钮出现', benchBtn > 0);
|
||
if (benchBtn > 0) {
|
||
await page.click('.bench-btn');
|
||
await waitFor(() => page.locator('.bench-overlay').count(), 5000, 400, '工作台覆盖层');
|
||
check('手机:工作台全屏覆盖层打开', true);
|
||
await page.screenshot({ path: `${OUT_DIR}/05-mobile-overlay.png` });
|
||
// tab 切换(PM 修复的接缝)。注意覆盖层内有两套 tab(外壳 tab 条 + BenchPanel 内部 tab),
|
||
// 用外壳 tab 条(.bench-overlay-tabs 内)点击,用最后匹配的 on 断言
|
||
await page.click('.bench-overlay-tabs .bench-tab:has-text("文献来源")');
|
||
const activeTab = await page.locator('.bench-overlay-tabs .bench-tab.on').first().textContent();
|
||
check('手机:覆盖层 tab 切换(PM 接线修复)', activeTab.includes('文献来源'), activeTab.trim());
|
||
await page.click('.bench-overlay .bench-close-btn, .bench-overlay [class*="close"]').catch(async () => {
|
||
await page.keyboard.press('Escape');
|
||
});
|
||
}
|
||
// 主题切换(浅色)
|
||
await page.click('.top-bar .icon-btn:last-child');
|
||
await page.waitForTimeout(300);
|
||
const theme = await page.evaluate(() => document.documentElement.dataset.theme);
|
||
check('主题:切换为浅色(dataset.theme)', theme === 'light');
|
||
const canvasBg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
|
||
check('主题:浅色 canvas 生效', canvasBg === 'rgb(246, 248, 252)', canvasBg);
|
||
await page.screenshot({ path: `${OUT_DIR}/06-mobile-light.png` });
|
||
await ctx.close();
|
||
}
|
||
|
||
await browser.close();
|
||
const passed = results.filter((r) => r.ok).length;
|
||
console.log(`\nM3 验收结果:${passed}/${results.length} 通过`);
|
||
process.exit(passed === results.length ? 0 : 1);
|
||
})();
|