test: add unit tests for extractImageFromClipboard
This commit is contained in:
168
tests/clipboard-image.test.ts
Normal file
168
tests/clipboard-image.test.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user