Files
tolaria/src/hooks/useImageDrop.test.ts
lucaronin fafa876a4d fix: remove duplicate image upload in useImageDrop, fix build errors
The useImageDrop hook's handleDrop was uploading files and inserting
image blocks, but BlockNote already has a native dropFile ProseMirror
plugin that does the same via editor.uploadFile — causing duplicate
uploads and image block insertions on every drop.

Fix: handleDrop now only resets the visual overlay state. BlockNote's
native handler handles the actual upload (which calls uploadImageFile)
and block insertion with proper drop-position support.

Also fix build: exclude test files from tsconfig.app.json (test files
use vi.Mock types from vitest, not available in the app build config).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:09:58 +01:00

164 lines
5.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { uploadImageFile, useImageDrop } from './useImageDrop'
import { createRef } from 'react'
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
convertFileSrc: vi.fn((path: string) => `asset://localhost/${path}`),
}))
vi.mock('../mock-tauri', () => ({
isTauri: () => false,
}))
// JSDOM lacks DragEvent and File.arrayBuffer — polyfill for tests
beforeAll(() => {
if (typeof globalThis.DragEvent === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).DragEvent = class DragEvent extends MouseEvent {
dataTransfer: DataTransfer | null
constructor(type: string, init?: DragEventInit) {
super(type, init)
this.dataTransfer = init?.dataTransfer ?? null
}
}
}
// File.prototype.arrayBuffer may be missing in older JSDOM
if (!File.prototype.arrayBuffer) {
File.prototype.arrayBuffer = function () {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.readAsArrayBuffer(this)
})
}
}
})
// Mock DataTransfer (JSDOM doesn't implement it)
function createMockDataTransfer(files: File[]) {
const items = files.map(f => ({ kind: 'file' as const, type: f.type, getAsFile: () => f }))
return {
items: { ...items, length: items.length },
files: Object.assign(files, { item: (i: number) => files[i] }),
dropEffect: 'none',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any as DataTransfer
}
function createDragEvent(type: string, files: File[], opts?: { relatedTarget?: EventTarget | null }) {
const dt = createMockDataTransfer(files)
return new DragEvent(type, {
dataTransfer: dt,
bubbles: true,
cancelable: true,
relatedTarget: opts?.relatedTarget ?? null,
})
}
describe('uploadImageFile', () => {
it('returns a data URL in browser mode', async () => {
const blob = new Blob(['fake-image-data'], { type: 'image/png' })
const file = new File([blob], 'test.png', { type: 'image/png' })
const url = await uploadImageFile(file)
expect(url).toMatch(/^data:image\/png;base64,/)
})
it('passes file to Tauri save_image in Tauri mode', async () => {
const mockTauri = await import('../mock-tauri')
const originalIsTauri = mockTauri.isTauri
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(mockTauri as any).isTauri = () => true
const { invoke, convertFileSrc } = await import('@tauri-apps/api/core')
vi.mocked(invoke).mockResolvedValue('/vault/attachments/123-test.png')
vi.mocked(convertFileSrc).mockReturnValue('asset://localhost/vault/attachments/123-test.png')
const blob = new Blob([new Uint8Array([0x89, 0x50])], { type: 'image/png' })
const file = new File([blob], 'test.png', { type: 'image/png' })
const url = await uploadImageFile(file, '/vault')
expect(invoke).toHaveBeenCalledWith('save_image', {
vaultPath: '/vault',
filename: 'test.png',
data: expect.any(String),
})
expect(url).toBe('asset://localhost/vault/attachments/123-test.png')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(mockTauri as any).isTauri = originalIsTauri
})
})
describe('useImageDrop', () => {
let container: HTMLDivElement
beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
})
afterEach(() => {
container.remove()
})
function renderImageDrop() {
const ref = createRef<HTMLDivElement>()
Object.defineProperty(ref, 'current', { value: container, writable: true })
return renderHook(() => useImageDrop({ containerRef: ref }))
}
it('sets isDragOver to true on dragover with image files', () => {
const { result } = renderImageDrop()
const file = new File(['data'], 'photo.png', { type: 'image/png' })
act(() => { container.dispatchEvent(createDragEvent('dragover', [file])) })
expect(result.current.isDragOver).toBe(true)
})
it('ignores dragover with non-image files', () => {
const { result } = renderImageDrop()
const file = new File(['data'], 'doc.pdf', { type: 'application/pdf' })
act(() => { container.dispatchEvent(createDragEvent('dragover', [file])) })
expect(result.current.isDragOver).toBe(false)
})
it('resets isDragOver on dragleave when leaving container', () => {
const { result } = renderImageDrop()
const file = new File(['data'], 'photo.png', { type: 'image/png' })
act(() => { container.dispatchEvent(createDragEvent('dragover', [file])) })
expect(result.current.isDragOver).toBe(true)
act(() => { container.dispatchEvent(createDragEvent('dragleave', [], { relatedTarget: document.body })) })
expect(result.current.isDragOver).toBe(false)
})
it('resets isDragOver on drop (upload handled by BlockNote natively)', () => {
const { result } = renderImageDrop()
const file = new File(['data'], 'photo.png', { type: 'image/png' })
act(() => { container.dispatchEvent(createDragEvent('dragover', [file])) })
expect(result.current.isDragOver).toBe(true)
act(() => { container.dispatchEvent(createDragEvent('drop', [file])) })
expect(result.current.isDragOver).toBe(false)
})
it('accepts jpeg, gif, and webp types', () => {
const { result } = renderImageDrop()
for (const type of ['image/jpeg', 'image/gif', 'image/webp']) {
const file = new File(['data'], `img.${type.split('/')[1]}`, { type })
act(() => { container.dispatchEvent(createDragEvent('dragover', [file])) })
expect(result.current.isDragOver).toBe(true)
act(() => { container.dispatchEvent(createDragEvent('dragleave', [], { relatedTarget: document.body })) })
}
})
})