From d850d88a3aed4aa59ba6c9044677fe8062a43f1f Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Wed, 8 Apr 2026 19:58:03 -0400 Subject: [PATCH] test: add unit tests for extractImageFromClipboard --- tests/clipboard-image.test.ts | 168 ++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/clipboard-image.test.ts diff --git a/tests/clipboard-image.test.ts b/tests/clipboard-image.test.ts new file mode 100644 index 0000000..aabbfd9 --- /dev/null +++ b/tests/clipboard-image.test.ts @@ -0,0 +1,168 @@ +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(); + }); +});