- 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
102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import { AiEventRequestSchema } from "@/lib/types";
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// AiEventRequestSchema – behavioral validation tests
|
||
//
|
||
// The schema is the contract between the client and the API route.
|
||
// Tests verify what the schema ALLOWS and what it REJECTS.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const VALID_PNG =
|
||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
||
const VALID_JPEG =
|
||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AJQAB/9k=";
|
||
|
||
describe("AiEventRequestSchema – images array", () => {
|
||
test("accepts a prompt with no images", () => {
|
||
const result = AiEventRequestSchema.safeParse({ prompt: "Team standup every Monday at 9am" });
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
test("accepts images array with one valid base64 image", () => {
|
||
const result = AiEventRequestSchema.safeParse({
|
||
prompt: "What events are on this flyer?",
|
||
images: [VALID_PNG],
|
||
});
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
test("accepts images array with multiple valid base64 images", () => {
|
||
const result = AiEventRequestSchema.safeParse({
|
||
images: [VALID_PNG, VALID_JPEG],
|
||
});
|
||
expect(result.success).toBe(true);
|
||
if (result.success) {
|
||
expect(result.data.images).toHaveLength(2);
|
||
}
|
||
});
|
||
|
||
test("accepts request with images only (no prompt)", () => {
|
||
const result = AiEventRequestSchema.safeParse({ images: [VALID_PNG] });
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
test("rejects request with neither prompt nor images", () => {
|
||
const result = AiEventRequestSchema.safeParse({});
|
||
expect(result.success).toBe(false);
|
||
});
|
||
|
||
test("rejects images array containing an invalid data URL", () => {
|
||
const result = AiEventRequestSchema.safeParse({
|
||
images: ["not-a-data-url"],
|
||
});
|
||
expect(result.success).toBe(false);
|
||
});
|
||
|
||
test("rejects images array containing a non-image data URL (e.g. PDF)", () => {
|
||
const result = AiEventRequestSchema.safeParse({
|
||
images: ["data:application/pdf;base64,abc123"],
|
||
});
|
||
expect(result.success).toBe(false);
|
||
});
|
||
|
||
test("rejects empty images array (must have at least one image OR a prompt)", () => {
|
||
// An empty images array with no prompt should fail the 'prompt or images' refinement
|
||
const result = AiEventRequestSchema.safeParse({ images: [] });
|
||
expect(result.success).toBe(false);
|
||
});
|
||
|
||
test("returns data.images as string[] when images are valid", () => {
|
||
const result = AiEventRequestSchema.safeParse({
|
||
images: [VALID_PNG, VALID_JPEG],
|
||
});
|
||
expect(result.success).toBe(true);
|
||
if (result.success) {
|
||
expect(Array.isArray(result.data.images)).toBe(true);
|
||
for (const img of result.data.images ?? []) {
|
||
expect(typeof img).toBe("string");
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("AiEventRequestSchema – prompt validation", () => {
|
||
test("accepts a plain text prompt with no images", () => {
|
||
const result = AiEventRequestSchema.safeParse({ prompt: "Birthday party Saturday" });
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
test("rejects a prompt that exceeds 2000 characters", () => {
|
||
const longPrompt = "a".repeat(2001);
|
||
const result = AiEventRequestSchema.safeParse({ prompt: longPrompt });
|
||
expect(result.success).toBe(false);
|
||
});
|
||
|
||
test("accepts a prompt of exactly 2000 characters", () => {
|
||
const maxPrompt = "a".repeat(2000);
|
||
const result = AiEventRequestSchema.safeParse({ prompt: maxPrompt });
|
||
expect(result.success).toBe(true);
|
||
});
|
||
});
|