import { describe, expect, it } from "vitest"; import { clipboardImageFiles, clipboardImageURL, insertClipboardImageReferences } from "@/lib/client/clipboard-images"; function clipboard(items: Array<{ kind: string; type: string; getAsFile: () => File | null }>, files: File[] = []) { return { items, files } as unknown as Pick; } function item(file: File | null, type = file?.type || "image/png") { return { kind: "file", type, getAsFile: () => file }; } describe("clipboardImageFiles", () => { it("accepts image items and avoids their duplicate files representation", () => { const png = new File(["png bytes"], "image", { type: "image/png" }); const jpeg = new File(["jpeg bytes"], "photo.jpeg", { type: "image/jpeg" }); const result = clipboardImageFiles(clipboard([item(png), item(jpeg)], [png, jpeg])); expect(result).toHaveLength(2); expect(result.map((file) => file.name)).toEqual(["pasted-image.png", "photo.jpeg"]); expect(result[1]).toBe(jpeg); expect(result[0].type).toBe("image/png"); }); it("falls back to files when clipboard items do not expose an image", () => { const webp = new File(["webp bytes"], "screenshot", { type: "image/webp" }); const text = new File(["https://example.test/image.png"], "link.txt", { type: "text/plain" }); const result = clipboardImageFiles(clipboard([item(null), { kind: "string", type: "text/plain", getAsFile: () => null }], [text, webp])); expect(result.map((file) => file.name)).toEqual(["screenshot.webp"]); }); it("ignores text, HTML, URLs, null files and non-image files", () => { const text = new File(["image URL"], "photo.png", { type: "text/plain" }); expect(clipboardImageFiles(clipboard([ { kind: "string", type: "text/html", getAsFile: () => null }, { kind: "string", type: "text/uri-list", getAsFile: () => null }, item(null), item(text) ], [text]))).toEqual([]); expect(clipboardImageFiles(null)).toEqual([]); }); it("normalizes generic screenshot names but keeps usable names and image bytes", async () => { const original = new File([Uint8Array.from([1, 2, 3, 4])], "capture.image", { type: "image/gif" }); const good = new File(["jpeg"], "holiday.jpg", { type: "image/jpeg" }); const [renamed, preserved] = clipboardImageFiles(clipboard([item(original), item(good)])); expect(renamed.name).toBe("capture.gif"); expect(new Uint8Array(await renamed.arrayBuffer())).toEqual(Uint8Array.from([1, 2, 3, 4])); expect(preserved).toBe(good); }); it("fills a missing File MIME from the image clipboard item even when its filename is usable", async () => { const original = new File([Uint8Array.from([7, 8, 9])], "screenshot.png"); const [file] = clipboardImageFiles(clipboard([item(original, "image/png")])); expect(file.name).toBe("screenshot.png"); expect(file.type).toBe("image/png"); expect(new Uint8Array(await file.arrayBuffer())).toEqual(Uint8Array.from([7, 8, 9])); }); }); describe("insertClipboardImageReferences", () => { it("replaces a selected range when the prompt has not changed", () => { expect(insertClipboardImageReferences("请修改这里的衣服", ["@图片1"], { text: "请修改这里的衣服", start: 3, end: 5 })).toBe("请修改 @图片1 的衣服"); expect(insertClipboardImageReferences("请修改 衣服", ["图片2"], { text: "请修改 衣服", start: 4, end: 5 })).toBe("请修改 @图片2 衣服"); }); it("appends to the latest prompt when the user edited it during upload", () => { expect(insertClipboardImageReferences("原提示词又加了内容", ["@图片3"], { text: "原提示词", start: 0, end: 4 })).toBe("原提示词又加了内容 @图片3"); }); it("deduplicates exact references without confusing image 1 and image 10", () => { expect(insertClipboardImageReferences("看 @图片10", ["@图片1", "@图片1", "@图片10"])) .toBe("看 @图片10 @图片1"); expect(insertClipboardImageReferences("看 @图片1", ["@图片1"])) .toBe("看 @图片1"); }); it("does not erase selection text if all requested references already exist", () => { expect(insertClipboardImageReferences("替换这里 @图片1", ["@图片1"], { text: "替换这里 @图片1", start: 2, end: 4 })).toBe("替换这里 @图片1"); }); }); describe("clipboardImageURL", () => { const asClipboard = (plain: string, html = "") => ({ getData: (type: string) => type === "text/plain" ? plain : html }) as Pick; it("preserves a single signed URL exactly after trimming, regardless of suffix", () => { const signed = "https://images.example.test/asset.image?Expires=123&Signature=A%2FB%3D&key=abc"; expect(clipboardImageURL(asClipboard(` ${signed} `))).toBe(signed); expect(clipboardImageURL(asClipboard("http://images.example.test/opaque?id=1"))) .toBe("http://images.example.test/opaque?id=1"); }); it("does not extract URLs from prose or accept more than one URL", () => { expect(clipboardImageURL(asClipboard("请看 https://images.example.test/a.png"))).toBeNull(); expect(clipboardImageURL(asClipboard("https://images.example.test/a.png\nhttps://images.example.test/b.png"))).toBeNull(); expect(clipboardImageURL(asClipboard("https://images.example.test/a.png more"))).toBeNull(); }); it("ignores HTML and rejects non-HTTP schemes and userinfo", () => { expect(clipboardImageURL(asClipboard("", ''))).toBeNull(); for (const value of [ "data:image/png;base64,AAAA", "file:///tmp/a.png", "javascript:alert(1)", "https://user:pass@images.example.test/a.png", "https://@images.example.test/a.png", "images.example.test/a.png" ]) expect(clipboardImageURL(asClipboard(value))).toBeNull(); expect(clipboardImageURL(null)).toBeNull(); }); it("rejects oversized URLs and embedded whitespace", () => { expect(clipboardImageURL(asClipboard(`https://images.example.test/${"a".repeat(8192)}`))).toBeNull(); expect(clipboardImageURL(asClipboard("https://images.example.test/a\tb"))).toBeNull(); }); });