236 lines
7.1 KiB
TypeScript
236 lines
7.1 KiB
TypeScript
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.getByText("请下载ppt文件以获得最佳效果").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);
|
||
}
|