Files
ComfyUI_frontend/src/utils/__tests__/eventUtils.test.ts
Brian Jemilo II a80f6d7922 Batch Drag & Drop Images (#8282)
## Summary

<!-- One sentence describing what changed and why. -->
Added feature to drag and drop multiple images into the UI and connect
them with a Batch Images node with tests to add convenience for users.
Only works with a group of images, mixing files not supported.

## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
I've updated our usage of Litegraph.createNode, honestly, that method is
pretty bad, onNodeCreated option method doesn't even return the node
created. I think I will probably go check out their repo to do a PR over
there. Anyways, I made a createNode method to avoid race conditions when
creating nodes for the paste actions. Will allow us to better
programmatically create nodes that do not have workflows that also need
to be connected to other nodes.

<!-- If this PR fixes an issue, uncomment and update the line below -->

https://www.notion.so/comfy-org/Implement-Multi-image-drag-and-drop-to-canvas-2eb6d73d36508195ad8addfc4367db10

## Screenshots (if applicable)

https://github.com/user-attachments/assets/d4155807-56e2-4e39-8ab1-16eda90f6a53

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8282-Batch-Drag-Drop-Images-2f16d73d365081c1ab31ce9da47a7be5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Austin Mroz <austin@comfy.org>
2026-02-11 17:39:41 -08:00

108 lines
3.3 KiB
TypeScript

import { extractFileFromDragEvent } from '@/utils/eventUtils'
import { describe, expect, it } from 'vitest'
describe('eventUtils', () => {
describe('extractFileFromDragEvent', () => {
it('should handle drops with no data', async () => {
const actual = await extractFileFromDragEvent(new FakeDragEvent('drop'))
expect(actual).toBe(undefined)
})
it('should handle drops with dataTransfer but no files', async () => {
const actual = await extractFileFromDragEvent(
new FakeDragEvent('drop', { dataTransfer: new DataTransfer() })
)
expect(actual).toBe(undefined)
})
it('should handle drops with dataTransfer with files', async () => {
const fileWithWorkflowMaybeWhoKnows = new File(
[new Uint8Array()],
'fake_workflow.json',
{
type: 'application/json'
}
)
const dataTransfer = new DataTransfer()
dataTransfer.items.add(fileWithWorkflowMaybeWhoKnows)
const event = new FakeDragEvent('drop', { dataTransfer })
const actual = await extractFileFromDragEvent(event)
expect(actual).toBe(fileWithWorkflowMaybeWhoKnows)
})
it('should handle drops with multiple image files', async () => {
const imageFile1 = new File([new Uint8Array()], 'image1.png', {
type: 'image/png'
})
const imageFile2 = new File([new Uint8Array()], 'image2.jpg', {
type: 'image/jpeg'
})
const dataTransfer = new DataTransfer()
dataTransfer.items.add(imageFile1)
dataTransfer.items.add(imageFile2)
const event = new FakeDragEvent('drop', { dataTransfer })
const actual = await extractFileFromDragEvent(event)
expect(actual).toBeDefined()
expect((actual as FileList).length).toBe(2)
expect((actual as FileList)[0]).toBe(imageFile1)
expect((actual as FileList)[1]).toBe(imageFile2)
})
it('should return undefined when dropping multiple non-image files', async () => {
const file1 = new File([new Uint8Array()], 'file1.txt', {
type: 'text/plain'
})
const file2 = new File([new Uint8Array()], 'file2.txt', {
type: 'text/plain'
})
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file1)
dataTransfer.items.add(file2)
const event = new FakeDragEvent('drop', { dataTransfer })
const actual = await extractFileFromDragEvent(event)
expect(actual).toBe(undefined)
})
// Skip until we can setup MSW
it.skip('should handle drops with URLs', async () => {
const urlWithWorkflow = 'https://fakewebsite.notreal/fake_workflow.json'
const dataTransfer = new DataTransfer()
dataTransfer.setData('text/uri-list', urlWithWorkflow)
dataTransfer.setData('text/x-moz-url', urlWithWorkflow)
const event = new FakeDragEvent('drop', { dataTransfer })
const actual = await extractFileFromDragEvent(event)
expect(actual).toBeInstanceOf(File)
})
})
})
// Needed to keep the dataTransfer defined
class FakeDragEvent extends DragEvent {
override dataTransfer: DataTransfer | null
override clientX: number
override clientY: number
constructor(
type: string,
{ dataTransfer, clientX, clientY }: DragEventInit = {}
) {
super(type)
this.dataTransfer = dataTransfer ?? null
this.clientX = clientX ?? 0
this.clientY = clientY ?? 0
}
}