- 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>
149 lines
7.2 KiB
JavaScript
149 lines
7.2 KiB
JavaScript
// bench.spec.js —— T 归属。L5 主线 7 图谱(TC-E2E-07-*)+ 主线 8 抽取(TC-E2E-08-*)。
|
||
// 图谱:答案图谱(真实 BFF 返回 243 节点 → 桌面/平板超 50 降级列表视图;移动端上限 30)、
|
||
// 实体搜索(真实 Neo4j name_norm)、邻域展开、节点详情。
|
||
// 抽取:真实上游 8000 任务列表/详情/日志;上传在 mock 拦截下模拟成功。
|
||
import { test, expect } from '@playwright/test';
|
||
import { tier, selectBenchTab, openBench, benchScope, openSessionPanel, closeSessionPanel, cleanupSessionsByTitle } from './helpers.js';
|
||
|
||
test.describe('E2E-07 知识图谱', () => {
|
||
let pageRef = null;
|
||
test.afterEach(async () => {
|
||
if (pageRef) await cleanupSessionsByTitle(pageRef, 'R1图谱');
|
||
});
|
||
|
||
test('实体搜索 → 邻域渲染 → 节点详情 → 答案图谱降级/渲染', async ({ page }) => {
|
||
test.setTimeout(180_000);
|
||
const t = tier(page);
|
||
pageRef = page;
|
||
await cleanupSessionsByTitle(page, 'R1图谱');
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
|
||
// 打开知识图谱 tab
|
||
await selectBenchTab(page, '知识图谱');
|
||
const sc = benchScope(page);
|
||
const kgPane = sc.locator('.kg-pane');
|
||
await expect(kgPane).toBeVisible({ timeout: 10_000 });
|
||
|
||
// 1. 空态(无问答时提示引导搜索)
|
||
await expect(kgPane.locator('.empty-note')).toContainText('图谱', { timeout: 10_000 });
|
||
|
||
// 2. 实体搜索(真实 Neo4j;精确/前缀匹配,<1s 内返回——超时上限放宽到 10s)
|
||
const searchInput = kgPane.locator('.search-box');
|
||
await searchInput.fill('OsSPL14');
|
||
await kgPane.locator('.search-btn').click();
|
||
await expect(kgPane.locator('.result-item').first()).toBeVisible({ timeout: 10_000 });
|
||
await expect(kgPane.locator('.result-item').first()).toContainText('OsSPL14');
|
||
|
||
// 3. 选中结果 → 邻域渲染(一跳)。真实数据 OsSPL14 一跳返回 51 节点,
|
||
// 按 FR-KG-001 节点数 >50 产品降级为列表视图——先等待两种终态之一出现,再按形态断言
|
||
const firstResult = kgPane.locator('.result-item').first();
|
||
await firstResult.click();
|
||
await expect(kgPane.locator('.degrade-note, .graph-svg').first()).toBeVisible({ timeout: 20_000 });
|
||
const degraded = (await kgPane.locator('.degrade-note').count()) > 0;
|
||
if (degraded) {
|
||
await expect(kgPane.locator('.node-list')).toBeVisible();
|
||
await expect(kgPane.locator('.node-list-item').first()).toBeVisible();
|
||
} else {
|
||
await expect(kgPane.locator('.node-g').first()).toBeVisible();
|
||
}
|
||
await expect(kgPane.locator('.nb-center')).toContainText(/OsSPL14/i, { timeout: 20_000 });
|
||
|
||
// 4. 节点详情:图模式点 node-g;列表模式点 node-list-item(均打开侧滑面板)
|
||
if (degraded) {
|
||
await kgPane.locator('.node-list-item').first().click();
|
||
} else {
|
||
await kgPane.locator('.node-g').first().click();
|
||
}
|
||
await expect(page.locator('.kg-entity-detail')).toBeVisible({ timeout: 10_000 });
|
||
await expect(page.locator('.kg-entity-detail .close-btn')).toBeVisible();
|
||
await page.locator('.kg-entity-detail .close-btn').click();
|
||
await expect(page.locator('.kg-entity-detail')).toHaveCount(0);
|
||
|
||
// 5. 邻域深度切换(深度 2 较慢,断言慢提示存在即可)
|
||
if (await kgPane.locator('.depth-row').count()) {
|
||
await kgPane.locator('.depth-row .chip').nth(1).click();
|
||
await expect(kgPane.locator('.nb-slow, .depth-hint').first()).toBeAttached({ timeout: 60_000 });
|
||
}
|
||
|
||
// 6. 答案图谱路径:经历史会话(已有消息)验证 graph 块渲染路径。
|
||
// 先关闭工作台(手机覆盖层/平板抽屉 scrim 会拦截后续顶栏点击),再打开会话面板
|
||
if (tier(page) === 'mobile') {
|
||
await page.locator('.bench-overlay .bench-close-btn').click();
|
||
await expect(page.locator('.bench-overlay')).toHaveCount(0);
|
||
} else if (tier(page) === 'tablet') {
|
||
await page.keyboard.press('Escape');
|
||
await expect(page.locator('.drawer.right.open')).toHaveCount(0);
|
||
}
|
||
await openSessionPanel(page);
|
||
const item = page.locator('.session-item').first();
|
||
await item.click();
|
||
await expect(page.locator('.msg.assistant, .empty-state')).toBeAttached({ timeout: 10_000 });
|
||
// 关闭会话抽屉(scrim 会拦截工作台按钮点击),再打开知识图谱 tab
|
||
await closeSessionPanel(page);
|
||
await selectBenchTab(page, '知识图谱');
|
||
// 历史消息 graph 块渲染为画布或降级列表(节点 >50 → 降级提示 + 列表视图)
|
||
await expect(
|
||
page.locator('.kg-pane .graph-svg, .kg-pane .node-list, .kg-pane .empty-note').first()
|
||
).toBeVisible({ timeout: 15_000 });
|
||
if (await page.locator('.kg-pane .degrade-note').count()) {
|
||
await expect(page.locator('.kg-pane .node-list')).toBeVisible();
|
||
}
|
||
});
|
||
});
|
||
|
||
test.describe('E2E-08 提取任务', () => {
|
||
test('任务列表 → 详情展开 → 运行日志 → 上传(mock 成功)', async ({ page }) => {
|
||
test.setTimeout(120_000);
|
||
await cleanupSessionsByTitle(page, 'R1抽取');
|
||
|
||
// 上传接口 mock:成功响应(避免真实 MinerU 环境问题影响 UI 断言)
|
||
await page.route('**/api/extraction/upload', (route) =>
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ success: true, job_id: 'mockjob000001', status_url: '/api/extraction/jobs/mockjob000001', message: '已提交抽取任务' }),
|
||
})
|
||
);
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
await selectBenchTab(page, '提取任务');
|
||
const sc = benchScope(page);
|
||
const extPane = sc.locator('.extraction-pane');
|
||
|
||
// 1. 任务列表(真实上游 49 个历史任务)
|
||
await expect(extPane.locator('.prog-item').first()).toBeVisible({ timeout: 15_000 });
|
||
const pctText = await extPane.locator('.pi-pct').first().textContent();
|
||
expect(pctText).toMatch(/%|失败|错误/); // 百分比或 failed 错误态
|
||
|
||
// 2. 详情展开(点击头部)
|
||
const firstJob = extPane.locator('.prog-item').first();
|
||
await firstJob.locator('.pi-head').click();
|
||
await expect(extPane.locator('.job-detail').first()).toBeVisible({ timeout: 15_000 });
|
||
await expect(extPane.locator('.job-detail .status-row')).toBeVisible();
|
||
|
||
// 3. 运行日志(mono 展示;真实上游为空属正常——DISCOVERY.md §3.4)
|
||
await extPane.locator('.log-toggle').click();
|
||
await expect(extPane.locator('.run-log')).toBeVisible({ timeout: 10_000 });
|
||
|
||
// 4. 上传(mock 成功):选择文件 → 成功 ✓ 提示 + 刷新列表
|
||
const fileInput = extPane.locator('.hidden-input');
|
||
await fileInput.setInputFiles({
|
||
name: 'r1-sample.pdf',
|
||
mimeType: 'application/pdf',
|
||
buffer: Buffer.from('%PDF-1.4 mock'),
|
||
});
|
||
await expect(extPane.locator('.upload-msg.ok')).toBeVisible({ timeout: 10_000 });
|
||
|
||
// 5. 非 PDF 被拒(前端拦截,不发请求)
|
||
await fileInput.setInputFiles({
|
||
name: 'not-a-pdf.txt',
|
||
mimeType: 'text/plain',
|
||
buffer: Buffer.from('hello'),
|
||
});
|
||
await expect(extPane.locator('.upload-msg.err')).toContainText('仅支持 PDF', { timeout: 10_000 });
|
||
});
|
||
});
|