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 = [ "生成完成", '', "[下载视频](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", }); }); });