// 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 }); }); });