feat: support multiple image uploads for AI event generation 🖼️
- Updated OpenRouter integration to accept an array of image URLs - Updated ImagePicker to use the `multiple` attribute natively - Added `appendImagesDeduped` for handling client-side image deduplication - Enhanced clipboard pasting to extract multiple images at once - Rendered multiple images in a horizontal thumbnail strip in the AIToolbar - Added tests to cover multi-image logic and AI request mapping
This commit is contained in:
103
tests/multi-image.test.ts
Normal file
103
tests/multi-image.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
appendImagesDeduped,
|
||||
imageFileKey,
|
||||
} from "@/lib/multi-image";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multi-image helpers – behavioral tests
|
||||
//
|
||||
// These functions are pure; they own the "append + dedup" contract.
|
||||
// Tests describe what the system DOES, not how it's implemented.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("imageFileKey – stable identity for dedup", () => {
|
||||
test("returns a string combining name and size", () => {
|
||||
const file = new File(["hello"], "flyer.png", { type: "image/png" });
|
||||
const key = imageFileKey(file);
|
||||
expect(typeof key).toBe("string");
|
||||
expect(key).toContain("flyer.png");
|
||||
expect(key).toContain(String(file.size));
|
||||
});
|
||||
|
||||
test("two files with the same name and size produce the same key", () => {
|
||||
const a = new File(["hello"], "a.png", { type: "image/png" });
|
||||
const b = new File(["hello"], "a.png", { type: "image/png" });
|
||||
expect(imageFileKey(a)).toBe(imageFileKey(b));
|
||||
});
|
||||
|
||||
test("two files with the same name but different content produce different keys", () => {
|
||||
const a = new File(["hello"], "a.png", { type: "image/png" });
|
||||
const b = new File(["hello world"], "a.png", { type: "image/png" });
|
||||
expect(imageFileKey(a)).not.toBe(imageFileKey(b));
|
||||
});
|
||||
|
||||
test("two files with different names but same content produce different keys", () => {
|
||||
const a = new File(["hello"], "a.png", { type: "image/png" });
|
||||
const b = new File(["hello"], "b.png", { type: "image/png" });
|
||||
expect(imageFileKey(a)).not.toBe(imageFileKey(b));
|
||||
});
|
||||
});
|
||||
|
||||
describe("appendImagesDeduped – append with deduplication", () => {
|
||||
const makeFile = (name: string, content = "data") =>
|
||||
new File([content], name, { type: "image/png" });
|
||||
|
||||
test("appends new files to an empty list", () => {
|
||||
const result = appendImagesDeduped([], [makeFile("a.png")]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("a.png");
|
||||
});
|
||||
|
||||
test("appends new files to an existing list", () => {
|
||||
const existing = [makeFile("a.png")];
|
||||
const incoming = [makeFile("b.png")];
|
||||
const result = appendImagesDeduped(existing, incoming);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((f) => f.name)).toEqual(["a.png", "b.png"]);
|
||||
});
|
||||
|
||||
test("silently ignores incoming files that are exact duplicates (same name+size)", () => {
|
||||
const existing = [makeFile("a.png")];
|
||||
const incoming = [makeFile("a.png")]; // identical name + content = same size
|
||||
const result = appendImagesDeduped(existing, incoming);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("a.png");
|
||||
});
|
||||
|
||||
test("appends non-duplicate files even when some duplicates are in the batch", () => {
|
||||
const existing = [makeFile("a.png")];
|
||||
const incoming = [makeFile("a.png"), makeFile("b.png")];
|
||||
const result = appendImagesDeduped(existing, incoming);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((f) => f.name)).toEqual(["a.png", "b.png"]);
|
||||
});
|
||||
|
||||
test("preserves existing list order — new files are appended at the end", () => {
|
||||
const existing = [makeFile("first.png"), makeFile("second.png")];
|
||||
const incoming = [makeFile("third.png")];
|
||||
const result = appendImagesDeduped(existing, incoming);
|
||||
expect(result.map((f) => f.name)).toEqual([
|
||||
"first.png",
|
||||
"second.png",
|
||||
"third.png",
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns a new array (does not mutate the existing list)", () => {
|
||||
const existing = [makeFile("a.png")];
|
||||
const incoming = [makeFile("b.png")];
|
||||
const result = appendImagesDeduped(existing, incoming);
|
||||
expect(result).not.toBe(existing); // new reference
|
||||
expect(existing).toHaveLength(1); // original untouched
|
||||
});
|
||||
|
||||
test("handles multiple incoming files with internal duplicates", () => {
|
||||
// Two identical files in the same incoming batch
|
||||
const existing: File[] = [];
|
||||
const incoming = [makeFile("a.png"), makeFile("a.png"), makeFile("b.png")];
|
||||
const result = appendImagesDeduped(existing, incoming);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((f) => f.name)).toEqual(["a.png", "b.png"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user