import { describe, expect, test } from "bun:test"; import { extractImageFromClipboard } from "@/lib/clipboard-image"; // --------------------------------------------------------------------------- // Clipboard image extraction – pure function tests // // Public interface under test: extractImageFromClipboard(clipboardData) // Takes a DataTransfer (or null) and returns the first image File, or null. // // Two resolution paths (in priority order): // 1. clipboardData.files – browser-normalised FileList, most reliable // 2. clipboardData.items – DataTransferItemList fallback // // MIME matching uses startsWith("image/") to handle OS-specific variants // like "image/x-png" that a strict allowlist would miss. // --------------------------------------------------------------------------- // ─── Fakes ──────────────────────────────────────────────────────────────────── function makeItem(type: string, file: File | null = null): DataTransferItem { return { kind: file ? "file" : "string", type, getAsFile: () => file, getAsString: () => {}, webkitGetAsEntry: () => null, } as unknown as DataTransferItem; } function makeItemList(...items: DataTransferItem[]): DataTransferItemList { return { length: items.length, ...Object.fromEntries(items.map((item, i) => [i, item])), [Symbol.iterator]: function* () { yield* items; }, } as unknown as DataTransferItemList; } function makeFileList(...files: File[]): FileList { return { length: files.length, ...Object.fromEntries(files.map((f, i) => [i, f])), item: (i: number) => files[i] ?? null, [Symbol.iterator]: function* () { yield* files; }, } as unknown as FileList; } function makeDataTransfer({ files = [] as File[], items = [] as DataTransferItem[], } = {}): DataTransfer { return { files: makeFileList(...files), items: makeItemList(...items), } as unknown as DataTransfer; } const PNG_FILE = new File(["png"], "image.png", { type: "image/png" }); const JPEG_FILE = new File(["jpg"], "photo.jpg", { type: "image/jpeg" }); const WEBP_FILE = new File(["webp"], "img.webp", { type: "image/webp" }); const GIF_FILE = new File(["gif"], "anim.gif", { type: "image/gif" }); // ─── Path 1: files array (primary) ─────────────────────────────────────────── describe("extractImageFromClipboard – files array (primary path)", () => { test("returns PNG from files array", () => { const dt = makeDataTransfer({ files: [PNG_FILE] }); expect(extractImageFromClipboard(dt)).toBe(PNG_FILE); }); test("returns JPEG from files array", () => { const dt = makeDataTransfer({ files: [JPEG_FILE] }); expect(extractImageFromClipboard(dt)).toBe(JPEG_FILE); }); test("returns WebP from files array", () => { const dt = makeDataTransfer({ files: [WEBP_FILE] }); expect(extractImageFromClipboard(dt)).toBe(WEBP_FILE); }); test("returns first image when files array has multiple images", () => { const dt = makeDataTransfer({ files: [PNG_FILE, JPEG_FILE] }); expect(extractImageFromClipboard(dt)).toBe(PNG_FILE); }); test("prefers files array over items when both present", () => { const dt = makeDataTransfer({ files: [JPEG_FILE], items: [makeItem("image/png", PNG_FILE)], }); // files is primary — JPEG should win expect(extractImageFromClipboard(dt)).toBe(JPEG_FILE); }); }); // ─── Path 2: items fallback ─────────────────────────────────────────────────── describe("extractImageFromClipboard – items fallback", () => { test("returns PNG from items when files array is empty", () => { const dt = makeDataTransfer({ items: [makeItem("image/png", PNG_FILE)] }); expect(extractImageFromClipboard(dt)).toBe(PNG_FILE); }); test("returns JPEG from items when files array is empty", () => { const dt = makeDataTransfer({ items: [makeItem("image/jpeg", JPEG_FILE)] }); expect(extractImageFromClipboard(dt)).toBe(JPEG_FILE); }); test("skips text items, returns image from items", () => { const dt = makeDataTransfer({ items: [makeItem("text/plain", null), makeItem("image/png", PNG_FILE)], }); expect(extractImageFromClipboard(dt)).toBe(PNG_FILE); }); test("returns null when getAsFile() returns null", () => { const dt = makeDataTransfer({ items: [makeItem("image/png", null)] }); expect(extractImageFromClipboard(dt)).toBeNull(); }); }); // ─── startsWith("image/") broadening ───────────────────────────────────────── describe("extractImageFromClipboard – broad image/* MIME matching", () => { test("accepts image/gif (any image/* accepted now)", () => { const dt = makeDataTransfer({ files: [GIF_FILE] }); expect(extractImageFromClipboard(dt)).toBe(GIF_FILE); }); test("accepts image/x-png (Linux-specific variant)", () => { const xpng = new File(["x"], "x.png", { type: "image/x-png" }); const dt = makeDataTransfer({ files: [xpng] }); expect(extractImageFromClipboard(dt)).toBe(xpng); }); test("accepts image/bmp", () => { const bmp = new File(["b"], "b.bmp", { type: "image/bmp" }); const dt = makeDataTransfer({ files: [bmp] }); expect(extractImageFromClipboard(dt)).toBe(bmp); }); test("rejects text/plain even if misidentified", () => { const fake = new File(["t"], "t.txt", { type: "text/plain" }); const dt = makeDataTransfer({ files: [fake] }); expect(extractImageFromClipboard(dt)).toBeNull(); }); }); // ─── Null / empty cases ─────────────────────────────────────────────────────── describe("extractImageFromClipboard – null / empty", () => { test("returns null when clipboardData is null", () => { expect(extractImageFromClipboard(null)).toBeNull(); }); test("returns null when clipboardData is undefined", () => { expect(extractImageFromClipboard(undefined)).toBeNull(); }); test("returns null when both files and items are empty", () => { const dt = makeDataTransfer(); expect(extractImageFromClipboard(dt)).toBeNull(); }); test("returns null for text-only clipboard", () => { const dt = makeDataTransfer({ items: [makeItem("text/plain", null)] }); expect(extractImageFromClipboard(dt)).toBeNull(); }); });