deerflow2/frontend/tests/e2e/message-and-history.spec.ts

139 lines
4.7 KiB
TypeScript

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<typeof openChat>[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();
});
});