mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-02 19:49:58 +00:00
## Summary <!-- One sentence describing what changed and why. --> Added feature to drag image into workflow to create a load image node if the image does not have workflow meta data. Also added tests for usePaste.ts as I extracted code to be reusable there and there wasn't any tests. ## Changes - **What**: <!-- Core functionality added/modified --> app.ts handleFile updated, usePaste.ts usePaste updated with new method pasteImageNode ## Review Focus <!-- Fixes #ISSUE_NUMBER --> Not sure if it has an issue, just has a notion task. https://www.notion.so/comfy-org/Drag-in-an-image-that-s-not-a-workflow-and-being-able-to-directly-loading-it-as-Load-Image-2156d73d365080c4851ffc1425e06caf ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> https://github.com/user-attachments/assets/0403e4f1-2a99-4939-bf01-3d9e8f9834bb ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7898-Drag-image-to-load-image-2e26d73d36508187abdff986e8087370) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
315 lines
8.5 KiB
TypeScript
315 lines
8.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type {
|
|
LGraphCanvas,
|
|
LGraph,
|
|
LGraphGroup,
|
|
LGraphNode
|
|
} from '@/lib/litegraph/src/litegraph'
|
|
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
|
import { app } from '@/scripts/app'
|
|
import { isImageNode } from '@/utils/litegraphUtil'
|
|
import { pasteImageNode, usePaste } from './usePaste'
|
|
|
|
function createMockNode() {
|
|
return {
|
|
pos: [0, 0],
|
|
pasteFile: vi.fn(),
|
|
pasteFiles: vi.fn()
|
|
}
|
|
}
|
|
|
|
function createImageFile(
|
|
name: string = 'test.png',
|
|
type: string = 'image/png'
|
|
): File {
|
|
return new File([''], name, { type })
|
|
}
|
|
|
|
function createAudioFile(
|
|
name: string = 'test.mp3',
|
|
type: string = 'audio/mpeg'
|
|
): File {
|
|
return new File([''], name, { type })
|
|
}
|
|
|
|
function createDataTransfer(files: File[] = []): DataTransfer {
|
|
const dataTransfer = new DataTransfer()
|
|
files.forEach((file) => dataTransfer.items.add(file))
|
|
return dataTransfer
|
|
}
|
|
|
|
const mockCanvas = {
|
|
current_node: null as LGraphNode | null,
|
|
graph: {
|
|
add: vi.fn(),
|
|
change: vi.fn()
|
|
} as Partial<LGraph> as LGraph,
|
|
graph_mouse: [100, 200],
|
|
pasteFromClipboard: vi.fn(),
|
|
_deserializeItems: vi.fn()
|
|
} as Partial<LGraphCanvas> as LGraphCanvas
|
|
|
|
const mockCanvasStore = {
|
|
canvas: mockCanvas,
|
|
getCanvas: vi.fn(() => mockCanvas)
|
|
}
|
|
|
|
const mockWorkspaceStore = {
|
|
shiftDown: false
|
|
}
|
|
|
|
vi.mock('@vueuse/core', () => ({
|
|
useEventListener: vi.fn((target, event, handler) => {
|
|
target.addEventListener(event, handler)
|
|
return () => target.removeEventListener(event, handler)
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
|
useCanvasStore: () => mockCanvasStore
|
|
}))
|
|
|
|
vi.mock('@/stores/workspaceStore', () => ({
|
|
useWorkspaceStore: () => mockWorkspaceStore
|
|
}))
|
|
|
|
vi.mock('@/scripts/app', () => ({
|
|
app: {
|
|
loadGraphData: vi.fn()
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/lib/litegraph/src/litegraph', () => ({
|
|
LiteGraph: {
|
|
createNode: vi.fn()
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/utils/litegraphUtil', () => ({
|
|
isAudioNode: vi.fn(),
|
|
isImageNode: vi.fn(),
|
|
isVideoNode: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/workbench/eventHelpers', () => ({
|
|
shouldIgnoreCopyPaste: vi.fn()
|
|
}))
|
|
|
|
describe('pasteImageNode', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.mocked(mockCanvas.graph!.add).mockImplementation(
|
|
(node: LGraphNode | LGraphGroup) => node as LGraphNode
|
|
)
|
|
})
|
|
|
|
it('should create new LoadImage node when no image node provided', () => {
|
|
const mockNode = createMockNode()
|
|
vi.mocked(LiteGraph.createNode).mockReturnValue(
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
const file = createImageFile()
|
|
const dataTransfer = createDataTransfer([file])
|
|
|
|
pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items)
|
|
|
|
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage')
|
|
expect(mockNode.pos).toEqual([100, 200])
|
|
expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode)
|
|
expect(mockCanvas.graph!.change).toHaveBeenCalled()
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
|
})
|
|
|
|
it('should use existing image node when provided', () => {
|
|
const mockNode = createMockNode()
|
|
const file = createImageFile()
|
|
const dataTransfer = createDataTransfer([file])
|
|
|
|
pasteImageNode(
|
|
mockCanvas as unknown as LGraphCanvas,
|
|
dataTransfer.items,
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
|
expect(mockNode.pasteFiles).toHaveBeenCalledWith([file])
|
|
})
|
|
|
|
it('should handle multiple image files', () => {
|
|
const mockNode = createMockNode()
|
|
const file1 = createImageFile('test1.png')
|
|
const file2 = createImageFile('test2.jpg', 'image/jpeg')
|
|
const dataTransfer = createDataTransfer([file1, file2])
|
|
|
|
pasteImageNode(
|
|
mockCanvas as unknown as LGraphCanvas,
|
|
dataTransfer.items,
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(file1)
|
|
expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2])
|
|
})
|
|
|
|
it('should do nothing when no image files present', () => {
|
|
const mockNode = createMockNode()
|
|
const dataTransfer = createDataTransfer()
|
|
|
|
pasteImageNode(
|
|
mockCanvas as unknown as LGraphCanvas,
|
|
dataTransfer.items,
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
expect(mockNode.pasteFile).not.toHaveBeenCalled()
|
|
expect(mockNode.pasteFiles).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should filter non-image items', () => {
|
|
const mockNode = createMockNode()
|
|
const imageFile = createImageFile()
|
|
const textFile = new File([''], 'test.txt', { type: 'text/plain' })
|
|
const dataTransfer = createDataTransfer([textFile, imageFile])
|
|
|
|
pasteImageNode(
|
|
mockCanvas as unknown as LGraphCanvas,
|
|
dataTransfer.items,
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(imageFile)
|
|
expect(mockNode.pasteFiles).toHaveBeenCalledWith([imageFile])
|
|
})
|
|
})
|
|
|
|
describe('usePaste', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockCanvas.current_node = null
|
|
mockWorkspaceStore.shiftDown = false
|
|
vi.mocked(mockCanvas.graph!.add).mockImplementation(
|
|
(node: LGraphNode | LGraphGroup) => node as LGraphNode
|
|
)
|
|
})
|
|
|
|
it('should handle image paste', async () => {
|
|
const mockNode = createMockNode()
|
|
vi.mocked(LiteGraph.createNode).mockReturnValue(
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
usePaste()
|
|
|
|
const file = createImageFile()
|
|
const dataTransfer = createDataTransfer([file])
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
await vi.waitFor(() => {
|
|
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage')
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
|
})
|
|
})
|
|
|
|
it('should handle audio paste', async () => {
|
|
const mockNode = createMockNode()
|
|
vi.mocked(LiteGraph.createNode).mockReturnValue(
|
|
mockNode as unknown as LGraphNode
|
|
)
|
|
|
|
usePaste()
|
|
|
|
const file = createAudioFile()
|
|
const dataTransfer = createDataTransfer([file])
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
await vi.waitFor(() => {
|
|
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadAudio')
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
|
})
|
|
})
|
|
|
|
it('should handle workflow JSON paste', async () => {
|
|
const workflow = { version: '1.0', nodes: [], extra: {} }
|
|
|
|
usePaste()
|
|
|
|
const dataTransfer = new DataTransfer()
|
|
dataTransfer.setData('text/plain', JSON.stringify(workflow))
|
|
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
await vi.waitFor(() => {
|
|
expect(app.loadGraphData).toHaveBeenCalledWith(workflow)
|
|
})
|
|
})
|
|
|
|
it('should ignore paste when shift is down', () => {
|
|
mockWorkspaceStore.shiftDown = true
|
|
|
|
usePaste()
|
|
|
|
const file = createImageFile()
|
|
const dataTransfer = createDataTransfer([file])
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
expect(LiteGraph.createNode).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should use existing image node when selected', () => {
|
|
const mockNode = {
|
|
is_selected: true,
|
|
pasteFile: vi.fn(),
|
|
pasteFiles: vi.fn()
|
|
} as unknown as Partial<LGraphNode> as LGraphNode
|
|
mockCanvas.current_node = mockNode
|
|
vi.mocked(isImageNode).mockReturnValue(true)
|
|
|
|
usePaste()
|
|
|
|
const file = createImageFile()
|
|
const dataTransfer = createDataTransfer([file])
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
|
})
|
|
|
|
it('should call canvas pasteFromClipboard for non-workflow text', () => {
|
|
usePaste()
|
|
|
|
const dataTransfer = new DataTransfer()
|
|
dataTransfer.setData('text/plain', 'just some text')
|
|
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
expect(mockCanvas.pasteFromClipboard).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle clipboard items with metadata', async () => {
|
|
const data = { test: 'data' }
|
|
const encoded = btoa(JSON.stringify(data))
|
|
const html = `<div data-metadata="${encoded}"></div>`
|
|
|
|
usePaste()
|
|
|
|
const dataTransfer = new DataTransfer()
|
|
dataTransfer.setData('text/html', html)
|
|
|
|
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
|
document.dispatchEvent(event)
|
|
|
|
await vi.waitFor(() => {
|
|
expect(mockCanvas._deserializeItems).toHaveBeenCalledWith(
|
|
data,
|
|
expect.any(Object)
|
|
)
|
|
})
|
|
})
|
|
})
|