// 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(); }); });