Files
local-cal/tests/clipboard-image.test.ts

169 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
});
});