127 lines
6.1 KiB
TypeScript
127 lines
6.1 KiB
TypeScript
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<DataTransfer, "items" | "files">;
|
|
}
|
|
|
|
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<DataTransfer, "getData">;
|
|
|
|
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("", '<img src="https://images.example.test/a.png">'))).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();
|
|
});
|
|
});
|