ai-chatbot/tests/routes/canvas-chat-helpers.test.ts

511 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from "@playwright/test";
import {
collectMessageImageUrls,
extractAssistantImageUrls,
extractVideoUrls,
normalizeMediaUrl,
stripMediaMarkup,
} from "@/components/canvas/chat-panel/canvas-chat-media";
import {
buildDetailResultMarkdown,
buildDetailReviewAppendMarkdown,
buildDetailReviewPlans,
} from "@/components/canvas/chat-panel/canvas-detail-markdown";
import {
DETAIL_BUNDLE_SLOT_INDEXES,
DETAIL_GENERATION_MODE_OPTIONS,
DETAIL_MODEL_SLOT_INDEX,
DETAIL_PRODUCT_SLOT_INDEXES,
DETAIL_SLOT_CONFIGS,
DETAIL_TARGET_HEIGHT,
DETAIL_TARGET_WIDTH,
extractDetailFrameCount,
extractFirstNumber,
extractFontWeight,
normalizeDetailGenerationMode,
sanitizeDetailImageModelPrompt,
splitDetailKeywords,
} from "@/components/canvas/chat-panel/canvas-detail-config";
import {
createDetailArchiveSnapshot,
patchDetailArchiveList,
prependDetailArchive,
replaceSavedDetailArchive,
sanitizeDetailArchiveList,
} from "@/components/canvas/chat-panel/canvas-detail-archive";
import {
applyDetailSlotAttachment,
fillDetailSlotAttachments,
getDetailSlotTargetIndexes,
} from "@/components/canvas/chat-panel/canvas-detail-slots";
import {
buildCanvasUploadedImageAttachment,
CANVAS_IMAGE_UPLOAD_MAX_BYTES,
getCanvasUploadImageTitle,
normalizeUploadedAssetId,
validateCanvasImageUploadFile,
} from "@/components/canvas/chat-panel/canvas-upload-helpers";
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
import type { ChatMessage } from "@/lib/types";
test.describe("canvas chat pure helpers", () => {
test("normalizes media urls without query strings or trailing slashes", () => {
expect(normalizeMediaUrl(" HTTPS://EXAMPLE.COM/assets/image.png?sig=secret#preview ")).toBe(
"https://example.com/assets/image.png"
);
expect(normalizeMediaUrl("not-a-url?cache=1#hash/")).toBe("not-a-url");
});
test("collects message image urls from parts, attachments, and legacy content", () => {
const message = {
id: "message-1",
role: "user",
parts: [
{
type: "file",
url: "https://cdn.example.com/upload-a.png?x=1",
mediaType: "image/png",
},
{
type: "text",
text: "参考 ![a](https://cdn.example.com/markdown-a.webp) 和 https://cdn.example.com/plain-a.jpg?token=1",
},
],
attachments: [
{
name: "attachment",
url: "https://cdn.example.com/attachment-a.jpeg?download=1",
contentType: "image/jpeg",
},
],
content: "legacy https://cdn.example.com/legacy-a.png",
} as unknown as ChatMessage;
expect(collectMessageImageUrls(message)).toEqual([
"https://cdn.example.com/upload-a.png?x=1",
"https://cdn.example.com/markdown-a.webp",
"https://cdn.example.com/plain-a.jpg?token=1",
"https://cdn.example.com/attachment-a.jpeg?download=1",
"https://cdn.example.com/legacy-a.png",
]);
});
test("filters assistant result images that match nearest user references", () => {
const referenceKeys = new Set(["https://cdn.example.com/reference.png"]);
const urls = extractAssistantImageUrls(
[
"![ref](https://cdn.example.com/reference.png?token=secret)",
"![result](https://cdn.example.com/result.png?token=secret)",
].join("\n"),
referenceKeys
);
expect(urls).toEqual(["https://cdn.example.com/result.png?token=secret"]);
});
test("extracts and strips video markup without removing surrounding text", () => {
const text = [
"生成完成",
'<video src="https://cdn.example.com/video-a.mp4?sig=1"></video>',
"[下载视频](https://cdn.example.com/video-b.mp4)",
"继续优化",
].join("\n");
expect(extractVideoUrls(text)).toEqual([
"https://cdn.example.com/video-a.mp4?sig=1",
"https://cdn.example.com/video-b.mp4",
]);
expect(stripMediaMarkup(text)).toBe("生成完成\n\n\n继续优化");
});
test("builds detail review plans with clamped count and localized fallbacks", () => {
const plans = buildDetailReviewPlans({
language: "zh",
count: 9,
summary: " 已验证卖点 ",
brief: {
productSpu: "",
demandType: "",
productName: "无线吸尘器",
targetAudience: "养宠家庭",
mainUser: "",
usageScenario: "客厅清洁",
triggerMoment: "",
coreNeed: "快速清理毛发",
riskConcern: "吸力不足",
expectedResult: "地面干净",
coreSellingPoint: "强吸力,轻量,低噪音",
positioningStatement: "",
userVoice: "",
competitorReference: "",
priceBand: "",
brandTone: "",
bannedWords: "",
},
});
expect(plans).toHaveLength(5);
expect(plans[0]).toMatchObject({
index: 1,
title: "真实需求反馈1",
});
expect(plans[0]?.reviewText).toContain("养宠家庭");
expect(plans[0]?.reviewText).toContain("已验证卖点");
expect(plans[0]?.imagePrompt).toContain("无线吸尘器");
});
test("keeps detail slot indexes and generation mode options stable", () => {
expect(DETAIL_TARGET_WIDTH).toBe(790);
expect(DETAIL_TARGET_HEIGHT).toBe(1300);
expect(DETAIL_SLOT_CONFIGS.map((slot) => slot.role)).toEqual([
"product",
"product",
"product",
"product",
"product",
"model",
"bundle",
"bundle",
"bundle",
]);
expect(DETAIL_PRODUCT_SLOT_INDEXES).toEqual([0, 1, 2, 3, 4]);
expect(DETAIL_MODEL_SLOT_INDEX).toBe(5);
expect(DETAIL_BUNDLE_SLOT_INDEXES).toEqual([6, 7, 8]);
expect(DETAIL_GENERATION_MODE_OPTIONS.map((option) => option.value)).toEqual([
"classic",
"agent_layout",
]);
});
test("normalizes detail config parser helpers defensively", () => {
expect(normalizeDetailGenerationMode("agent_layout")).toBe("agent_layout");
expect(normalizeDetailGenerationMode("unknown")).toBe("classic");
expect(extractDetailFrameCount("需要 23 张详情页")).toBe(20);
expect(extractDetailFrameCount("需要 2 屏")).toBe(5);
expect(extractDetailFrameCount("没有明确数量")).toBe(15);
expect(extractFirstNumber("标题 64px / 700", 32)).toBe(64);
expect(extractFirstNumber("无数字", 32)).toBe(32);
expect(extractFontWeight("主标题 64px / 700 / 行高1.15", "600")).toBe("700");
expect(extractFontWeight("weight: 500", "600")).toBe("500");
expect(extractFontWeight("无权重", "600")).toBe("600");
expect(splitDetailKeywords("强吸力,轻量; 低噪音 | 易清洁", 3)).toEqual([
"强吸力",
"轻量",
"低噪音",
]);
});
test("sanitizes detail image model prompts away from pixel-locked layout instructions", () => {
const sanitized = sanitizeDetailImageModelPrompt(
[
"字体规范:主标题 64px / 700 / 行高1.15",
"输出为竖版电商详情图,尺寸固定 790x1300 像素。",
"安全区 y=64~360,左右边距56px,模块间距24px。",
"商业摄影质感,留出清晰文案排版区,不要水印。",
"突出产品真实材质与使用场景。",
].join("\n")
);
expect(sanitized).not.toContain("790x1300");
expect(sanitized).not.toContain("64px");
expect(sanitized).not.toContain("56px");
expect(sanitized).toContain("输出为竖版电商详情图。");
expect(sanitized).toContain("顶部标题安全区");
expect(sanitized).toContain("左右留白充足");
expect(sanitized).toContain("突出产品真实材质与使用场景。");
});
test("builds stable detail result and appended review markdown", () => {
const markdown = buildDetailResultMarkdown({
generationMode: "agent_layout",
summary: "详情页生成成功",
images: [
{
originalUrl: "https://cdn.example.com/detail-1.png",
thumbnailUrl: "https://cdn.example.com/detail-1-thumb.webp",
},
],
prompts: ["商业摄影质感"],
skillFlow: {
name: "详情页生成",
version: "1.0",
steps: ["拆解卖点", "生成分镜"],
},
qualityChecks: {
passed: true,
},
screens: [
{
screenType: "A",
title: "核心卖点",
useModel: true,
fabe: {
b: "收益",
},
},
],
reviewGroups: [
{
reviewText: "真实好用",
images: [
{
originalUrl: "https://cdn.example.com/review-1.png",
thumbnailUrl: "https://cdn.example.com/review-1-thumb.webp",
},
],
},
],
});
expect(markdown).toContain("生成路线:AGENT排版(画面+可编辑文字层)");
expect(markdown).toContain("质检结果:通过");
expect(markdown).toContain("![thumb|||https://cdn.example.com/detail-1.png|||2K]");
expect(markdown).toContain("评价 1:真实好用");
expect(
buildDetailReviewAppendMarkdown({
failedCount: 1,
groups: [
{
reviewText: "追加评价",
images: [
{
originalUrl: "https://cdn.example.com/review-2.png",
thumbnailUrl: "https://cdn.example.com/review-2-thumb.webp",
},
],
},
],
})
).toContain("失败 1 条,已跳过。");
});
test("sanitizes and sorts detail archives from persisted payloads", () => {
const archives = sanitizeDetailArchiveList([
{
id: "older",
title: "",
prompt: " old prompt ",
generationMode: "invalid",
brief: {
productName: "旧产品",
targetAudience: "人群",
},
slots: [
{
url: "https://cdn.example.com/product.png",
name: "",
contentType: "",
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
uiSurfaceLatestReview: {
screenIndex: 2,
action: "unsafe",
passed: true,
issues: [" issue-a ", "", "issue-b"],
},
},
{
id: "newer",
title: "新项目",
prompt: "new prompt",
generationMode: "agent_layout",
brief: {
coreNeed: "核心需求",
},
slots: [],
createdAt: "2026-01-03T00:00:00.000Z",
},
{
id: "",
createdAt: "2026-01-04T00:00:00.000Z",
},
]);
expect(archives.map((archive) => archive.id)).toEqual(["newer", "older"]);
expect(archives[0]?.generationMode).toBe("agent_layout");
expect(archives[1]).toMatchObject({
title: "未命名详情项目",
generationMode: "classic",
brief: {
productName: "旧产品",
coreNeed: "",
},
});
expect(archives[1]?.slots[0]).toMatchObject({
url: "https://cdn.example.com/product.png",
name: "产品图1",
contentType: "image/jpeg",
});
expect(archives[1]?.uiSurfaceLatestReview).toMatchObject({
screenIndex: 2,
action: undefined,
passed: true,
issues: ["issue-a", "issue-b"],
});
});
test("creates and updates detail archive lists with stable limits", () => {
const archive = createDetailArchiveSnapshot({
archiveId: "draft",
createdAt: 1000,
prompt: " 第一行\n第二行 ",
generationMode: "classic",
summary: "摘要",
imageCount: 2,
coverUrl: "https://cdn.example.com/cover.png",
brief: {
productSpu: " SPU-001 ",
demandType: "",
productName: "产品名",
targetAudience: "",
mainUser: "",
usageScenario: " 客厅清洁 ",
triggerMoment: "",
coreNeed: "",
riskConcern: "",
expectedResult: "",
coreSellingPoint: "",
positioningStatement: "",
userVoice: "",
competitorReference: "",
priceBand: "",
brandTone: "",
bannedWords: "",
},
slots: [
{
name: "产品",
url: "https://cdn.example.com/product.png",
contentType: "image/png",
},
null,
],
});
expect(archive).toMatchObject({
id: "draft",
title: "SPU-001+客厅清洁",
prompt: "第一行 第二行",
createdAt: 1000,
updatedAt: 1000,
summary: "摘要",
imageCount: 2,
coverUrl: "https://cdn.example.com/cover.png",
});
expect(archive.slots[0]).toMatchObject({
name: "产品",
url: "https://cdn.example.com/product.png",
contentType: "image/png",
});
const patched = patchDetailArchiveList([archive], "draft", { summary: "新摘要" }, 2000);
expect(patched[0]).toMatchObject({ summary: "新摘要", updatedAt: 2000 });
const saved = { ...archive, id: "saved", updatedAt: 3000 };
expect(replaceSavedDetailArchive([archive], "draft", saved).map((item) => item.id)).toEqual([
"saved",
]);
const manyArchives = Array.from({ length: 35 }, (_, index) => ({
...archive,
id: `archive-${index}`,
createdAt: index,
updatedAt: index,
}));
expect(prependDetailArchive(manyArchives, saved)).toHaveLength(30);
});
test("fills detail slots by current role group without crossing groups", () => {
expect(getDetailSlotTargetIndexes(0)).toEqual([0, 1, 2, 3, 4]);
expect(getDetailSlotTargetIndexes(3)).toEqual([3, 4]);
expect(getDetailSlotTargetIndexes(5)).toEqual([5]);
expect(getDetailSlotTargetIndexes(7)).toEqual([7, 8]);
expect(getDetailSlotTargetIndexes(-1)).toEqual([]);
expect(getDetailSlotTargetIndexes(99)).toEqual([]);
const current = Array.from({ length: DETAIL_SLOT_CONFIGS.length }, () => null);
const productA: PickedObjectAttachment = {
name: "product-a",
url: "https://cdn.example.com/product-a.png",
contentType: "image/png",
};
const productB: PickedObjectAttachment = {
name: "product-b",
url: "https://cdn.example.com/product-b.png",
contentType: "image/png",
};
const updatedSingle = applyDetailSlotAttachment(current, 5, productA);
expect(updatedSingle[5]).toBe(productA);
expect(current[5]).toBeNull();
const filled = fillDetailSlotAttachments(current, getDetailSlotTargetIndexes(3), [
productA,
productB,
]);
expect(filled.filledCount).toBe(2);
expect(filled.slots[3]).toBe(productA);
expect(filled.slots[4]).toBe(productB);
expect(filled.slots[5]).toBeNull();
});
test("validates and normalizes canvas image upload helper payloads", () => {
expect(validateCanvasImageUploadFile(null)).toEqual({
ok: false,
message: "请选择图片文件",
});
expect(
validateCanvasImageUploadFile({
name: "demo.txt",
type: "text/plain",
size: 10,
})
).toEqual({
ok: false,
message: "请上传图片文件",
});
expect(
validateCanvasImageUploadFile({
name: "huge.png",
type: "image/png",
size: CANVAS_IMAGE_UPLOAD_MAX_BYTES + 1,
})
).toEqual({
ok: false,
message: "图片大小不能超过 50MB",
});
expect(
validateCanvasImageUploadFile({
name: "valid.webp",
type: "image/webp",
size: CANVAS_IMAGE_UPLOAD_MAX_BYTES,
})
).toEqual({ ok: true });
expect(getCanvasUploadImageTitle(" product.hero.final.png ")).toBe("product.hero.final");
expect(getCanvasUploadImageTitle(".png")).toBe("图片");
expect(normalizeUploadedAssetId(" asset-123 ")).toBe("asset-123");
expect(normalizeUploadedAssetId(" ")).toBeUndefined();
expect(
buildCanvasUploadedImageAttachment({
file: {
name: "photo.png",
type: "",
size: 100,
},
imageUrl: "https://cdn.example.com/photo.png",
assetId: "asset-1",
})
).toEqual({
name: "photo.png",
url: "https://cdn.example.com/photo.png",
contentType: "image/jpeg",
assetId: "asset-1",
});
});
});