- tests/docgen/:DC-UNIT/API/CONF/E2E/UI/REGRESSION/PERF 分组用例与可复跑脚本,报告逐条留痕(R1-R3 全记录) - 意图 30 条前后端同表双跑(pytest + node) - Playwright headless:docgen-ui/docgen-regression spec + b3 benchpanel 第 5 TAB 断言更新 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
266 lines
14 KiB
JavaScript
266 lines
14 KiB
JavaScript
// docgen-ui.spec.js —— docgen P0 R1 UI 验收(测试 agent 归属)
|
||
// 覆盖:DC-UI-01/02/03/04/05/06/07/08/09/10/11/12/13 + DC-REGRESSION-04(可自动化部分)。
|
||
// 目标:vite 5180(TL 部署的 R1 被测前端)+ 真实 BFF 8090 / vstore 8094。
|
||
// 依赖真实 LLM(DeepSeek)的步骤耗时较长(生成 ~60-120s),全局 timeout 沿用 480s。
|
||
// 说明:涉及「精确视觉样式」的检查点(26×26 圆角/hairline 描边等)以 boundingBox 存在性
|
||
// 为准,纯视觉项按 R1 口径留人工待 TL 验收时复核。
|
||
|
||
import { test, expect } from '@playwright/test';
|
||
import { benchPanel } from './helpers.js';
|
||
|
||
const DOCGEN_Q = '帮我写一份年度工作总结';
|
||
const QA_Q = '水稻基因是什么?介绍一下';
|
||
|
||
async function sendText(page, text) {
|
||
const composer = page.locator('.composer-wrap');
|
||
await composer.locator('textarea').fill(text);
|
||
await composer.locator('.send-btn').click();
|
||
}
|
||
|
||
async function waitDocCard(page, timeout = 240_000) {
|
||
const card = page.locator('.msg.assistant .doc-card').last();
|
||
await expect(card).toBeVisible({ timeout });
|
||
return card;
|
||
}
|
||
|
||
test('DC-UI-03/05/06 | 自然触发 docgen:徽标 + pipeline 五步 + 文档卡 + 自动切文档 TAB', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
|
||
// 断言走 docgen 分支(generate 请求发出,且无 QA 流请求)
|
||
const genReq = page.waitForRequest((r) => r.url().includes('/api/docgen/generate'), { timeout: 30_000 });
|
||
const qaReqSeen = { n: 0 };
|
||
page.on('request', (r) => { if (r.url().includes('/api/qa/chat/stream')) qaReqSeen.n += 1; });
|
||
|
||
await sendText(page, DOCGEN_Q);
|
||
await genReq;
|
||
|
||
// 骨架:pipeline 五步 + 能力徽标「文档生成」
|
||
const pipe = page.locator('.msg.assistant .pipeline-card').last();
|
||
await expect(pipe).toBeVisible({ timeout: 10_000 });
|
||
await expect(pipe.locator('.p-step')).toHaveCount(5);
|
||
const capBadge = pipe.locator('.badge.cap').first();
|
||
await expect(capBadge).toBeVisible({ timeout: 10_000 });
|
||
expect(await capBadge.textContent()).toContain('文档生成');
|
||
|
||
// 等待生成完成(真实 LLM):pipeline done + 文档卡出现
|
||
await waitDocCard(page);
|
||
await expect(pipe.locator('.p-step.done')).toHaveCount(5, { timeout: 30_000 });
|
||
|
||
// 文档卡要素 + 自动切「文档」TAB
|
||
const card = page.locator('.msg.assistant .doc-card').last();
|
||
await expect(card.locator('.dc-open')).toBeVisible();
|
||
const bench = benchPanel(page);
|
||
await expect(bench.locator('.bench-tab.on')).toContainText('文档', { timeout: 15_000 });
|
||
await expect(bench.locator('.doc-pane')).toBeVisible();
|
||
|
||
// 无 QA 流请求(分流正确)
|
||
expect(qaReqSeen.n).toBe(0);
|
||
});
|
||
|
||
test('DC-UI-07 | 文档 TAB 预览:markdown 渲染 + 自动切 TAB 后内容可用', async ({ page }) => {
|
||
// 直接生成一个文档(复用 03 的路径),然后进 TAB 操作
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
await sendText(page, DOCGEN_Q);
|
||
await waitDocCard(page);
|
||
// R2:dev3 BUG-3/4 已修复(adoptArtifact 无 md 时自动拉取 content;open 同 id 且有内容复用)。
|
||
// 还原原始流程断言:生成成功自动切文档 TAB 后预览应直接出现(无需刷新绕行)。
|
||
const bench = benchPanel(page);
|
||
await expect(bench.locator('.bench-tab.on')).toContainText('文档', { timeout: 15_000 });
|
||
|
||
// 预览:markdown 渲染 HTML(标题/段落存在)
|
||
const preview = bench.locator('.doc-preview.md-text');
|
||
await expect(preview).toBeVisible({ timeout: 20_000 });
|
||
const html = await preview.innerHTML();
|
||
expect(html.includes('<h1') || html.includes('<h2')).toBe(true); // 标题渲染
|
||
|
||
});
|
||
|
||
test('DC-UI-08 | 就地编辑:真实失焦 PATCH + 块间切换不误触发 + 刷新持久(R3 focusout 修复版)', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
await sendText(page, DOCGEN_Q);
|
||
await waitDocCard(page);
|
||
const bench = benchPanel(page);
|
||
await expect(bench.locator('.bench-tab.on')).toContainText('文档', { timeout: 15_000 });
|
||
await expect(bench.locator('.doc-preview.md-text')).toBeVisible({ timeout: 20_000 });
|
||
|
||
// 进入编辑模式
|
||
await bench.locator('.dh-actions .pane-btn').first().click();
|
||
const editRoot = bench.locator('.doc-edit.md-text');
|
||
await expect(editRoot).toBeVisible();
|
||
|
||
const patches = [];
|
||
page.on('request', (r) => { if (r.url().includes('/content') && r.method() === 'PATCH') patches.push(r.url()); });
|
||
|
||
// R3 阶段 1:真实用户流程——编辑块 → 失焦到编辑区外 → 必现 PATCH(R2 证据 realBlockBlurPATCH=0 应翻转)
|
||
const blocks = editRoot.locator('[contenteditable="true"]');
|
||
await expect(blocks.first()).toBeVisible({ timeout: 10_000 });
|
||
await blocks.first().click();
|
||
await blocks.first().fill('R3就地编辑验证段落。');
|
||
// 失焦到编辑区外(点击非编辑元素 .dh-title,焦点离开 editRoot)
|
||
await bench.locator('.dh-title').click();
|
||
await page.waitForTimeout(2500); // focusout(30ms) + scheduleSave 800ms debounce + 余量
|
||
expect(patches.length, 'DC-UI-08 阶段1 FAIL:真实失焦到编辑区外未触发 PATCH 保存').toBeGreaterThanOrEqual(1);
|
||
const patchesAfterPhase1 = patches.length;
|
||
|
||
// R3 阶段 2:块间焦点切换不误触发(relatedTarget 在编辑区内 → 不序列化)
|
||
await blocks.nth(1).click(); // 焦点从 body 进入块 2(进入不触发保存)
|
||
await page.waitForTimeout(1200);
|
||
expect(patches.length, 'DC-UI-08 阶段2 FAIL:块间焦点切换误触发了 PATCH').toBe(patchesAfterPhase1);
|
||
await blocks.nth(1).fill('R3第二段修改。');
|
||
await blocks.nth(2).click(); // 块 2 → 块 3 焦点迁移(编辑区内)
|
||
await page.waitForTimeout(1200);
|
||
expect(patches.length, 'DC-UI-08 阶段2 FAIL:编辑区内焦点迁移误触发了 PATCH').toBe(patchesAfterPhase1);
|
||
// 最后失焦出编辑区 → 一次 PATCH(含两处修改)
|
||
await bench.locator('.dh-title').click();
|
||
await page.waitForTimeout(2500);
|
||
expect(patches.length, 'DC-UI-08 阶段2 FAIL:出编辑区后未保存两处修改').toBe(patchesAfterPhase1 + 1);
|
||
|
||
// R3 阶段 3:刷新/重进 TAB 内容不丢(PATCH 已落 BFF)
|
||
await page.reload();
|
||
const sess = page.locator('.session-item').filter({ hasText: DOCGEN_Q }).first();
|
||
await expect(sess).toBeVisible({ timeout: 15_000 });
|
||
await sess.click();
|
||
const card = page.locator('.msg.assistant .doc-card').last();
|
||
await expect(card).toBeVisible({ timeout: 20_000 });
|
||
await card.locator('.dc-open').click();
|
||
await expect(bench.locator('.doc-preview.md-text')).toBeVisible({ timeout: 20_000 });
|
||
const savedHtml = await bench.locator('.doc-preview.md-text').innerHTML();
|
||
expect(savedHtml.includes('R3就地编辑验证段落'), 'DC-UI-08 阶段3 FAIL:刷新后编辑内容未持久').toBe(true);
|
||
expect(savedHtml.includes('R3第二段修改'), 'DC-UI-08 阶段3 FAIL:第二段编辑内容未持久').toBe(true);
|
||
});
|
||
|
||
test('DC-UI-09 | 「用 prompt 修改」输入行 → edit 端点(真实 LLM)', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
await sendText(page, DOCGEN_Q);
|
||
await waitDocCard(page);
|
||
const bench = benchPanel(page);
|
||
await expect(bench.locator('.bench-tab.on')).toContainText('文档', { timeout: 15_000 });
|
||
|
||
const promptInput = bench.locator('.edit-prompt-row input, .edit-prompt-row textarea').first();
|
||
await expect(promptInput).toBeVisible();
|
||
const editResp = page.waitForResponse((r) => r.url().includes('/edit') && r.request().method() === 'POST', { timeout: 30_000 });
|
||
await promptInput.fill('帮我改写这份总结');
|
||
await bench.locator('.edit-prompt-row button').first().click();
|
||
const resp = await editResp;
|
||
// R1 实测:BFF 护栏把 edit 类指令判为 422(dev2 路由护栏与意图词典口径问题——见报告 R1 记录)
|
||
// 前端按失败可见原则展示 editError;本断言记录真实结果(不做预期美化)
|
||
const status = resp.status();
|
||
expect([200, 422]).toContain(status);
|
||
if (status === 422) {
|
||
await expect(bench.locator('.doc-save-state.error').first()).toBeVisible({ timeout: 15_000 });
|
||
}
|
||
});
|
||
|
||
test('DC-UI-10 | 导出 PDF(DocPane 按钮 → export 端点)', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
await sendText(page, DOCGEN_Q);
|
||
await waitDocCard(page);
|
||
const bench = benchPanel(page);
|
||
await expect(bench.locator('.bench-tab.on')).toContainText('文档', { timeout: 15_000 });
|
||
|
||
const expResp = page.waitForResponse((r) => r.url().includes('/export') && r.request().method() === 'POST', { timeout: 30_000 });
|
||
await bench.locator('.export-row .pane-btn.exp').first().click(); // PDF
|
||
const resp = await expResp;
|
||
// R1 实测:export 同样被意图护栏 422(见报告);记录真实结果
|
||
expect([200, 422]).toContain(resp.status());
|
||
});
|
||
|
||
test('DC-UI-11 | 右栏拖宽 + localStorage 持久化', async ({ page }) => {
|
||
await page.goto('/');
|
||
const resizer = page.locator('.bench-resizer');
|
||
await expect(resizer).toBeVisible();
|
||
|
||
const wrap = page.locator('.bench-wrap');
|
||
const before = await wrap.evaluate((el) => el.getBoundingClientRect().width);
|
||
|
||
const box = await resizer.boundingBox();
|
||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||
await page.mouse.down();
|
||
await page.mouse.move(box.x - 120, box.y + box.height / 2, { steps: 8 });
|
||
await page.mouse.up();
|
||
|
||
const after = await wrap.evaluate((el) => el.getBoundingClientRect().width);
|
||
expect(after).toBeGreaterThan(before); // 向左拖 → 变宽
|
||
expect(after).toBeLessThanOrEqual(Math.round((await page.viewportSize()).width * 0.6));
|
||
|
||
const stored = await page.evaluate(() => localStorage.getItem('pa-bench-width'));
|
||
expect(stored).not.toBeNull();
|
||
|
||
await page.reload();
|
||
await expect(page.locator('.bench-wrap')).toBeVisible();
|
||
const afterReload = await wrap.evaluate((el) => el.getBoundingClientRect().width);
|
||
expect(Math.round(afterReload)).toBe(Math.round(after)); // 刷新保持
|
||
});
|
||
|
||
test('DC-UI-12 | 折叠手柄(右栏 28px 点击区保留 + 状态持久化)', async ({ page }) => {
|
||
await page.goto('/');
|
||
const handle = page.locator('.bench-wrap > .bench-handle-btn'); // P3 AppShell 手柄(旧 BenchPanel 按钮为第二处)
|
||
await expect(handle).toBeVisible();
|
||
const hb = await handle.boundingBox();
|
||
expect(hb).not.toBeNull();
|
||
|
||
await handle.click();
|
||
await expect(page.locator('.app-shell')).toHaveClass(/bench-collapsed/); // 折叠类绑在 app-shell
|
||
const collapsed = await page.evaluate(() => localStorage.getItem('pa-bench-collapsed'));
|
||
expect(collapsed).not.toBeNull();
|
||
// 折叠态 resizer 隐藏(不拖拽)
|
||
await expect(page.locator('.bench-resizer')).toBeHidden();
|
||
|
||
await handle.click();
|
||
await expect(page.locator('.app-shell')).not.toHaveClass(/bench-collapsed/);
|
||
});
|
||
|
||
test('DC-UI-13 | 聊天框「导出成PDF」→ export 分支(无 generate 请求)', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
// 先生成一个活动文档
|
||
await sendText(page, DOCGEN_Q);
|
||
await waitDocCard(page);
|
||
await expect(benchPanel(page).locator('.bench-tab.on')).toContainText('文档', { timeout: 15_000 });
|
||
|
||
// 输入导出指令:必须走 export 分支(不产生 generate 请求)
|
||
const genSeen = { n: 0 };
|
||
page.on('request', (r) => { if (r.url().includes('/api/docgen/generate')) genSeen.n += 1; });
|
||
const expReq = page.waitForRequest((r) => r.url().includes('/api/docgen/artifacts') && r.url().includes('/export'), { timeout: 30_000 });
|
||
await sendText(page, '导出成PDF');
|
||
await expReq;
|
||
expect(genSeen.n).toBe(0); // 分流正确:无 generate
|
||
});
|
||
|
||
test('DC-UI-04 | QA 原路径不回退(30 条 #8)', async ({ page }) => {
|
||
// R1 口径:QA 真实流会触发既有 BFF 断流崩溃问题(TL 已单独记录),
|
||
// 本用例改用「打开已有 QA 会话」验证 QA 路径渲染不回退(不触发流)。
|
||
// 前端路由分流本体由 DC-INTENT-CASES(前端 30/30)+ DC-UI-03(docgen 分支请求断言)覆盖。
|
||
await page.goto('/');
|
||
const item = page.locator('.session-item').filter({ hasText: '水稻SPL基因家族研究有哪些新的进展?' }).first();
|
||
await expect(item).toBeVisible({ timeout: 10_000 });
|
||
await item.click();
|
||
// QA 历史消息恢复:pipeline 卡存在且无 docgen 文档卡
|
||
const pipe = page.locator('.msg.assistant .pipeline-card').last();
|
||
await expect(pipe).toBeVisible({ timeout: 15_000 });
|
||
expect(await page.locator('.msg.assistant .doc-card').count()).toBe(0);
|
||
// 无 docgen generate 请求
|
||
const genSeen = { n: 0 };
|
||
page.on('request', (r) => { if (r.url().includes('/api/docgen/generate')) genSeen.n += 1; });
|
||
await page.waitForTimeout(2000);
|
||
expect(genSeen.n).toBe(0);
|
||
});
|
||
|
||
test('DC-REGRESSION-04 | 历史/QA 消息 pipeline 卡无徽标(capabilities 缺省零渲染)', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.locator('.composer-wrap textarea')).toBeVisible();
|
||
// 一条 QA 消息(等待其 pipeline 骨架出现即可,不必等完整答案)
|
||
// R1 口径:不触发真实 QA 流(既有 BFF 断流崩溃问题),打开历史 QA 会话验证
|
||
const item = page.locator('.session-item').filter({ hasText: '水稻SPL基因家族研究有哪些新的进展?' }).first();
|
||
await expect(item).toBeVisible({ timeout: 10_000 });
|
||
await item.click();
|
||
const qaPipe = page.locator('.msg.assistant .pipeline-card').last();
|
||
await expect(qaPipe).toBeVisible({ timeout: 15_000 });
|
||
expect(await qaPipe.locator('.badge.cap').count()).toBe(0); // 无 capabilities → 零渲染
|
||
});
|