import { expect, test } from "@playwright/test"; import { THREAD_WITH_HISTORY, THREAD_WITH_MARKDOWN, THREAD_WITH_TODOS, openChat, reuseThreadChatEntry, skipIfMissingThread, waitForMessageListReady, } from "./support/chat-helpers"; async function waitForAnyMessages( page: Parameters[0], timeoutMs = 15_000, ) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const count = await page.locator(".is-user, .is-assistant").count(); if (count > 0) { return count; } await page.waitForTimeout(300); } return 0; } test.describe("聊天工作台 / 消息区与历史", () => { test("DF-MSG-001 固定 fixture 历史消息可见", async ({ page }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_HISTORY, "FRONTEND_E2E_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_HISTORY!)); await waitForMessageListReady(page, { requireMessages: false }); const messageCount = await waitForAnyMessages(page); testInfo.skip(messageCount === 0, "当前历史线程没有可见消息。"); await expect(page.locator(".is-user, .is-assistant").first()).toBeVisible(); }); test("DF-MSG-002 Markdown 消息结构可见", async ({ page }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_MARKDOWN, "FRONTEND_E2E_MARKDOWN_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_MARKDOWN!)); await waitForMessageListReady(page, { requireMessages: false }); const messageCount = await waitForAnyMessages(page); testInfo.skip( messageCount === 0, "当前线程没有可用于断言 Markdown 结构的历史消息。", ); const markdownCandidates = page.locator( ".is-assistant strong, .is-assistant ul li, .is-assistant ol li, .is-assistant code", ); await expect(markdownCandidates.first()).toBeVisible(); }); test("DF-MSG-003 长历史线程中可出现滚动到底部按钮", async ({ page, }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_HISTORY, "FRONTEND_E2E_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_HISTORY!)); await waitForMessageListReady(page, { requireMessages: false }); const messageCount = await waitForAnyMessages(page); testInfo.skip(messageCount === 0, "当前历史线程没有可见消息。"); const messageLog = page.getByRole("log").first(); const canScroll = await messageLog.evaluate((element) => { const target = element as HTMLElement; return target.scrollHeight - target.clientHeight > 20; }); testInfo.skip( canScroll === false, "当前线程消息区高度不足,无法触发滚动到底部按钮。", ); await messageLog.hover(); await page.mouse.wheel(0, -1200); await messageLog.evaluate((element) => { const target = element as HTMLElement; target.scrollTop = Math.max(0, target.scrollTop - 1200); }); await expect(page.getByTitle("滚动到底部")).toBeVisible(); }); test("DF-MSG-004 刷新前后用户消息关键内容保持一致", async ({ page, }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_HISTORY, "FRONTEND_E2E_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_HISTORY!)); await waitForMessageListReady(page, { requireMessages: false }); const normalizeText = (text: string) => text.replace(/\s+/g, " ").trim(); const beforeUsers = (await page.locator(".is-user").allTextContents()) .map(normalizeText) .filter(Boolean); testInfo.skip(beforeUsers.length === 0, "当前历史线程没有可见用户消息。"); await page.reload(); await expect(page.locator("textarea[name='message']")).toBeVisible(); await waitForMessageListReady(page, { requireMessages: false }); const afterUsers = (await page.locator(".is-user").allTextContents()) .map(normalizeText) .filter(Boolean); expect(afterUsers.length).toBe(beforeUsers.length); for (const sample of beforeUsers.slice( 0, Math.min(3, beforeUsers.length), )) { expect(afterUsers.some((text) => text.includes(sample))).toBeTruthy(); } }); test("DF-MSG-005 含 todos 的线程显示 To-dos 入口", async ({ page, }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_TODOS, "FRONTEND_E2E_TODOS_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_TODOS!)); await waitForMessageListReady(page, { requireMessages: false }); const todoButton = page.getByRole("button", { name: /To-dos/i }); testInfo.skip( (await todoButton.count()) === 0, "当前线程未展示 To-dos 入口。", ); await expect(todoButton).toBeVisible(); }); });