test: 截图测试office套件文件的展示

This commit is contained in:
肖应宇 2026-04-11 14:24:13 +08:00 committed by Titan
parent 99f6f8dac2
commit c2313466d6
1 changed files with 222 additions and 0 deletions

View File

@ -0,0 +1,222 @@
import {
expect,
test,
type Locator,
type Page,
type TestInfo,
} from "@playwright/test";
import { v4 as uuid } from "uuid";
import {
newChatEntry,
openChat,
sendMessage,
waitForMessageListReady,
} from "./support/chat-helpers";
const FILE_CASES = [
{ kind: "html", label: "html", regex: /\.html?/i },
{
kind: "image",
label: "image",
regex: /\.(png|jpe?g|gif|webp|bmp|svg|ico|avif|tiff?)/i,
},
{ kind: "md", label: "md", regex: /\.md/i },
{ kind: "docx", label: "docx", regex: /\.docx?/i },
{ kind: "pptx", label: "pptx", regex: /\.pptx?/i },
{ kind: "xlsx", label: "xlsx", regex: /\.xlsx?/i },
] as const;
test.use({
video: "on",
screenshot: "on",
});
test.describe("聊天工作台 / 智能体产物生成预览与下载", () => {
test("DF-ART-GEN-001 生成并逐个点击 html/image/md/docx/pptx/xlsx 卡片截图", async ({
page,
}, testInfo) => {
const startedAt = Date.now();
test.setTimeout(12 * 60 * 1000);
const threadId = uuid();
logStatus("开始测试", `threadId=${threadId}`);
await openChat(page, newChatEntry(threadId));
await waitForMessageListReady(page, { requireMessages: false });
logStatus("发送生成指令");
await sendMessage(page, buildGeneratePrompt());
await waitForArtifactsReady(page, FILE_CASES, startedAt);
await openArtifactPanel(page);
logStatus("Artifacts 列表已就绪,开始逐类校验");
await capture(page, testInfo, "artifact-list-ready");
for (const file of FILE_CASES) {
logStatus("校验文件类型", file.label);
const card = artifactCardByPattern(page, file.regex);
await expect(card).toBeVisible();
await card.click();
logStatus("点击并截图", file.label);
await waitAfterCardClick(page, file.kind);
await capture(page, testInfo, `card-clicked-${file.label}`);
logStatus("类型处理完成", file.label);
}
logStatus("测试完成");
});
});
function buildGeneratePrompt(): string {
return [
"请一次性创建以下 6 个文件到 /mnt/user-data/outputs并在完成后调用 present_files",
"1) e2e-artifacts-page.html包含标题 DF_E2E_HTML 和一段正文。",
"2) e2e-artifacts-image.png生成一张包含文字 DF_E2E_IMAGE 的图片。",
"3) e2e-artifacts-notes.md标题为 DF_E2E_MD并引用上面的图片。",
"4) e2e-artifacts-report.docx包含标题 DF_E2E_DOCX 和一段文字。",
"5) e2e-artifacts-slides.pptx至少 2 页,包含 DF_E2E_PPTX。",
"6) e2e-artifacts-table.xlsx至少 2 列 3 行,并包含 DF_E2E_XLSX。",
"注意:所有文件都要真实写入输出目录,不要只在回复里描述。",
].join("\n");
}
async function openArtifactPanel(page: Page): Promise<void> {
const button = page.getByTestId("artifacts-open-button");
await expect(button).toBeVisible({ timeout: 120_000 });
await button.click();
await expect(page.getByTestId("artifact-file-list").first()).toBeVisible();
}
async function waitForArtifactsReady(
page: Page,
requiredCases: ReadonlyArray<(typeof FILE_CASES)[number]>,
startedAt: number,
): Promise<void> {
let pollRound = 0;
await expect
.poll(
async () => {
pollRound += 1;
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
const list = page.getByTestId("artifact-file-list").first();
// 1) 优先直接检查已展示的 artifact-file-list
if (!(await list.isVisible().catch(() => false))) {
// 2) 列表不存在时再尝试通过按钮打开
const openButton = page.getByTestId("artifacts-open-button").first();
if (!(await openButton.isVisible().catch(() => false))) {
logStatus(
"等待 artifacts 入口或列表出现",
`轮次=${pollRound}, 已耗时=${elapsedSeconds}s`,
);
return false;
}
await openButton.click();
await expect(page.getByTestId("artifact-file-list").first()).toBeVisible({
timeout: 5_000,
});
}
const allFileNames = await getArtifactFileNames(page);
const found = requiredCases
.filter((fileCase) =>
allFileNames.some((name) => fileCase.regex.test(name)),
)
.map((fileCase) => fileCase.label);
const missing = requiredCases
.filter(
(fileCase) =>
!allFileNames.some((name) => fileCase.regex.test(name)),
)
.map((fileCase) => fileCase.label);
logStatus(
"等待文件类型齐全",
`轮次=${pollRound}, 已耗时=${elapsedSeconds}s, 已找到=[${found.join(", ")}], 缺失=[${missing.join(", ")}]`,
);
return missing.length === 0;
},
{
timeout: 8 * 60 * 1000,
intervals: [1000, 2000, 3000, 5000],
},
)
.toBeTruthy();
}
function artifactCardByPattern(page: Page, pattern: RegExp): Locator {
return page.locator("[data-testid='artifact-file-card']").filter({
has: page.locator("[data-slot='card-title'] div[title]").filter({
hasText: pattern,
}),
}).first();
}
async function waitAfterCardClick(page: Page, kind: string): Promise<void> {
if (kind === "docx") {
await expect(page.locator(".docx-preview-wrap").first()).toBeVisible({
timeout: 60_000,
});
return;
}
if (kind === "xlsx") {
await expect(page.locator("#artifact-xlsx-preview").first()).toBeVisible({
timeout: 60_000,
});
return;
}
if (kind === "pptx") {
await expect(page.locator(".pptx-preview-wrap").first()).toBeVisible({
timeout: 60_000,
});
return;
}
if (kind === "md") {
await page.waitForTimeout(1200);
return;
}
await expect(page.getByTitle(/Artifact preview(?::.*)?$/i)).toBeVisible({
timeout: 30_000,
});
}
async function capture(page: Page, testInfo: TestInfo, name: string): Promise<void> {
const path = testInfo.outputPath(`${name}.png`);
await page.screenshot({ path, fullPage: true });
await testInfo.attach(name, {
path,
contentType: "image/png",
});
}
function logStatus(step: string, detail?: string): void {
const timestamp = new Date().toISOString();
if (detail) {
console.log(`[E2E][${timestamp}] ${step} | ${detail}`);
return;
}
console.log(`[E2E][${timestamp}] ${step}`);
}
async function getArtifactFileNames(page: Page): Promise<string[]> {
const titleNodes = page.locator(
"[data-testid='artifact-file-card'] [data-slot='card-title'] div[title]",
);
const titleCount = await titleNodes.count();
if (titleCount > 0) {
const names: string[] = [];
for (let i = 0; i < titleCount; i += 1) {
const value = (await titleNodes.nth(i).getAttribute("title"))?.trim();
if (value) {
names.push(value);
}
}
return names;
}
// fallback: if title attr is absent, use first text line of each card
const cardTexts = await page.getByTestId("artifact-file-card").allTextContents();
return cardTexts
.map((text) => text.split("\n")[0]?.trim() ?? "")
.filter(Boolean);
}