- 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>
143 lines
6.4 KiB
JavaScript
143 lines
6.4 KiB
JavaScript
// session-theme.spec.js —— T 归属。L5 主线 4 会话管理(TC-E2E-04-*)+ 主线 6 主题(TC-E2E-06-*)。
|
||
// R1 结构:新建/重命名/搜索/分组(核心,硬断言,缺陷独立);活动高亮与删除入口为缺陷探针
|
||
// (独立 test,失败即缺陷证据,不连累核心用例)。三档视口行为一致但会话数据共享——桌面执行核心,
|
||
// 平板/手机执行新建+搜索精简分支。
|
||
import { test, expect } from '@playwright/test';
|
||
import { tier, newSession, cleanupSessionsByTitle, cleanupNewSessions, sessionIds } from './helpers.js';
|
||
|
||
const PREFIX = 'R1会话-';
|
||
|
||
test.describe('E2E-04 会话管理', () => {
|
||
let pageRef = null;
|
||
let preIds = null;
|
||
test.afterEach(async () => {
|
||
// 按 ID 快照精确清理(标题前缀只覆盖已重命名的会话;未重命名的「新会话」按 ID 删)
|
||
if (pageRef) {
|
||
await cleanupSessionsByTitle(pageRef, PREFIX);
|
||
await cleanupNewSessions(pageRef, preIds);
|
||
}
|
||
});
|
||
|
||
test('新建/重命名/搜索过滤/分组(核心)', async ({ page }) => {
|
||
test.setTimeout(120_000);
|
||
pageRef = page;
|
||
preIds = await sessionIds(page);
|
||
await cleanupSessionsByTitle(page, PREFIX);
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
|
||
// 1. 新建两个会话(连点两次 → 两个独立会话,不重复)
|
||
const before = await page.locator('.session-item').count();
|
||
await newSession(page);
|
||
await newSession(page);
|
||
await expect(page.locator('.session-item')).toHaveCount(before + 2, { timeout: 10_000 });
|
||
|
||
// 2. 双击重命名(Enter 提交);标题同步到列表与顶栏(FR-SESSION-006)
|
||
const firstItem = page.locator('.session-item').first();
|
||
await firstItem.dblclick();
|
||
const renameInput = firstItem.locator('.rename-input');
|
||
await expect(renameInput).toBeVisible();
|
||
await renameInput.fill(`${PREFIX}甲`);
|
||
await renameInput.press('Enter');
|
||
await expect(page.locator('.session-item').first().locator('.title')).toContainText(`${PREFIX}甲`);
|
||
if (tier(page) !== 'mobile') {
|
||
await expect(page.locator('.top-title .t')).toContainText(`${PREFIX}甲`);
|
||
}
|
||
// 第二个会话也重命名,便于后文按前缀过滤断言
|
||
const secondItem = page.locator('.session-item').nth(1);
|
||
await secondItem.dblclick();
|
||
await secondItem.locator('.rename-input').fill(`${PREFIX}乙`);
|
||
await secondItem.locator('.rename-input').press('Enter');
|
||
await expect(page.locator('.session-item').nth(1).locator('.title')).toContainText(`${PREFIX}乙`);
|
||
|
||
// 3. 搜索过滤:大小写不敏感子串;无匹配文案;清空恢复;期间无网络请求(FR-SESSION-005)
|
||
let reqCount = 0;
|
||
const onReq = () => { reqCount += 1; };
|
||
page.on('request', onReq);
|
||
const searchBox = page.locator('.search-box input').first();
|
||
await searchBox.fill('r1会话'); // 小写 r1 应命中大写 R1(大小写不敏感)
|
||
await expect(page.locator('.session-item')).toHaveCount(2);
|
||
await searchBox.fill('甲');
|
||
await expect(page.locator('.session-item')).toHaveCount(1);
|
||
await searchBox.fill('不存在的会话名XYZ');
|
||
await expect(page.locator('.empty-search')).toContainText('无匹配会话');
|
||
await expect(page.locator('.session-item')).toHaveCount(0);
|
||
await searchBox.fill('');
|
||
await expect(page.locator('.session-item')).toHaveCount(before + 2);
|
||
expect(reqCount, '搜索过滤不应发网络请求').toBe(0);
|
||
page.off('request', onReq);
|
||
|
||
// 4. 分组标签(今天组存在——新会话更新时间为今天,FR-SESSION-001)
|
||
await expect(page.locator('.group-label').first()).toContainText('今天');
|
||
});
|
||
|
||
// 缺陷探针 A(BUG-R1-02 修复验证):点击会话应置活动态高亮(FR-SESSION-009)
|
||
test('活动态高亮探针', async ({ page }) => {
|
||
test.setTimeout(60_000);
|
||
pageRef = page;
|
||
preIds = await sessionIds(page);
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
await newSession(page);
|
||
await newSession(page);
|
||
|
||
// 点击第二项(选中它,并自动切换到它)
|
||
const second = page.locator('.session-item').nth(1);
|
||
await second.click();
|
||
// 硬断言:活动项应有 .active(FR-SESSION-009)
|
||
await expect(second, '点击会话后应显示活动态高亮(FR-SESSION-009)').toHaveClass(/active/, { timeout: 10_000 });
|
||
});
|
||
|
||
// 缺陷探针 B(BUG-R1-03 修复验证):删除入口是否存在(FR-SESSION-007 二次确认弹层需可达)
|
||
test('删除入口探针', async ({ page }) => {
|
||
test.setTimeout(60_000);
|
||
pageRef = page;
|
||
preIds = await sessionIds(page);
|
||
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
await newSession(page);
|
||
|
||
const firstItem = page.locator('.session-item').first();
|
||
await firstItem.click({ button: 'right' });
|
||
// 硬断言:右键应唤出删除确认弹层(BUG-R1-03 修复验证)
|
||
await expect(
|
||
page.locator('.confirm-box'),
|
||
'右键应唤出删除确认弹层(FR-SESSION-007 删除需二次确认)'
|
||
).toBeVisible({ timeout: 5_000 });
|
||
});
|
||
});
|
||
|
||
test.describe('E2E-06 主题', () => {
|
||
test('深浅切换即时生效 + 刷新持久化', async ({ page }) => {
|
||
test.setTimeout(60_000);
|
||
await page.goto('/');
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
|
||
// 初始主题(默认深色或 localStorage 已有值)
|
||
const initial = await page.evaluate(() => document.documentElement.dataset.theme);
|
||
const themeBtn = page.locator('.top-bar .icon-btn').last();
|
||
|
||
// 切换到另一主题 → dataset 即时更新 + localStorage 落值
|
||
await themeBtn.click();
|
||
await expect
|
||
.poll(() => page.evaluate(() => document.documentElement.dataset.theme), { timeout: 5_000 })
|
||
.not.toBe(initial);
|
||
const toggled = await page.evaluate(() => document.documentElement.dataset.theme);
|
||
const stored = await page.evaluate(() => localStorage.getItem('pa-theme'));
|
||
expect(stored).toBe(toggled);
|
||
|
||
// 刷新 → 保持
|
||
await page.reload();
|
||
await expect(page.locator('.app-shell')).toBeVisible();
|
||
await expect
|
||
.poll(() => page.evaluate(() => document.documentElement.dataset.theme), { timeout: 5_000 })
|
||
.toBe(toggled);
|
||
|
||
// 复原初始主题(避免影响后续用例的视觉断言)
|
||
if (toggled !== initial) await themeBtn.click();
|
||
});
|
||
});
|