diff --git a/packages/kilo-vscode/tests/unit/image-attachments-utils.test.ts b/packages/kilo-vscode/tests/unit/image-attachments-utils.test.ts new file mode 100644 index 0000000000..c5b50bbaeb --- /dev/null +++ b/packages/kilo-vscode/tests/unit/image-attachments-utils.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "bun:test" +import { + ACCEPTED_IMAGE_TYPES, + isAcceptedImageType, + isDragLeavingComponent, +} from "../../webview-ui/src/hooks/image-attachments-utils" + +describe("ACCEPTED_IMAGE_TYPES", () => { + it("includes the standard image MIME types", () => { + expect(ACCEPTED_IMAGE_TYPES).toContain("image/png") + expect(ACCEPTED_IMAGE_TYPES).toContain("image/jpeg") + expect(ACCEPTED_IMAGE_TYPES).toContain("image/gif") + expect(ACCEPTED_IMAGE_TYPES).toContain("image/webp") + }) +}) + +describe("isAcceptedImageType", () => { + it("returns true for accepted types", () => { + expect(isAcceptedImageType("image/png")).toBe(true) + expect(isAcceptedImageType("image/jpeg")).toBe(true) + expect(isAcceptedImageType("image/gif")).toBe(true) + expect(isAcceptedImageType("image/webp")).toBe(true) + }) + + it("returns false for non-image types", () => { + expect(isAcceptedImageType("application/pdf")).toBe(false) + expect(isAcceptedImageType("text/plain")).toBe(false) + expect(isAcceptedImageType("video/mp4")).toBe(false) + }) + + it("returns false for empty string", () => { + expect(isAcceptedImageType("")).toBe(false) + }) + + it("returns false for image types not in the accepted list", () => { + expect(isAcceptedImageType("image/svg+xml")).toBe(false) + expect(isAcceptedImageType("image/bmp")).toBe(false) + }) +}) + +describe("isDragLeavingComponent", () => { + it("returns true when relatedTarget is null (left the page)", () => { + const el = { contains: () => false } as unknown as HTMLElement + expect(isDragLeavingComponent(null, el)).toBe(true) + }) + + it("returns false when relatedTarget is a child (contains returns true)", () => { + const child = {} as EventTarget + const parent = { contains: (n: Node) => n === child } as unknown as HTMLElement + expect(isDragLeavingComponent(child, parent)).toBe(false) + }) + + it("returns true when relatedTarget is outside (contains returns false)", () => { + const outside = {} as EventTarget + const container = { contains: () => false } as unknown as HTMLElement + expect(isDragLeavingComponent(outside, container)).toBe(true) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/hooks/image-attachments-utils.ts b/packages/kilo-vscode/webview-ui/src/hooks/image-attachments-utils.ts new file mode 100644 index 0000000000..39bd74e01f --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/hooks/image-attachments-utils.ts @@ -0,0 +1,15 @@ +export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] + +/** Returns true if the given MIME type is an accepted image type. */ +export function isAcceptedImageType(mimeType: string): boolean { + return ACCEPTED_IMAGE_TYPES.includes(mimeType) +} + +/** + * Check if a drag-leave event is leaving the component (not just entering a child). + * Returns true if dragging has actually left the component boundary. + */ +export function isDragLeavingComponent(relatedTarget: EventTarget | null, currentTarget: HTMLElement): boolean { + if (!relatedTarget) return true + return !currentTarget.contains(relatedTarget as Node) +} diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts b/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts index 7853908337..f9f128cd35 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts @@ -1,6 +1,7 @@ import { createSignal } from "solid-js" +import { ACCEPTED_IMAGE_TYPES, isAcceptedImageType, isDragLeavingComponent } from "./image-attachments-utils" -export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] +export { ACCEPTED_IMAGE_TYPES } export interface ImageAttachment { id: string @@ -14,7 +15,7 @@ export function useImageAttachments() { const [dragging, setDragging] = createSignal(false) const add = (file: File) => { - if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) return + if (!isAcceptedImageType(file.type)) return const reader = new FileReader() reader.onload = () => { const attachment: ImageAttachment = { @@ -53,7 +54,7 @@ export function useImageAttachments() { } const handleDragLeave = (event: DragEvent) => { - if (!event.relatedTarget || !(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node)) { + if (isDragLeavingComponent(event.relatedTarget, event.currentTarget as HTMLElement)) { setDragging(false) } }