- 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>
139 lines
7.1 KiB
JavaScript
139 lines
7.1 KiB
JavaScript
// main-flow.spec.js —— T 归属。L5 主线 1:主流程(TC-E2E-01-*)+ 主线 2:检索模式(TC-E2E-02-*)。
|
||
// 真实 BFF 上游:QA 约 37s/次(DISCOVERY.md);LLM 402 → answer 为 fallback 证据列表,
|
||
// 答案内容断言回退为结构断言(TEST_CASES.md §3.3,依赖充值)。
|
||
import { test, expect } from '@playwright/test';
|
||
import { tier, openSessionPanel, closeSessionPanel, newSession, submitQuestion, waitForAnswerEnd, cleanupSessionsByTitle, trackConsoleErrors } from './helpers.js';
|
||
|
||
test.describe('E2E-01 主流程', () => {
|
||
let pageRef = null;
|
||
test.afterEach(async () => {
|
||
if (pageRef) {
|
||
// 清理双前缀:新建时的 R1 前缀会被首问自动标题覆盖为问题前缀(FR-SESSION-008)
|
||
await cleanupSessionsByTitle(pageRef, 'R1主流程');
|
||
await cleanupSessionsByTitle(pageRef, '水稻SPL基因家族的功能研究');
|
||
}
|
||
});
|
||
|
||
test('主流程:提问→五相位推进→答案→来源/图谱 tab→刷新恢复', async ({ page }) => {
|
||
test.setTimeout(300_000);
|
||
const t = tier(page);
|
||
const titlePrefix = t === 'desktop' ? 'R1主流程' : 'R1主流程' + (t === 'tablet' ? '-T' : '-M');
|
||
pageRef = page;
|
||
const consoleErrors = [];
|
||
trackConsoleErrors(page, consoleErrors);
|
||
await cleanupSessionsByTitle(page, titlePrefix);
|
||
await cleanupSessionsByTitle(page, '水稻SPL基因家族的功能研究');
|
||
|
||
// 1. 首屏加载 → 欢迎空态(.empty-state)
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
|
||
// 2. 新建会话(桌面左栏 / 平板·手机经抽屉)
|
||
const listBefore = await page.locator('.session-item').count();
|
||
await newSession(page);
|
||
await expect(page.locator('.session-item')).toHaveCount(listBefore + 1, { timeout: 10_000 });
|
||
await closeSessionPanel(page);
|
||
await expect(page.locator('.empty-state')).toBeVisible();
|
||
|
||
// 3. 问答模式提问(真实上游 ~37s;240s 上限内)
|
||
const question = '水稻SPL基因家族的功能研究有哪些关键进展?';
|
||
await submitQuestion(page, question, 'ask');
|
||
|
||
// 4. 用户气泡立即出现 + 助手骨架 pipeline 五相位 waiting
|
||
await expect(page.locator('.msg.user .bubble').last()).toContainText(question.slice(0, 12), { timeout: 10_000 });
|
||
const pipe = page.locator('.msg.assistant .pipeline-card').last();
|
||
await expect(pipe).toBeVisible({ timeout: 10_000 });
|
||
await expect(pipe.locator('.p-step')).toHaveCount(5);
|
||
|
||
// 5. 五相位推进:检索相位 running(真实上游 92s 内必经);等待终态
|
||
await expect(pipe.locator('.p-step.running').first()).toBeVisible({ timeout: 150_000 });
|
||
const res = await waitForAnswerEnd(page, { timeout: 240_000 });
|
||
expect(res.sawRunning, '流式期间应有相位处于 running').toBe(true);
|
||
expect(res.done, 'pipeline 徽章终态应为 done').toBe(true);
|
||
|
||
// 6. 终态:pipeline 全部 done、消息含来源块与 text 块(402 fallback:结构断言,依赖充值)
|
||
await expect(pipe.locator('.p-step')).toHaveCount(5);
|
||
await expect(pipe.locator('.p-step.done')).toHaveCount(5);
|
||
const asst = page.locator('.msg.assistant').last();
|
||
await expect(asst.locator('.inline-sources .src-chip').first()).toBeVisible({ timeout: 20_000 });
|
||
|
||
// 揭示动画收尾:点跳过加速。终态判据 = 最终 text 块出现
|
||
//(reveal 完成时 stream 块被替换为 text 块,二者互斥;reduced-motion 环境直接定格 text)
|
||
const skip = asst.locator('.skip-btn');
|
||
if (await skip.count()) await skip.click().catch(() => {});
|
||
const mdText = asst.locator('.md-text').first();
|
||
await expect(mdText).toBeVisible({ timeout: 180_000 });
|
||
|
||
// 6b. Markdown 渲染硬断言(BUG-R1-06 已修复验证):
|
||
// 触发条件收紧为真实 markdown 语法——行首列表标记「- 」或成对 **加粗**,
|
||
// 避免把行内连字符(如 vegetative-to-reproductive)误判为列表
|
||
const rawText = await mdText.textContent().catch(() => '');
|
||
const listLike = /^- /m.test(rawText);
|
||
const boldLike = /\*\*[^*]+\*\*/.test(rawText);
|
||
if (listLike || boldLike) {
|
||
const html = await mdText.innerHTML().catch(() => '');
|
||
const listOk = !listLike || html.includes('<li');
|
||
const boldOk = !boldLike || html.includes('<strong') || html.includes('<em');
|
||
expect(
|
||
listOk && boldOk,
|
||
`答案含 markdown 语法(${listLike ? '行首列表' : ''}${listLike && boldLike ? '/' : ''}${boldLike ? '加粗' : ''}),应渲染为 HTML 标签(FR-CHAT-004),实际 HTML: ${html.slice(0, 200)}`
|
||
).toBe(true);
|
||
}
|
||
|
||
// 7. 会话状态 done:左栏条目状态点(桌面/抽屉内)
|
||
const activeItem = page.locator('.session-item.active').first();
|
||
await expect(activeItem).toBeVisible();
|
||
await expect(activeItem.locator('.status-dot.done')).toBeVisible({ timeout: 15_000 });
|
||
|
||
// 8. 刷新 → 点击该会话 → 历史恢复(全部块重渲染)
|
||
await page.reload();
|
||
await openSessionPanel(page);
|
||
// 点击自动标题为问题前 30 字符的会话
|
||
await page.locator('.session-item').filter({ hasText: '水稻SPL基因家族的功能研究' }).first().click();
|
||
await expect(page.locator('.msg.assistant').last()).toBeVisible({ timeout: 15_000 });
|
||
await expect(page.locator('.msg.assistant .pipeline-card').last()).toBeVisible();
|
||
await expect(page.locator('.msg.assistant .inline-sources .src-chip').first()).toBeVisible();
|
||
|
||
// 9. 控制台无未捕获错误
|
||
expect(consoleErrors, `页面应无 console error,实际: ${consoleErrors.join(' | ')}`).toHaveLength(0);
|
||
});
|
||
});
|
||
|
||
test.describe('E2E-02 检索模式', () => {
|
||
let pageRef = null;
|
||
test.afterEach(async () => {
|
||
if (pageRef) {
|
||
await cleanupSessionsByTitle(pageRef, 'R1检索');
|
||
await cleanupSessionsByTitle(pageRef, '水稻SPL基因家族的功能研究');
|
||
}
|
||
});
|
||
|
||
test('检索模式:只出来源不出答案块,use_llm=false', async ({ page }) => {
|
||
test.setTimeout(300_000);
|
||
const t = tier(page);
|
||
pageRef = page;
|
||
await cleanupSessionsByTitle(page, 'R1检索');
|
||
await cleanupSessionsByTitle(page, '水稻SPL基因家族的功能研究');
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
const listBefore = await page.locator('.session-item').count();
|
||
await newSession(page);
|
||
await expect(page.locator('.session-item')).toHaveCount(listBefore + 1);
|
||
await closeSessionPanel(page);
|
||
|
||
// 检索模式提问
|
||
await submitQuestion(page, '水稻SPL基因家族的功能研究有哪些关键进展?', 'search');
|
||
|
||
// 等待终态(真实 search 单次约 92s)
|
||
const res = await waitForAnswerEnd(page, { timeout: 240_000 });
|
||
expect(res.done, 'pipeline 徽章终态应为 done').toBe(true);
|
||
|
||
// 断言:无 text 答案块;有 sources 块;有 pipeline 卡
|
||
const asst = page.locator('.msg.assistant').last();
|
||
await expect(asst.locator('.inline-sources .src-chip').first()).toBeVisible({ timeout: 20_000 });
|
||
await expect(asst.locator('.md-text')).toHaveCount(0);
|
||
await expect(asst.locator('.stream-wrap')).toHaveCount(0);
|
||
});
|
||
});
|