Files
PlantAI-rag-new/tests/e2e/main-flow.spec.js
T
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

139 lines
7.1 KiB
JavaScript
Raw 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.
// 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);
});
});