Files
jerryandClaude Fable 5 1e94de7464 PlantAI-rag-new 首次提交:文献知识工作台
- 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>
2026-08-25 20:47:52 +08:00

147 lines
7.2 KiB
JavaScript
Raw Permalink 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.
// 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);
})();