Files
PlantAI-rag-new/tests/e2e/helpers.js
T

191 lines
7.4 KiB
JavaScript
Raw Normal View History

// helpers.js —— T 归属。L5 用例公共工具:档位判定、会话面板/工作台入口、提问与等待、清理。
// 注意:抽屉在 DOM 中常驻(Teleport),关闭态仅 translateX 移出视口——Playwright 的 :visible
// 仍会命中它,因此所有交互选择器必须按容器精确限定(aside-host / .drawer.left / .drawer.right / .bench-overlay)。
import { expect } from '@playwright/test';
export function tier(page) {
const w = page.viewportSize().width;
if (w >= 1280) return 'desktop';
if (w >= 1024) return 'laptop';
if (w >= 768) return 'tablet';
return 'mobile';
}
/** 会话面板精确定位(桌面=左栏;平板/手机=左抽屉内) */
export function sessionPanel(page) {
return page.locator('.aside-host .session-panel, .drawer.left .session-panel');
}
/** 工作台面板精确定位(桌面=右栏;平板=右抽屉内;手机=覆盖层内) */
export function benchPanel(page) {
return page.locator('.bench-wrap .bench-panel, .drawer.right .bench-panel, .bench-overlay .bench-panel');
}
/** 当前档位下「唯一可交互」的工作台容器(按容器形态精确限定,避免命中隐藏副本) */
export function benchScope(page) {
const t = tier(page);
if (t === 'desktop' || t === 'laptop') return page.locator('.bench-wrap');
if (t === 'tablet') return page.locator('.drawer.right');
return page.locator('.bench-overlay');
}
/** 打开会话面板(平板/手机点汉堡;桌面已常驻) */
export async function openSessionPanel(page) {
const t = tier(page);
if (t === 'tablet' || t === 'mobile') {
if (!(await page.locator('.drawer.left.open').count())) {
await page.locator('.hamburger-btn').click();
await expect(page.locator('.drawer.left.open')).toBeVisible();
}
}
}
/** 关闭会话面板(抽屉形态:Esc 优先;scrim 点击取右侧未被抽屉覆盖的坐标) */
export async function closeSessionPanel(page) {
const t = tier(page);
if (t === 'tablet' || t === 'mobile') {
if (await page.locator('.drawer.left.open').count()) {
// 抽屉(左 260px)覆盖 scrim 中心,直接点 scrim 元素中心会因被遮挡而挂起——
// 用 Esc(Drawer 监听 document keydown),失败再点 scrim 右侧安全坐标
await page.keyboard.press('Escape');
if (await page.locator('.drawer.left.open').count()) {
const vp = page.viewportSize();
await page.mouse.click(vp.width - 15, Math.floor(vp.height / 2));
}
await expect(page.locator('.drawer.left.open')).toHaveCount(0);
}
}
}
/** 打开工作台(桌面=右栏已可见;平板=右抽屉;手机=全屏覆盖层) */
export async function openBench(page) {
const t = tier(page);
if (t === 'tablet') {
if (!(await page.locator('.drawer.right.open').count())) {
await page.locator('.bench-btn').click();
await expect(page.locator('.drawer.right.open')).toBeVisible();
}
} else if (t === 'mobile') {
if (!(await page.locator('.bench-overlay').count())) {
await page.locator('.bench-btn').click();
await expect(page.locator('.bench-overlay')).toBeVisible();
}
}
}
/** 工作台 tab 按钮(按容器精确限定) */
export function benchTab(page, label) {
const t = tier(page);
const scope =
t === 'desktop' ? '.bench-wrap' : t === 'tablet' ? '.drawer.right' : '.bench-overlay .bench-overlay-tabs';
return page.locator(`${scope} .bench-tab`).filter({ hasText: label }).first();
}
/** 点击工作台某个 tab */
export async function selectBenchTab(page, label) {
await openBench(page);
await benchTab(page, label).click();
}
/** 新建会话(返回后会话面板保持打开;调用方按需 closeSessionPanel) */
export async function newSession(page) {
await openSessionPanel(page);
const before = await page.locator('.session-item').count();
await sessionPanel(page).locator('.new-task-btn').click();
await expect(page.locator('.session-item')).toHaveCount(before + 1, { timeout: 10_000 });
await expect(page.locator('.empty-state')).toBeVisible();
}
/** 模式 key → 中文 label(labels.MODES:ask=问答 plan=方案 search=检索) */
const MODE_LABELS = { ask: '问答', plan: '方案', search: '检索' };
/** 在作曲器提问(可选切换模式 pill;mode 为 key,按中文 label 匹配) */
export async function submitQuestion(page, question, mode = 'ask') {
const composer = page.locator('.composer');
if (mode !== 'ask') {
const label = MODE_LABELS[mode] || mode;
await composer.locator('.mode-pill').filter({ hasText: new RegExp(`^${label}$`) }).click();
await expect(composer.locator('.mode-pill.on')).toContainText(label);
}
await composer.locator('textarea').fill(question);
await composer.locator('.send-btn').click();
}
/**
* 等待本轮问答终态(pipeline 徽章 done/error),期间观测相位 running 推进。
* 返回 { done, sawRunning };揭示动画未完成时点击「跳过」加速。
*/
export async function waitForAnswerEnd(page, { timeout = 240_000 } = {}) {
let sawRunning = false;
const badge = page.locator('.msg.assistant .pipeline-card .badge').last();
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
if (await page.locator('.p-step.running').count()) sawRunning = true;
const cls = await badge.getAttribute('class').catch(() => '');
if (cls && cls.includes('done')) break;
if (cls && cls.includes('error')) break;
await page.waitForTimeout(800);
}
const cls = (await badge.getAttribute('class').catch(() => '')) || '';
// 揭示动画:出现跳过按钮则点击,加速拿到最终 text 块
const skip = page.locator('.msg.assistant .skip-btn').last();
if (await skip.count()) await skip.click().catch(() => {});
return { done: cls.includes('done'), sawRunning };
}
/** 清理本轮测试创建的会话(按标题前缀匹配,经 API 删除,避免污染共享库) */
export async function cleanupSessionsByTitle(page, prefix) {
try {
const list = await page.request.get('/api/sessions');
if (!list.ok()) return;
const items = await list.json();
for (const s of items) {
if ((s.title || '').startsWith(prefix)) {
await page.request.delete(`/api/sessions/${s.id}`);
}
}
} catch {
/* 清理失败不影响测试结论 */
}
}
/**
* 快照当前全部会话 ID(测试开始时调用)。
* 按 ID 精确清理可避免标题前缀误伤他人会话(共享 SQLite,PM 的集成会话同名共存)。
*/
export async function sessionIds(page) {
try {
const list = await page.request.get('/api/sessions');
if (!list.ok()) return null;
const items = await list.json();
return new Set(items.map((s) => s.id));
} catch {
return null;
}
}
/** 删除本轮新建的会话(ID 不在 preIds 快照内的) */
export async function cleanupNewSessions(page, preIds) {
if (!preIds) return;
try {
const list = await page.request.get('/api/sessions');
if (!list.ok()) return;
const items = await list.json();
for (const s of items) {
if (!preIds.has(s.id)) {
await page.request.delete(`/api/sessions/${s.id}`);
}
}
} catch {
/* 清理失败不影响测试结论 */
}
}
/** 页面控制台错误收集(失败证据用) */
export function trackConsoleErrors(page, bucket) {
page.on('console', (msg) => {
if (msg.type() === 'error') bucket.push(msg.text());
});
page.on('pageerror', (err) => bucket.push(String(err)));
}