From e968078edef021fc57bb01e3f95b688f6d15bc80 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Wed, 7 Jan 2026 00:32:47 -0600 Subject: [PATCH 1/9] Dragging non-workflow image will create a load image node --- src/composables/usePaste.ts | 81 +++++++++++++++++++++---------------- src/scripts/app.ts | 9 ++++- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index 9a40fab9a3c..6bfefab81b3 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -1,7 +1,7 @@ import { useEventListener } from '@vueuse/core' +import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { app } from '@/scripts/app' @@ -26,6 +26,48 @@ function pasteClipboardItems(data: DataTransfer): boolean { return false } +function pasteItemsOnNode( + items: DataTransferItemList, + node: LGraphNode | null, + contentType: string +): void { + if (!node) return + + const filteredItems = Array.from(items).filter((item) => + item.type.startsWith(contentType) + ) + + const blob = filteredItems[0]?.getAsFile() + if (!blob) return + + node.pasteFile?.(blob) + node.pasteFiles?.( + Array.from(filteredItems) + .map((i) => i.getAsFile()) + .filter((f) => f !== null) + ) +} + +export function pasteImageNode( + canvas: LGraphCanvas, + items: DataTransferItemList, + imageNode: LGraphNode | null = null +): void { + const { graph, graph_mouse: [posX, posY] } = canvas + + if (!imageNode) { + // No image node selected: add a new one + const newNode = LiteGraph.createNode('LoadImage') + if (newNode) { + newNode.pos = [posX, posY] + imageNode = graph?.add(newNode) ?? null + } + graph?.change() + } + + pasteItemsOnNode(items, imageNode, 'image') +} + /** * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data */ @@ -33,28 +75,6 @@ export const usePaste = () => { const workspaceStore = useWorkspaceStore() const canvasStore = useCanvasStore() - const pasteItemsOnNode = ( - items: DataTransferItemList, - node: LGraphNode | null, - contentType: string - ) => { - if (!node) return - - const filteredItems = Array.from(items).filter((item) => - item.type.startsWith(contentType) - ) - - const blob = filteredItems[0]?.getAsFile() - if (!blob) return - - node.pasteFile?.(blob) - node.pasteFiles?.( - Array.from(filteredItems) - .map((i) => i.getAsFile()) - .filter((f) => f !== null) - ) - } - useEventListener(document, 'paste', async (e) => { if (shouldIgnoreCopyPaste(e.target)) { // Default system copy @@ -80,8 +100,10 @@ export const usePaste = () => { const isVideoNodeSelected = isNodeSelected && isVideoNode(currentNode) const isAudioNodeSelected = isNodeSelected && isAudioNode(currentNode) - let imageNode: LGraphNode | null = isImageNodeSelected ? currentNode : null let audioNode: LGraphNode | null = isAudioNodeSelected ? currentNode : null + const imageNode: LGraphNode | null = isImageNodeSelected + ? currentNode + : null const videoNode: LGraphNode | null = isVideoNodeSelected ? currentNode : null @@ -89,16 +111,7 @@ export const usePaste = () => { // Look for image paste data for (const item of items) { if (item.type.startsWith('image/')) { - if (!imageNode) { - // No image node selected: add a new one - const newNode = LiteGraph.createNode('LoadImage') - if (newNode) { - newNode.pos = [canvas.graph_mouse[0], canvas.graph_mouse[1]] - imageNode = graph?.add(newNode) ?? null - } - graph?.change() - } - pasteItemsOnNode(items, imageNode, 'image') + pasteImageNode(canvas as LGraphCanvas, items, imageNode) return } else if (item.type.startsWith('video/')) { if (!videoNode) { diff --git a/src/scripts/app.ts b/src/scripts/app.ts index cd75be7f76c..e0266b6534a 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -96,6 +96,7 @@ import { type ComfyWidgetConstructor } from './widgets' import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale' import { extractFileFromDragEvent } from '@/utils/eventUtils' import { getWorkflowDataFromFile } from '@/scripts/metadata/parser' +import { pasteImageNode } from '@/composables/usePaste' export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview' @@ -1439,10 +1440,16 @@ export class ComfyApp { */ async handleFile(file: File, openSource?: WorkflowOpenSource) { const fileName = file.name.replace(/\.\w+$/, '') // Strip file extension + console.log('DROPPED!') const workflowData = await getWorkflowDataFromFile(file) - if (!workflowData) { + if (!workflowData && !file.type.startsWith('image')) { this.showErrorOnFileLoad(file) return + } else if (file.type.startsWith('image')) { + const transfer = new DataTransfer(); + transfer.items.add(file); + pasteImageNode(this.canvas, transfer.items) + return } const { workflow, prompt, parameters, templates } = workflowData From 677f4c61ec758e1803103cbba4bb33314e4b8ee8 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Wed, 7 Jan 2026 23:54:46 -0600 Subject: [PATCH 2/9] Add tests for usePaste.ts --- src/composables/usePaste.test.ts | 277 +++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 src/composables/usePaste.test.ts diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts new file mode 100644 index 00000000000..a130a9c4a78 --- /dev/null +++ b/src/composables/usePaste.test.ts @@ -0,0 +1,277 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { type LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' +import { app } from '@/scripts/app' +import { isImageNode } from '@/utils/litegraphUtil' +import { pasteImageNode, usePaste } from './usePaste' + +const mockCanvas = { + current_node: null, + graph: { + add: vi.fn(), + change: vi.fn() + }, + graph_mouse: [100, 200], + pasteFromClipboard: vi.fn(), + _deserializeItems: vi.fn() +} + +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() + mockCanvas.graph.add.mockImplementation((node) => node) + }) + + it('should create new LoadImage node when no image node provided', () => { + const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } + vi.mocked(LiteGraph.createNode) + .mockReturnValue(mockNode as unknown as LGraphNode) + + const file = new File([''], 'test.png', { type: 'image/png' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + pasteImageNode(mockCanvas as any, 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 = { pasteFile: vi.fn(), pasteFiles: vi.fn() } + + const file = new File([''], 'test.png', { type: 'image/png' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + + expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + expect(mockNode.pasteFiles).toHaveBeenCalledWith([file]) + }) + + it('should handle multiple image files', () => { + const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } + + const file1 = new File([''], 'test1.png', { type: 'image/png' }) + const file2 = new File([''], 'test2.jpg', { type: 'image/jpeg' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file1) + dataTransfer.items.add(file2) + + pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + + expect(mockNode.pasteFile).toHaveBeenCalledWith(file1) + expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2]) + }) + + it('should do nothing when no image files present', () => { + const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } + + const dataTransfer = new DataTransfer() + + pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + + expect(mockNode.pasteFile).not.toHaveBeenCalled() + expect(mockNode.pasteFiles).not.toHaveBeenCalled() + }) + + it('should filter non-image items', () => { + const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } + + const imageFile = new File([''], 'test.png', { type: 'image/png' }) + const textFile = new File([''], 'test.txt', { type: 'text/plain' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(textFile) + dataTransfer.items.add(imageFile) + + pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + + expect(mockNode.pasteFile).toHaveBeenCalledWith(imageFile) + expect(mockNode.pasteFiles).toHaveBeenCalledWith([imageFile]) + }) +}) + +describe('usePaste', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCanvas.current_node = null + mockWorkspaceStore.shiftDown = false + mockCanvas.graph.add.mockImplementation((node) => node) + }) + + it('should handle image paste', async () => { + const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } + vi.mocked(LiteGraph.createNode) + .mockReturnValue(mockNode as unknown as LGraphNode) + + usePaste() + + const file = new File([''], 'test.png', { type: 'image/png' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) + document.dispatchEvent(event) + + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') + expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + }) + + it('should handle audio paste', async () => { + const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } + vi.mocked(LiteGraph.createNode) + .mockReturnValue(mockNode as unknown as LGraphNode) + + usePaste() + + const file = new File([''], 'test.mp3', { type: 'audio/mp3' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) + document.dispatchEvent(event) + + await new Promise(resolve => setTimeout(resolve, 0)) + + 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 new Promise(resolve => setTimeout(resolve, 0)) + + expect(app.loadGraphData).toHaveBeenCalledWith(workflow) + }) + + it('should ignore paste when shift is down', () => { + mockWorkspaceStore.shiftDown = true + + usePaste() + + const file = new File([''], 'test.png', { type: 'image/png' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(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() + } + mockCanvas.current_node = mockNode as any + vi.mocked(isImageNode).mockReturnValue(true) + + usePaste() + + const file = new File([''], 'test.png', { type: 'image/png' }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) + document.dispatchEvent(event) + + expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + }) + + it('should call canvas pasteFromClipboard for text inputs', () => { + usePaste() + + const input = document.createElement('input') + input.type = 'text' + document.body.appendChild(input) + + const dataTransfer = new DataTransfer() + dataTransfer.setData('text/plain', 'test') + + const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) + Object.defineProperty(event, 'target', { value: input, writable: false }) + + document.dispatchEvent(event) + + document.body.removeChild(input) + }) + + it('should handle clipboard items with metadata', () => { + const data = { test: 'data' } + const encoded = btoa(new TextEncoder().encode(JSON.stringify(data)) + .reduce((acc, byte) => acc + String.fromCharCode(byte), '')) + const html = `
` + + usePaste() + + const dataTransfer = new DataTransfer() + dataTransfer.setData('text/html', html) + + const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) + document.dispatchEvent(event) + + expect(mockCanvas._deserializeItems).toHaveBeenCalledWith(data, {}) + }) +}) From d8b9b470d53fa74c2ba7fbf4c7c03cfb7479b739 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Thu, 8 Jan 2026 00:15:42 -0600 Subject: [PATCH 3/9] Updated logic so workflow images work as expected. --- src/scripts/app.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index e0266b6534a..1bfce7580e0 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1442,14 +1442,16 @@ export class ComfyApp { const fileName = file.name.replace(/\.\w+$/, '') // Strip file extension console.log('DROPPED!') const workflowData = await getWorkflowDataFromFile(file) - if (!workflowData && !file.type.startsWith('image')) { + if (!workflowData) { + if (file.type.startsWith('image')) { + const transfer = new DataTransfer() + transfer.items.add(file) + pasteImageNode(this.canvas, transfer.items) + return + } + this.showErrorOnFileLoad(file) return - } else if (file.type.startsWith('image')) { - const transfer = new DataTransfer(); - transfer.items.add(file); - pasteImageNode(this.canvas, transfer.items) - return } const { workflow, prompt, parameters, templates } = workflowData From 00b4e34581c8b383deb58bdc94b09958d1ad2416 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Thu, 8 Jan 2026 00:41:29 -0600 Subject: [PATCH 4/9] Use vi.waitFor() --- src/composables/usePaste.test.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index a130a9c4a78..10c7491def3 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -162,10 +162,10 @@ describe('usePaste', () => { const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) - await new Promise(resolve => setTimeout(resolve, 0)) - - expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') - expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + await vi.waitFor(() => { + expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') + expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + }) }) it('should handle audio paste', async () => { @@ -182,10 +182,10 @@ describe('usePaste', () => { const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) - await new Promise(resolve => setTimeout(resolve, 0)) - - expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadAudio') - expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + await vi.waitFor(() => { + expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadAudio') + expect(mockNode.pasteFile).toHaveBeenCalledWith(file) + }) }) it('should handle workflow JSON paste', async () => { @@ -199,9 +199,9 @@ describe('usePaste', () => { const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) - await new Promise(resolve => setTimeout(resolve, 0)) - - expect(app.loadGraphData).toHaveBeenCalledWith(workflow) + await vi.waitFor(() => { + expect(app.loadGraphData).toHaveBeenCalledWith(workflow) + }) }) it('should ignore paste when shift is down', () => { From 5ae8ad9b0d6f6bf43a6d33cae787b8fcbde8657d Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Thu, 8 Jan 2026 00:44:15 -0600 Subject: [PATCH 5/9] Removed log --- src/scripts/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 1bfce7580e0..0701f694d81 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1440,7 +1440,6 @@ export class ComfyApp { */ async handleFile(file: File, openSource?: WorkflowOpenSource) { const fileName = file.name.replace(/\.\w+$/, '') // Strip file extension - console.log('DROPPED!') const workflowData = await getWorkflowDataFromFile(file) if (!workflowData) { if (file.type.startsWith('image')) { From 1cffc71fdd4efec5bb5500c52379825f9242cb06 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Thu, 8 Jan 2026 00:59:03 -0600 Subject: [PATCH 6/9] Code Rabbit Review Updates --- src/composables/usePaste.test.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index 10c7491def3..e256d5214b2 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -240,28 +240,21 @@ describe('usePaste', () => { expect(mockNode.pasteFile).toHaveBeenCalledWith(file) }) - it('should call canvas pasteFromClipboard for text inputs', () => { + it('should call canvas pasteFromClipboard for non-workflow text', () => { usePaste() - const input = document.createElement('input') - input.type = 'text' - document.body.appendChild(input) - const dataTransfer = new DataTransfer() - dataTransfer.setData('text/plain', 'test') + dataTransfer.setData('text/plain', 'just some text') const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) - Object.defineProperty(event, 'target', { value: input, writable: false }) - document.dispatchEvent(event) - document.body.removeChild(input) + expect(mockCanvas.pasteFromClipboard).toHaveBeenCalled() }) it('should handle clipboard items with metadata', () => { const data = { test: 'data' } - const encoded = btoa(new TextEncoder().encode(JSON.stringify(data)) - .reduce((acc, byte) => acc + String.fromCharCode(byte), '')) + const encoded = btoa(JSON.stringify(data)) const html = `
` usePaste() From e7e846d6fefc098a64482fb3e357445f2e6c3028 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Thu, 8 Jan 2026 01:05:57 -0600 Subject: [PATCH 7/9] Updating more types in the tests for Code Rabbit --- src/composables/usePaste.test.ts | 44 +++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index e256d5214b2..be5e6512186 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { type LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' +import type { LGraphCanvas, 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' @@ -69,14 +70,15 @@ describe('pasteImageNode', () => { it('should create new LoadImage node when no image node provided', () => { const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } - vi.mocked(LiteGraph.createNode) - .mockReturnValue(mockNode as unknown as LGraphNode) + vi.mocked(LiteGraph.createNode).mockReturnValue( + mockNode as unknown as LGraphNode + ) const file = new File([''], 'test.png', { type: 'image/png' }) const dataTransfer = new DataTransfer() dataTransfer.items.add(file) - pasteImageNode(mockCanvas as any, dataTransfer.items) + pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items) expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') expect(mockNode.pos).toEqual([100, 200]) @@ -92,7 +94,11 @@ describe('pasteImageNode', () => { const dataTransfer = new DataTransfer() dataTransfer.items.add(file) - pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + pasteImageNode( + mockCanvas as unknown as LGraphCanvas, + dataTransfer.items, + mockNode as unknown as LGraphNode + ) expect(mockNode.pasteFile).toHaveBeenCalledWith(file) expect(mockNode.pasteFiles).toHaveBeenCalledWith([file]) @@ -107,7 +113,11 @@ describe('pasteImageNode', () => { dataTransfer.items.add(file1) dataTransfer.items.add(file2) - pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + pasteImageNode( + mockCanvas as unknown as LGraphCanvas, + dataTransfer.items, + mockNode as unknown as LGraphNode + ) expect(mockNode.pasteFile).toHaveBeenCalledWith(file1) expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2]) @@ -118,7 +128,11 @@ describe('pasteImageNode', () => { const dataTransfer = new DataTransfer() - pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + pasteImageNode( + mockCanvas as unknown as LGraphCanvas, + dataTransfer.items, + mockNode as unknown as LGraphNode + ) expect(mockNode.pasteFile).not.toHaveBeenCalled() expect(mockNode.pasteFiles).not.toHaveBeenCalled() @@ -133,7 +147,11 @@ describe('pasteImageNode', () => { dataTransfer.items.add(textFile) dataTransfer.items.add(imageFile) - pasteImageNode(mockCanvas as any, dataTransfer.items, mockNode as any) + pasteImageNode( + mockCanvas as unknown as LGraphCanvas, + dataTransfer.items, + mockNode as unknown as LGraphNode + ) expect(mockNode.pasteFile).toHaveBeenCalledWith(imageFile) expect(mockNode.pasteFiles).toHaveBeenCalledWith([imageFile]) @@ -150,8 +168,9 @@ describe('usePaste', () => { it('should handle image paste', async () => { const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } - vi.mocked(LiteGraph.createNode) - .mockReturnValue(mockNode as unknown as LGraphNode) + vi.mocked(LiteGraph.createNode).mockReturnValue( + mockNode as unknown as LGraphNode + ) usePaste() @@ -170,8 +189,9 @@ describe('usePaste', () => { it('should handle audio paste', async () => { const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } - vi.mocked(LiteGraph.createNode) - .mockReturnValue(mockNode as unknown as LGraphNode) + vi.mocked(LiteGraph.createNode).mockReturnValue( + mockNode as unknown as LGraphNode + ) usePaste() From 0a9ffe59e4c670ab99f80d33819d83a7f8f770f2 Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Thu, 8 Jan 2026 23:52:39 -0600 Subject: [PATCH 8/9] Updating types --- src/composables/usePaste.test.ts | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index be5e6512186..8756185ea79 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -1,20 +1,25 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +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' const mockCanvas = { - current_node: null, + current_node: null as LGraphNode | null, graph: { add: vi.fn(), change: vi.fn() - }, + } as Partial as LGraph, graph_mouse: [100, 200], pasteFromClipboard: vi.fn(), _deserializeItems: vi.fn() -} +} as Partial as LGraphCanvas const mockCanvasStore = { canvas: mockCanvas, @@ -65,7 +70,9 @@ vi.mock('@/workbench/eventHelpers', () => ({ describe('pasteImageNode', () => { beforeEach(() => { vi.clearAllMocks() - mockCanvas.graph.add.mockImplementation((node) => node) + vi.mocked(mockCanvas.graph!.add).mockImplementation( + (node: LGraphNode | LGraphGroup) => node as LGraphNode + ) }) it('should create new LoadImage node when no image node provided', () => { @@ -82,8 +89,8 @@ describe('pasteImageNode', () => { expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage') expect(mockNode.pos).toEqual([100, 200]) - expect(mockCanvas.graph.add).toHaveBeenCalledWith(mockNode) - expect(mockCanvas.graph.change).toHaveBeenCalled() + expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode) + expect(mockCanvas.graph!.change).toHaveBeenCalled() expect(mockNode.pasteFile).toHaveBeenCalledWith(file) }) @@ -163,7 +170,9 @@ describe('usePaste', () => { vi.clearAllMocks() mockCanvas.current_node = null mockWorkspaceStore.shiftDown = false - mockCanvas.graph.add.mockImplementation((node) => node) + vi.mocked(mockCanvas.graph!.add).mockImplementation( + (node: LGraphNode | LGraphGroup) => node as LGraphNode + ) }) it('should handle image paste', async () => { @@ -244,8 +253,8 @@ describe('usePaste', () => { is_selected: true, pasteFile: vi.fn(), pasteFiles: vi.fn() - } - mockCanvas.current_node = mockNode as any + } as unknown as Partial as LGraphNode + mockCanvas.current_node = mockNode vi.mocked(isImageNode).mockReturnValue(true) usePaste() From f08e3b7a8a57551379f7c655265fbc760f274fbb Mon Sep 17 00:00:00 2001 From: Brian Jemilo II Date: Fri, 9 Jan 2026 02:00:32 -0600 Subject: [PATCH 9/9] Created factories, updated mp3 meme type, and vi.waitFor --- src/composables/usePaste.test.ts | 105 ++++++++++++++++++------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index 8756185ea79..4e1ac3503c5 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -10,6 +10,34 @@ 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: { @@ -76,14 +104,13 @@ describe('pasteImageNode', () => { }) it('should create new LoadImage node when no image node provided', () => { - const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } + const mockNode = createMockNode() vi.mocked(LiteGraph.createNode).mockReturnValue( mockNode as unknown as LGraphNode ) - const file = new File([''], 'test.png', { type: 'image/png' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file) + const file = createImageFile() + const dataTransfer = createDataTransfer([file]) pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items) @@ -95,11 +122,9 @@ describe('pasteImageNode', () => { }) it('should use existing image node when provided', () => { - const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } - - const file = new File([''], 'test.png', { type: 'image/png' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file) + const mockNode = createMockNode() + const file = createImageFile() + const dataTransfer = createDataTransfer([file]) pasteImageNode( mockCanvas as unknown as LGraphCanvas, @@ -112,13 +137,10 @@ describe('pasteImageNode', () => { }) it('should handle multiple image files', () => { - const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } - - const file1 = new File([''], 'test1.png', { type: 'image/png' }) - const file2 = new File([''], 'test2.jpg', { type: 'image/jpeg' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file1) - dataTransfer.items.add(file2) + 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, @@ -131,9 +153,8 @@ describe('pasteImageNode', () => { }) it('should do nothing when no image files present', () => { - const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } - - const dataTransfer = new DataTransfer() + const mockNode = createMockNode() + const dataTransfer = createDataTransfer() pasteImageNode( mockCanvas as unknown as LGraphCanvas, @@ -146,13 +167,10 @@ describe('pasteImageNode', () => { }) it('should filter non-image items', () => { - const mockNode = { pasteFile: vi.fn(), pasteFiles: vi.fn() } - - const imageFile = new File([''], 'test.png', { type: 'image/png' }) + const mockNode = createMockNode() + const imageFile = createImageFile() const textFile = new File([''], 'test.txt', { type: 'text/plain' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(textFile) - dataTransfer.items.add(imageFile) + const dataTransfer = createDataTransfer([textFile, imageFile]) pasteImageNode( mockCanvas as unknown as LGraphCanvas, @@ -176,17 +194,15 @@ describe('usePaste', () => { }) it('should handle image paste', async () => { - const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } + const mockNode = createMockNode() vi.mocked(LiteGraph.createNode).mockReturnValue( mockNode as unknown as LGraphNode ) usePaste() - const file = new File([''], 'test.png', { type: 'image/png' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file) - + const file = createImageFile() + const dataTransfer = createDataTransfer([file]) const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) @@ -197,17 +213,15 @@ describe('usePaste', () => { }) it('should handle audio paste', async () => { - const mockNode = { pos: [0, 0], pasteFile: vi.fn(), pasteFiles: vi.fn() } + const mockNode = createMockNode() vi.mocked(LiteGraph.createNode).mockReturnValue( mockNode as unknown as LGraphNode ) usePaste() - const file = new File([''], 'test.mp3', { type: 'audio/mp3' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file) - + const file = createAudioFile() + const dataTransfer = createDataTransfer([file]) const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) @@ -238,10 +252,8 @@ describe('usePaste', () => { usePaste() - const file = new File([''], 'test.png', { type: 'image/png' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file) - + const file = createImageFile() + const dataTransfer = createDataTransfer([file]) const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) @@ -259,10 +271,8 @@ describe('usePaste', () => { usePaste() - const file = new File([''], 'test.png', { type: 'image/png' }) - const dataTransfer = new DataTransfer() - dataTransfer.items.add(file) - + const file = createImageFile() + const dataTransfer = createDataTransfer([file]) const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) @@ -281,7 +291,7 @@ describe('usePaste', () => { expect(mockCanvas.pasteFromClipboard).toHaveBeenCalled() }) - it('should handle clipboard items with metadata', () => { + it('should handle clipboard items with metadata', async () => { const data = { test: 'data' } const encoded = btoa(JSON.stringify(data)) const html = `
` @@ -294,6 +304,11 @@ describe('usePaste', () => { const event = new ClipboardEvent('paste', { clipboardData: dataTransfer }) document.dispatchEvent(event) - expect(mockCanvas._deserializeItems).toHaveBeenCalledWith(data, {}) + await vi.waitFor(() => { + expect(mockCanvas._deserializeItems).toHaveBeenCalledWith( + data, + expect.any(Object) + ) + }) }) })