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 { createNode, isImageNode } from '@/utils/litegraphUtil' import { cloneDataTransfer, pasteImageNode, pasteImageNodes, 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 as LGraph, graph_mouse: [100, 200], pasteFromClipboard: vi.fn(), _deserializeItems: vi.fn() } as Partial 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', () => ({ createNode: vi.fn(), 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 | null) => node as LGraphNode ) }) it('should create new LoadImage node when no image node provided', async () => { const mockNode = createMockNode() vi.mocked(createNode).mockResolvedValue(mockNode as unknown as LGraphNode) const file = createImageFile() const dataTransfer = createDataTransfer([file]) await pasteImageNode( mockCanvas as unknown as LGraphCanvas, dataTransfer.items ) expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadImage') expect(mockNode.pasteFile).toHaveBeenCalledWith(file) }) it('should use existing image node when provided', async () => { const mockNode = createMockNode() const file = createImageFile() const dataTransfer = createDataTransfer([file]) await 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', async () => { const mockNode = createMockNode() const file1 = createImageFile('test1.png') const file2 = createImageFile('test2.jpg', 'image/jpeg') const dataTransfer = createDataTransfer([file1, file2]) await 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', async () => { const mockNode = createMockNode() const dataTransfer = createDataTransfer() await 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', async () => { const mockNode = createMockNode() const imageFile = createImageFile() const textFile = new File([''], 'test.txt', { type: 'text/plain' }) const dataTransfer = createDataTransfer([textFile, imageFile]) await pasteImageNode( mockCanvas as unknown as LGraphCanvas, dataTransfer.items, mockNode as unknown as LGraphNode ) expect(mockNode.pasteFile).toHaveBeenCalledWith(imageFile) expect(mockNode.pasteFiles).toHaveBeenCalledWith([imageFile]) }) }) describe('pasteImageNodes', () => { beforeEach(() => { vi.clearAllMocks() }) it('should create multiple nodes for multiple files', async () => { const mockNode1 = createMockNode() const mockNode2 = createMockNode() vi.mocked(createNode) .mockResolvedValueOnce(mockNode1 as unknown as LGraphNode) .mockResolvedValueOnce(mockNode2 as unknown as LGraphNode) const file1 = createImageFile('test1.png') const file2 = createImageFile('test2.jpg', 'image/jpeg') const fileList = createDataTransfer([file1, file2]).files const result = await pasteImageNodes( mockCanvas as unknown as LGraphCanvas, fileList ) expect(createNode).toHaveBeenCalledTimes(2) expect(createNode).toHaveBeenNthCalledWith(1, mockCanvas, 'LoadImage') expect(createNode).toHaveBeenNthCalledWith(2, mockCanvas, 'LoadImage') expect(mockNode1.pasteFile).toHaveBeenCalledWith(file1) expect(mockNode2.pasteFile).toHaveBeenCalledWith(file2) expect(result).toEqual([mockNode1, mockNode2]) }) it('should handle empty file list', async () => { const fileList = createDataTransfer([]).files const result = await pasteImageNodes( mockCanvas as unknown as LGraphCanvas, fileList ) expect(createNode).not.toHaveBeenCalled() expect(result).toEqual([]) }) }) describe('usePaste', () => { beforeEach(() => { vi.clearAllMocks() mockCanvas.current_node = null mockWorkspaceStore.shiftDown = false vi.mocked(mockCanvas.graph!.add).mockImplementation( (node: LGraphNode | LGraphGroup | null) => node as LGraphNode ) }) it('should handle image paste', async () => { const mockNode = createMockNode() vi.mocked(createNode).mockResolvedValue(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(createNode).toHaveBeenCalledWith(mockCanvas, '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 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 = `
` 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) ) }) }) }) describe('cloneDataTransfer', () => { it('should clone string data', () => { const original = new DataTransfer() original.setData('text/plain', 'test text') original.setData('text/html', '

test html

') const cloned = cloneDataTransfer(original) expect(cloned.getData('text/plain')).toBe('test text') expect(cloned.getData('text/html')).toBe('

test html

') }) it('should clone files', () => { const file1 = createImageFile('test1.png') const file2 = createImageFile('test2.jpg', 'image/jpeg') const original = createDataTransfer([file1, file2]) const cloned = cloneDataTransfer(original) // Files are added from both .files and .items, causing duplicates expect(cloned.files.length).toBeGreaterThanOrEqual(2) expect(Array.from(cloned.files)).toContain(file1) expect(Array.from(cloned.files)).toContain(file2) }) it('should preserve dropEffect and effectAllowed', () => { const original = new DataTransfer() original.dropEffect = 'copy' original.effectAllowed = 'copyMove' const cloned = cloneDataTransfer(original) expect(cloned.dropEffect).toBe('copy') expect(cloned.effectAllowed).toBe('copyMove') }) it('should handle empty DataTransfer', () => { const original = new DataTransfer() const cloned = cloneDataTransfer(original) expect(cloned.types.length).toBe(0) expect(cloned.files.length).toBe(0) }) it('should clone both string data and files', () => { const file = createImageFile() const original = createDataTransfer([file]) original.setData('text/plain', 'test') const cloned = cloneDataTransfer(original) expect(cloned.getData('text/plain')).toBe('test') // Files are added from both .files and .items expect(cloned.files.length).toBeGreaterThanOrEqual(1) expect(Array.from(cloned.files)).toContain(file) }) })