From 168af5310bbdb4c73876d9d40a8ce9dcf988384f Mon Sep 17 00:00:00 2001 From: DrJKL Date: Sun, 11 Jan 2026 23:54:49 -0800 Subject: [PATCH] fix: remove @ts-expect-error suppressions with proper type guards --- .../content/MissingCoreNodesMessage.test.ts | 62 ++++- src/components/graph/NodeContextMenu.vue | 39 ++- .../topbar/CurrentUserButton.test.ts | 28 ++- .../canvas/useSelectedLiteGraphItems.test.ts | 77 +++--- .../graph/useSelectionState.test.ts | 230 ++++++------------ .../maskeditor/useCanvasHistory.test.ts | 8 +- .../maskeditor/useCanvasManager.test.ts | 156 ++++++++---- .../maskeditor/useCanvasTransform.test.ts | 13 +- src/composables/usePaste.test.ts | 120 ++++----- src/composables/usePaste.ts | 10 +- src/extensions/core/groupNode.ts | 3 +- src/extensions/core/groupNodeManage.ts | 35 ++- src/extensions/core/uploadAudio.ts | 5 +- src/lib/litegraph/src/LGraph.ts | 52 ++-- src/lib/litegraph/src/LGraphCanvas.ts | 16 +- src/stores/workspace/searchBoxStore.ts | 23 +- 16 files changed, 490 insertions(+), 387 deletions(-) diff --git a/src/components/dialog/content/MissingCoreNodesMessage.test.ts b/src/components/dialog/content/MissingCoreNodesMessage.test.ts index 81c28045c..59d96b3f0 100644 --- a/src/components/dialog/content/MissingCoreNodesMessage.test.ts +++ b/src/components/dialog/content/MissingCoreNodesMessage.test.ts @@ -6,6 +6,7 @@ import { nextTick } from 'vue' import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { SystemStats } from '@/schemas/apiSchema' import { useSystemStatsStore } from '@/stores/systemStatsStore' // Mock the stores @@ -13,8 +14,8 @@ vi.mock('@/stores/systemStatsStore', () => ({ useSystemStatsStore: vi.fn() })) -const createMockNode = (type: string, version?: string): LGraphNode => - ({ +function createMockNode(type: string, version?: string): LGraphNode { + return Object.assign(Object.create(null), { type, properties: { cnr_id: 'comfy-core', ver: version }, id: 1, @@ -26,19 +27,54 @@ const createMockNode = (type: string, version?: string): LGraphNode => mode: 0, inputs: [], outputs: [] - }) as unknown as LGraphNode + }) +} + +interface MockSystemStatsStore { + systemStats: SystemStats | null + isLoading: boolean + error: Error | undefined + isInitialized: boolean + refetchSystemStats: ReturnType + getFormFactor: () => string +} + +function createMockSystemStats( + overrides: Partial = {} +): SystemStats { + return { + system: { + os: 'linux', + python_version: '3.10.0', + embedded_python: false, + comfyui_version: '1.0.0', + pytorch_version: '2.0.0', + argv: [], + ram_total: 16000000000, + ram_free: 8000000000, + ...overrides + }, + devices: [] + } +} + +function createMockSystemStatsStore(): MockSystemStatsStore { + return { + systemStats: null, + isLoading: false, + error: undefined, + isInitialized: true, + refetchSystemStats: vi.fn(), + getFormFactor: () => 'other' + } +} describe('MissingCoreNodesMessage', () => { - const mockSystemStatsStore = { - systemStats: null as { system?: { comfyui_version?: string } } | null, - refetchSystemStats: vi.fn() - } + let mockSystemStatsStore: MockSystemStatsStore beforeEach(() => { vi.clearAllMocks() - // Reset the mock store state - mockSystemStatsStore.systemStats = null - mockSystemStatsStore.refetchSystemStats = vi.fn() + mockSystemStatsStore = createMockSystemStatsStore() vi.mocked(useSystemStatsStore).mockReturnValue( mockSystemStatsStore as unknown as ReturnType ) @@ -86,9 +122,9 @@ describe('MissingCoreNodesMessage', () => { it('displays current ComfyUI version when available', async () => { // Set systemStats directly (store auto-fetches with useAsyncState) - mockSystemStatsStore.systemStats = { - system: { comfyui_version: '1.0.0' } - } + mockSystemStatsStore.systemStats = createMockSystemStats({ + comfyui_version: '1.0.0' + }) const missingCoreNodes = { '1.2.0': [createMockNode('TestNode', '1.2.0')] diff --git a/src/components/graph/NodeContextMenu.vue b/src/components/graph/NodeContextMenu.vue index 97ee1b9c9..e38b4b4a1 100644 --- a/src/components/graph/NodeContextMenu.vue +++ b/src/components/graph/NodeContextMenu.vue @@ -42,7 +42,14 @@ import { useElementBounding, useEventListener, useRafFn } from '@vueuse/core' import ContextMenu from 'primevue/contextmenu' import type { MenuItem } from 'primevue/menuitem' -import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue' +import { + computed, + onMounted, + onUnmounted, + ref, + useTemplateRef, + watchEffect +} from 'vue' import { registerNodeOptionsInstance, @@ -56,14 +63,29 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import ColorPickerMenu from './selectionToolbox/ColorPickerMenu.vue' +function getMenuElement( + menu: InstanceType | null +): HTMLElement | undefined { + if (!menu) return undefined + if ('container' in menu && menu.container instanceof HTMLElement) { + return menu.container + } + if ('$el' in menu && menu.$el instanceof HTMLElement) { + return menu.$el + } + return undefined +} + interface ExtendedMenuItem extends MenuItem { isColorSubmenu?: boolean shortcut?: string originalOption?: MenuOption } -const contextMenu = ref>() -const colorPickerMenu = ref>() +const contextMenu = + useTemplateRef>('contextMenu') +const colorPickerMenu = + useTemplateRef>('colorPickerMenu') const isOpen = ref(false) const { menuOptions, bump } = useMoreOptionsMenu() @@ -85,10 +107,7 @@ let lastOffsetY = 0 const updateMenuPosition = () => { if (!isOpen.value) return - const menuInstance = contextMenu.value as unknown as { - container?: HTMLElement - } - const menuEl = menuInstance?.container + const menuEl = getMenuElement(contextMenu.value) if (!menuEl) return const { scale, offset } = lgCanvas.ds @@ -137,11 +156,7 @@ useEventListener( if (!isOpen.value || !contextMenu.value) return const target = event.target as Node - const contextMenuInstance = contextMenu.value as unknown as { - container?: HTMLElement - $el?: HTMLElement - } - const menuEl = contextMenuInstance.container || contextMenuInstance.$el + const menuEl = getMenuElement(contextMenu.value) if (menuEl && !menuEl.contains(target)) { hide() diff --git a/src/components/topbar/CurrentUserButton.test.ts b/src/components/topbar/CurrentUserButton.test.ts index f1a1c2194..a41455ec9 100644 --- a/src/components/topbar/CurrentUserButton.test.ts +++ b/src/components/topbar/CurrentUserButton.test.ts @@ -1,5 +1,7 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' +import type { ComponentExposed } from 'vue-component-type-helpers' + import Button from '@/components/ui/button/Button.vue' import { beforeEach, describe, expect, it, vi } from 'vitest' import { h } from 'vue' @@ -9,6 +11,8 @@ import enMessages from '@/locales/en/main.json' with { type: 'json' } import CurrentUserButton from './CurrentUserButton.vue' +type CurrentUserButtonInstance = ComponentExposed + // Mock all firebase modules vi.mock('firebase/app', () => ({ initializeApp: vi.fn(), @@ -94,33 +98,31 @@ describe('CurrentUserButton', () => { }) it('toggles popover on button click', async () => { - const wrapper = mountComponent() as VueWrapper< - InstanceType - > + const wrapper = mountComponent() const popoverToggleSpy = vi.fn() // Override the ref with a mock implementation - wrapper.vm.popover = { - toggle: popoverToggleSpy - } as unknown as typeof wrapper.vm.popover + Object.assign(wrapper.vm, { + popover: { toggle: popoverToggleSpy } + }) await wrapper.findComponent(Button).trigger('click') expect(popoverToggleSpy).toHaveBeenCalled() }) it('hides popover when closePopover is called', async () => { - const wrapper = mountComponent() as VueWrapper< - InstanceType - > + const wrapper = mountComponent() // Replace the popover.hide method with a spy const popoverHideSpy = vi.fn() - wrapper.vm.popover = { - hide: popoverHideSpy - } as unknown as typeof wrapper.vm.popover + Object.assign(wrapper.vm, { + popover: { hide: popoverHideSpy } + }) // Directly call the closePopover method through the component instance - wrapper.vm.closePopover() + // closePopover is exposed via defineExpose in the component + const vm = wrapper.vm as CurrentUserButtonInstance + vm.closePopover() // Verify that popover.hide was called expect(popoverHideSpy).toHaveBeenCalled() diff --git a/src/composables/canvas/useSelectedLiteGraphItems.test.ts b/src/composables/canvas/useSelectedLiteGraphItems.test.ts index 0b936cf3b..e70b98f01 100644 --- a/src/composables/canvas/useSelectedLiteGraphItems.test.ts +++ b/src/composables/canvas/useSelectedLiteGraphItems.test.ts @@ -73,6 +73,20 @@ class MockReroute implements Positionable { } } +// Helper to create mock LGraphNode objects +function createMockLGraphNode( + id: number, + mode: number, + subgraphNodes?: LGraphNode[] +): LGraphNode { + return Object.assign(Object.create(null), { + id, + mode, + isSubgraphNode: subgraphNodes ? () => true : undefined, + subgraph: subgraphNodes ? { nodes: subgraphNodes } : undefined + }) +} + describe('useSelectedLiteGraphItems', () => { let canvasStore: ReturnType let mockCanvas: any @@ -208,8 +222,8 @@ describe('useSelectedLiteGraphItems', () => { describe('node-specific methods', () => { it('getSelectedNodes should return only LGraphNode instances', () => { const { getSelectedNodes } = useSelectedLiteGraphItems() - const node1 = { id: 1, mode: LGraphEventMode.ALWAYS } as LGraphNode - const node2 = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode + const node1 = createMockLGraphNode(1, LGraphEventMode.ALWAYS) + const node2 = createMockLGraphNode(2, LGraphEventMode.NEVER) // Mock app.canvas.selected_nodes app.canvas.selected_nodes = { '0': node1, '1': node2 } @@ -231,8 +245,8 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should toggle node modes correctly', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const node1 = { id: 1, mode: LGraphEventMode.ALWAYS } as LGraphNode - const node2 = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode + const node1 = createMockLGraphNode(1, LGraphEventMode.ALWAYS) + const node2 = createMockLGraphNode(2, LGraphEventMode.NEVER) app.canvas.selected_nodes = { '0': node1, '1': node2 } @@ -247,7 +261,7 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should set mode to ALWAYS when already in target mode', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const node = { id: 1, mode: LGraphEventMode.BYPASS } as LGraphNode + const node = createMockLGraphNode(1, LGraphEventMode.BYPASS) app.canvas.selected_nodes = { '0': node } @@ -260,17 +274,13 @@ describe('useSelectedLiteGraphItems', () => { it('getSelectedNodes should include nodes from subgraphs', () => { const { getSelectedNodes } = useSelectedLiteGraphItems() - const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode - const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode - const subgraphNode = { - id: 1, - mode: LGraphEventMode.ALWAYS, - isSubgraphNode: () => true, - subgraph: { - nodes: [subNode1, subNode2] - } - } as unknown as LGraphNode - const regularNode = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode + const subNode1 = createMockLGraphNode(11, LGraphEventMode.ALWAYS) + const subNode2 = createMockLGraphNode(12, LGraphEventMode.NEVER) + const subgraphNode = createMockLGraphNode(1, LGraphEventMode.ALWAYS, [ + subNode1, + subNode2 + ]) + const regularNode = createMockLGraphNode(2, LGraphEventMode.NEVER) app.canvas.selected_nodes = { '0': subgraphNode, '1': regularNode } @@ -284,17 +294,13 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should apply unified state to subgraph children', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode - const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode - const subgraphNode = { - id: 1, - mode: LGraphEventMode.ALWAYS, - isSubgraphNode: () => true, - subgraph: { - nodes: [subNode1, subNode2] - } - } as unknown as LGraphNode - const regularNode = { id: 2, mode: LGraphEventMode.BYPASS } as LGraphNode + const subNode1 = createMockLGraphNode(11, LGraphEventMode.ALWAYS) + const subNode2 = createMockLGraphNode(12, LGraphEventMode.NEVER) + const subgraphNode = createMockLGraphNode(1, LGraphEventMode.ALWAYS, [ + subNode1, + subNode2 + ]) + const regularNode = createMockLGraphNode(2, LGraphEventMode.BYPASS) app.canvas.selected_nodes = { '0': subgraphNode, '1': regularNode } @@ -315,16 +321,13 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should toggle to ALWAYS when subgraph is already in target mode', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode - const subNode2 = { id: 12, mode: LGraphEventMode.BYPASS } as LGraphNode - const subgraphNode = { - id: 1, - mode: LGraphEventMode.NEVER, // Already in NEVER mode - isSubgraphNode: () => true, - subgraph: { - nodes: [subNode1, subNode2] - } - } as unknown as LGraphNode + const subNode1 = createMockLGraphNode(11, LGraphEventMode.ALWAYS) + const subNode2 = createMockLGraphNode(12, LGraphEventMode.BYPASS) + // subgraphNode already in NEVER mode + const subgraphNode = createMockLGraphNode(1, LGraphEventMode.NEVER, [ + subNode1, + subNode2 + ]) app.canvas.selected_nodes = { '0': subgraphNode } diff --git a/src/composables/graph/useSelectionState.test.ts b/src/composables/graph/useSelectionState.test.ts index cf0e4bd84..1ba6d4d79 100644 --- a/src/composables/graph/useSelectionState.test.ts +++ b/src/composables/graph/useSelectionState.test.ts @@ -1,19 +1,24 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { ref } from 'vue' -import type { Ref } from 'vue' import { useSelectionState } from '@/composables/graph/useSelectionState' -import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab' +import type { Positionable } from '@/lib/litegraph/src/interfaces' import { LGraphEventMode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import { useNodeDefStore } from '@/stores/nodeDefStore' -import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore' -import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil' import { filterOutputNodes } from '@/utils/nodeFilterUtil' -// Test interfaces +vi.mock('@/utils/litegraphUtil', () => ({ + isLGraphNode: vi.fn(), + isImageNode: vi.fn(), + isLoad3dNode: vi.fn(() => false) +})) + +vi.mock('@/utils/nodeFilterUtil', () => ({ + filterOutputNodes: vi.fn() +})) + interface TestNodeConfig { type?: string mode?: LGraphEventMode @@ -22,163 +27,69 @@ interface TestNodeConfig { removable?: boolean } -interface TestNode { +class MockPositionable implements Positionable { + readonly id = 0 + readonly pos: [number, number] = [0, 0] + readonly boundingRect = [0, 0, 100, 100] as const type: string mode: LGraphEventMode flags?: { collapsed?: boolean } pinned?: boolean removable?: boolean - isSubgraphNode: () => boolean -} -type MockedItem = TestNode | { type: string; isNode: boolean } + constructor(config: TestNodeConfig = {}) { + this.type = config.type ?? 'TestNode' + this.mode = config.mode ?? LGraphEventMode.ALWAYS + this.flags = config.flags + this.pinned = config.pinned + this.removable = config.removable + } -// Mock all stores -vi.mock('@/renderer/core/canvas/canvasStore', () => ({ - useCanvasStore: vi.fn() -})) - -vi.mock('@/stores/nodeDefStore', () => ({ - useNodeDefStore: vi.fn() -})) - -vi.mock('@/stores/workspace/sidebarTabStore', () => ({ - useSidebarTabStore: vi.fn() -})) - -vi.mock('@/stores/workspace/nodeHelpStore', () => ({ - useNodeHelpStore: vi.fn() -})) - -vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({ - useNodeLibrarySidebarTab: vi.fn() -})) - -vi.mock('@/utils/litegraphUtil', () => ({ - isLGraphNode: vi.fn(), - isImageNode: vi.fn() -})) - -vi.mock('@/utils/nodeFilterUtil', () => ({ - filterOutputNodes: vi.fn() -})) - -const createTestNode = (config: TestNodeConfig = {}): TestNode => { - return { - type: config.type || 'TestNode', - mode: config.mode || LGraphEventMode.ALWAYS, - flags: config.flags, - pinned: config.pinned, - removable: config.removable, - isSubgraphNode: () => false + move(): void {} + snapToGrid(): boolean { + return false + } + isSubgraphNode(): boolean { + return false } } -// Mock comment/connection objects -const mockComment = { type: 'comment', isNode: false } -const mockConnection = { type: 'connection', isNode: false } +function createTestNode(config: TestNodeConfig = {}): MockPositionable { + return new MockPositionable(config) +} + +class MockNonNode implements Positionable { + readonly id = 0 + readonly pos: [number, number] = [0, 0] + readonly boundingRect = [0, 0, 100, 100] as const + readonly isNode = false + type: string + + constructor(type: string) { + this.type = type + } + + move(): void {} + snapToGrid(): boolean { + return false + } +} + +const mockComment = new MockNonNode('comment') +const mockConnection = new MockNonNode('connection') describe('useSelectionState', () => { - // Mock store instances - let mockSelectedItems: Ref - beforeEach(() => { vi.clearAllMocks() - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) - // Setup mock canvas store with proper ref - mockSelectedItems = ref([]) - vi.mocked(useCanvasStore).mockReturnValue({ - selectedItems: mockSelectedItems, - // Add minimal required properties for the store - $id: 'canvas', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $onAction: vi.fn(), - $dispose: vi.fn(), - _customProperties: new Set(), - _p: {} as any - } as any) - - // Setup mock node def store - vi.mocked(useNodeDefStore).mockReturnValue({ - fromLGraphNode: vi.fn((node: TestNode) => { - if (node?.type === 'TestNode') { - return { nodePath: 'test.TestNode', name: 'TestNode' } - } - return null - }), - // Add minimal required properties for the store - $id: 'nodeDef', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $onAction: vi.fn(), - $dispose: vi.fn(), - _customProperties: new Set(), - _p: {} as any - } as any) - - // Setup mock sidebar tab store - const mockToggleSidebarTab = vi.fn() - vi.mocked(useSidebarTabStore).mockReturnValue({ - activeSidebarTabId: null, - toggleSidebarTab: mockToggleSidebarTab, - // Add minimal required properties for the store - $id: 'sidebarTab', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $onAction: vi.fn(), - $dispose: vi.fn(), - _customProperties: new Set(), - _p: {} as any - } as any) - - // Setup mock node help store - const mockOpenHelp = vi.fn() - const mockCloseHelp = vi.fn() - const mockNodeHelpStore = { - isHelpOpen: false, - currentHelpNode: null, - openHelp: mockOpenHelp, - closeHelp: mockCloseHelp, - // Add minimal required properties for the store - $id: 'nodeHelp', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $onAction: vi.fn(), - $dispose: vi.fn(), - _customProperties: new Set(), - _p: {} as any - } - vi.mocked(useNodeHelpStore).mockReturnValue(mockNodeHelpStore as any) - - // Setup mock composables - vi.mocked(useNodeLibrarySidebarTab).mockReturnValue({ - id: 'node-library-tab', - title: 'Node Library', - type: 'custom', - render: () => null - } as any) - - // Setup mock utility functions vi.mocked(isLGraphNode).mockImplementation((item: unknown) => { - const typedItem = item as { isNode?: boolean } - return typedItem?.isNode !== false + if (typeof item !== 'object' || item === null) return false + return !('isNode' in item && item.isNode === false) }) - vi.mocked(isImageNode).mockImplementation((node: unknown) => { - const typedNode = node as { type?: string } - return typedNode?.type === 'ImageNode' - }) - vi.mocked(filterOutputNodes).mockImplementation( - (nodes: TestNode[]) => nodes.filter((n) => n.type === 'OutputNode') as any + vi.mocked(isImageNode).mockReturnValue(false) + vi.mocked(filterOutputNodes).mockImplementation((nodes) => + nodes.filter((n) => n.type === 'OutputNode') ) }) @@ -189,10 +100,10 @@ describe('useSelectionState', () => { }) test('should return true when items selected', () => { - // Update the mock data before creating the composable + const canvasStore = useCanvasStore() const node1 = createTestNode() const node2 = createTestNode() - mockSelectedItems.value = [node1, node2] + canvasStore.selectedItems.push(node1, node2) const { hasAnySelection } = useSelectionState() expect(hasAnySelection.value).toBe(true) @@ -201,9 +112,9 @@ describe('useSelectionState', () => { describe('Node Type Filtering', () => { test('should pick only LGraphNodes from mixed selections', () => { - // Update the mock data before creating the composable + const canvasStore = useCanvasStore() const graphNode = createTestNode() - mockSelectedItems.value = [graphNode, mockComment, mockConnection] + canvasStore.selectedItems.push(graphNode, mockComment, mockConnection) const { selectedNodes } = useSelectionState() expect(selectedNodes.value).toHaveLength(1) @@ -213,9 +124,9 @@ describe('useSelectionState', () => { describe('Node State Computation', () => { test('should detect bypassed nodes', () => { - // Update the mock data before creating the composable + const canvasStore = useCanvasStore() const bypassedNode = createTestNode({ mode: LGraphEventMode.BYPASS }) - mockSelectedItems.value = [bypassedNode] + canvasStore.selectedItems.push(bypassedNode) const { selectedNodes } = useSelectionState() const isBypassed = selectedNodes.value.some( @@ -225,10 +136,10 @@ describe('useSelectionState', () => { }) test('should detect pinned/collapsed states', () => { - // Update the mock data before creating the composable + const canvasStore = useCanvasStore() const pinnedNode = createTestNode({ pinned: true }) const collapsedNode = createTestNode({ flags: { collapsed: true } }) - mockSelectedItems.value = [pinnedNode, collapsedNode] + canvasStore.selectedItems.push(pinnedNode, collapsedNode) const { selectedNodes } = useSelectionState() const isPinned = selectedNodes.value.some((n) => n.pinned === true) @@ -244,9 +155,9 @@ describe('useSelectionState', () => { }) test('should provide non-reactive state computation', () => { - // Update the mock data before creating the composable + const canvasStore = useCanvasStore() const node = createTestNode({ pinned: true }) - mockSelectedItems.value = [node] + canvasStore.selectedItems.push(node) const { selectedNodes } = useSelectionState() const isPinned = selectedNodes.value.some((n) => n.pinned === true) @@ -261,8 +172,7 @@ describe('useSelectionState', () => { expect(isCollapsed).toBe(false) expect(isBypassed).toBe(false) - // Test with empty selection using new composable instance - mockSelectedItems.value = [] + canvasStore.selectedItems.length = 0 const { selectedNodes: newSelectedNodes } = useSelectionState() const newIsPinned = newSelectedNodes.value.some((n) => n.pinned === true) expect(newIsPinned).toBe(false) diff --git a/src/composables/maskeditor/useCanvasHistory.test.ts b/src/composables/maskeditor/useCanvasHistory.test.ts index 6281baf39..bf81196fa 100644 --- a/src/composables/maskeditor/useCanvasHistory.test.ts +++ b/src/composables/maskeditor/useCanvasHistory.test.ts @@ -67,7 +67,10 @@ vi.mock('@/stores/maskEditorStore', () => ({ // Mock ImageBitmap using safe global augmentation pattern if (typeof globalThis.ImageBitmap === 'undefined') { - globalThis.ImageBitmap = class ImageBitmap { + class MockImageBitmap implements Pick< + ImageBitmap, + 'width' | 'height' | 'close' + > { width: number height: number constructor(width = 100, height = 100) { @@ -75,7 +78,8 @@ if (typeof globalThis.ImageBitmap === 'undefined') { this.height = height } close() {} - } as unknown as typeof globalThis.ImageBitmap + } + Object.defineProperty(globalThis, 'ImageBitmap', { value: MockImageBitmap }) } describe('useCanvasHistory', () => { diff --git a/src/composables/maskeditor/useCanvasManager.test.ts b/src/composables/maskeditor/useCanvasManager.test.ts index 4fe40df6e..6ffe32f28 100644 --- a/src/composables/maskeditor/useCanvasManager.test.ts +++ b/src/composables/maskeditor/useCanvasManager.test.ts @@ -1,15 +1,85 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { MaskBlendMode } from '@/extensions/core/maskeditor/types' import { useCanvasManager } from '@/composables/maskeditor/useCanvasManager' -const mockStore = { - imgCanvas: null as any, - maskCanvas: null as any, - rgbCanvas: null as any, - imgCtx: null as any, - maskCtx: null as any, - rgbCtx: null as any, - canvasBackground: null as any, +import { MaskBlendMode } from '@/extensions/core/maskeditor/types' + +interface MockCanvasStyle { + mixBlendMode: string + opacity: string + backgroundColor: string +} + +interface MockCanvas { + width: number + height: number + style: Partial +} + +interface MockContext { + drawImage: ReturnType + getImageData?: ReturnType + putImageData?: ReturnType + globalCompositeOperation?: string + fillStyle?: string +} + +interface MockStore { + imgCanvas: MockCanvas | null + maskCanvas: MockCanvas | null + rgbCanvas: MockCanvas | null + imgCtx: MockContext | null + maskCtx: MockContext | null + rgbCtx: MockContext | null + canvasBackground: { style: Partial } | null + maskColor: { r: number; g: number; b: number } + maskBlendMode: MaskBlendMode + maskOpacity: number +} + +function getImgCanvas(): MockCanvas { + if (!mockStore.imgCanvas) throw new Error('imgCanvas not initialized') + return mockStore.imgCanvas +} + +function getMaskCanvas(): MockCanvas { + if (!mockStore.maskCanvas) throw new Error('maskCanvas not initialized') + return mockStore.maskCanvas +} + +function getRgbCanvas(): MockCanvas { + if (!mockStore.rgbCanvas) throw new Error('rgbCanvas not initialized') + return mockStore.rgbCanvas +} + +function getImgCtx(): MockContext { + if (!mockStore.imgCtx) throw new Error('imgCtx not initialized') + return mockStore.imgCtx +} + +function getMaskCtx(): MockContext { + if (!mockStore.maskCtx) throw new Error('maskCtx not initialized') + return mockStore.maskCtx +} + +function getRgbCtx(): MockContext { + if (!mockStore.rgbCtx) throw new Error('rgbCtx not initialized') + return mockStore.rgbCtx +} + +function getCanvasBackground(): { style: Partial } { + if (!mockStore.canvasBackground) + throw new Error('canvasBackground not initialized') + return mockStore.canvasBackground +} + +const mockStore: MockStore = { + imgCanvas: null, + maskCanvas: null, + rgbCanvas: null, + imgCtx: null, + maskCtx: null, + rgbCtx: null, + canvasBackground: null, maskColor: { r: 0, g: 0, b: 0 }, maskBlendMode: MaskBlendMode.Black, maskOpacity: 0.8 @@ -56,7 +126,8 @@ describe('useCanvasManager', () => { mockStore.imgCanvas = { width: 0, - height: 0 + height: 0, + style: {} } mockStore.maskCanvas = { @@ -70,7 +141,8 @@ describe('useCanvasManager', () => { mockStore.rgbCanvas = { width: 0, - height: 0 + height: 0, + style: {} } mockStore.canvasBackground = { @@ -93,12 +165,12 @@ describe('useCanvasManager', () => { await manager.invalidateCanvas(origImage, maskImage, null) - expect(mockStore.imgCanvas.width).toBe(512) - expect(mockStore.imgCanvas.height).toBe(512) - expect(mockStore.maskCanvas.width).toBe(512) - expect(mockStore.maskCanvas.height).toBe(512) - expect(mockStore.rgbCanvas.width).toBe(512) - expect(mockStore.rgbCanvas.height).toBe(512) + expect(getImgCanvas().width).toBe(512) + expect(getImgCanvas().height).toBe(512) + expect(getMaskCanvas().width).toBe(512) + expect(getMaskCanvas().height).toBe(512) + expect(getRgbCanvas().width).toBe(512) + expect(getRgbCanvas().height).toBe(512) }) it('should draw original image', async () => { @@ -109,7 +181,7 @@ describe('useCanvasManager', () => { await manager.invalidateCanvas(origImage, maskImage, null) - expect(mockStore.imgCtx.drawImage).toHaveBeenCalledWith( + expect(getImgCtx().drawImage).toHaveBeenCalledWith( origImage, 0, 0, @@ -127,7 +199,7 @@ describe('useCanvasManager', () => { await manager.invalidateCanvas(origImage, maskImage, paintImage) - expect(mockStore.rgbCtx.drawImage).toHaveBeenCalledWith( + expect(getRgbCtx().drawImage).toHaveBeenCalledWith( paintImage, 0, 0, @@ -144,7 +216,7 @@ describe('useCanvasManager', () => { await manager.invalidateCanvas(origImage, maskImage, null) - expect(mockStore.rgbCtx.drawImage).not.toHaveBeenCalled() + expect(getRgbCtx().drawImage).not.toHaveBeenCalled() }) it('should prepare mask', async () => { @@ -155,9 +227,9 @@ describe('useCanvasManager', () => { await manager.invalidateCanvas(origImage, maskImage, null) - expect(mockStore.maskCtx.drawImage).toHaveBeenCalled() - expect(mockStore.maskCtx.getImageData).toHaveBeenCalled() - expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() + expect(getMaskCtx().drawImage).toHaveBeenCalled() + expect(getMaskCtx().getImageData).toHaveBeenCalled() + expect(getMaskCtx().putImageData).toHaveBeenCalled() }) it('should throw error when canvas missing', async () => { @@ -196,12 +268,10 @@ describe('useCanvasManager', () => { await manager.updateMaskColor() - expect(mockStore.maskCtx.fillStyle).toBe('rgb(0, 0, 0)') - expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial') - expect(mockStore.maskCanvas.style.opacity).toBe('0.8') - expect(mockStore.canvasBackground.style.backgroundColor).toBe( - 'rgba(0,0,0,1)' - ) + expect(getMaskCtx().fillStyle).toBe('rgb(0, 0, 0)') + expect(getMaskCanvas().style.mixBlendMode).toBe('initial') + expect(getMaskCanvas().style.opacity).toBe('0.8') + expect(getCanvasBackground().style.backgroundColor).toBe('rgba(0,0,0,1)') }) it('should update mask color for white blend mode', async () => { @@ -212,9 +282,9 @@ describe('useCanvasManager', () => { await manager.updateMaskColor() - expect(mockStore.maskCtx.fillStyle).toBe('rgb(255, 255, 255)') - expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial') - expect(mockStore.canvasBackground.style.backgroundColor).toBe( + expect(getMaskCtx().fillStyle).toBe('rgb(255, 255, 255)') + expect(getMaskCanvas().style.mixBlendMode).toBe('initial') + expect(getCanvasBackground().style.backgroundColor).toBe( 'rgba(255,255,255,1)' ) }) @@ -227,9 +297,9 @@ describe('useCanvasManager', () => { await manager.updateMaskColor() - expect(mockStore.maskCanvas.style.mixBlendMode).toBe('difference') - expect(mockStore.maskCanvas.style.opacity).toBe('1') - expect(mockStore.canvasBackground.style.backgroundColor).toBe( + expect(getMaskCanvas().style.mixBlendMode).toBe('difference') + expect(getMaskCanvas().style.opacity).toBe('1') + expect(getCanvasBackground().style.backgroundColor).toBe( 'rgba(255,255,255,1)' ) }) @@ -238,8 +308,8 @@ describe('useCanvasManager', () => { const manager = useCanvasManager() mockStore.maskColor = { r: 128, g: 64, b: 32 } - mockStore.maskCanvas.width = 100 - mockStore.maskCanvas.height = 100 + getMaskCanvas().width = 100 + getMaskCanvas().height = 100 await manager.updateMaskColor() @@ -249,7 +319,7 @@ describe('useCanvasManager', () => { expect(mockImageData.data[i + 2]).toBe(32) } - expect(mockStore.maskCtx.putImageData).toHaveBeenCalledWith( + expect(getMaskCtx().putImageData).toHaveBeenCalledWith( mockImageData, 0, 0 @@ -258,22 +328,24 @@ describe('useCanvasManager', () => { it('should return early when canvas missing', async () => { const manager = useCanvasManager() + const maskCtxBeforeNull = getMaskCtx() mockStore.maskCanvas = null await manager.updateMaskColor() - expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled() + expect(maskCtxBeforeNull.getImageData).not.toHaveBeenCalled() }) it('should return early when context missing', async () => { const manager = useCanvasManager() + const canvasBgBeforeNull = getCanvasBackground() mockStore.maskCtx = null await manager.updateMaskColor() - expect(mockStore.canvasBackground.style.backgroundColor).toBe('') + expect(canvasBgBeforeNull.style.backgroundColor).toBe('') }) it('should handle different opacity values', async () => { @@ -283,7 +355,7 @@ describe('useCanvasManager', () => { await manager.updateMaskColor() - expect(mockStore.maskCanvas.style.opacity).toBe('0.5') + expect(getMaskCanvas().style.opacity).toBe('0.5') }) }) @@ -330,7 +402,7 @@ describe('useCanvasManager', () => { await manager.invalidateCanvas(origImage, maskImage, null) - expect(mockStore.maskCtx.globalCompositeOperation).toBe('source-over') + expect(getMaskCtx().globalCompositeOperation).toBe('source-over') }) }) }) diff --git a/src/composables/maskeditor/useCanvasTransform.test.ts b/src/composables/maskeditor/useCanvasTransform.test.ts index 95c153d29..aaee9e2e2 100644 --- a/src/composables/maskeditor/useCanvasTransform.test.ts +++ b/src/composables/maskeditor/useCanvasTransform.test.ts @@ -63,7 +63,7 @@ vi.mock('@/stores/maskEditorStore', () => ({ // Mock ImageData with improved type safety if (typeof globalThis.ImageData === 'undefined') { - globalThis.ImageData = class ImageData { + class MockImageData { data: Uint8ClampedArray width: number height: number @@ -95,12 +95,16 @@ if (typeof globalThis.ImageData === 'undefined') { this.data = new Uint8ClampedArray(dataOrWidth * widthOrHeight * 4) } } - } as unknown as typeof globalThis.ImageData + } + Object.defineProperty(globalThis, 'ImageData', { value: MockImageData }) } // Mock ImageBitmap for test environment using safe type casting if (typeof globalThis.ImageBitmap === 'undefined') { - globalThis.ImageBitmap = class ImageBitmap { + class MockImageBitmap implements Pick< + ImageBitmap, + 'width' | 'height' | 'close' + > { width: number height: number constructor(width = 100, height = 100) { @@ -108,7 +112,8 @@ if (typeof globalThis.ImageBitmap === 'undefined') { this.height = height } close() {} - } as unknown as typeof globalThis.ImageBitmap + } + Object.defineProperty(globalThis, 'ImageBitmap', { value: MockImageBitmap }) } describe('useCanvasTransform', () => { diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts index 4e1ac3503..d48e9e114 100644 --- a/src/composables/usePaste.test.ts +++ b/src/composables/usePaste.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { LGraphCanvas, - LGraph, LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph' @@ -10,11 +9,19 @@ import { app } from '@/scripts/app' import { isImageNode } from '@/utils/litegraphUtil' import { pasteImageNode, usePaste } from './usePaste' -function createMockNode() { +interface MockPasteNode { + pos: [number, number] + pasteFile: (file: File) => void + pasteFiles: (files: File[]) => void + is_selected?: boolean +} + +function createMockNode(options?: Partial): MockPasteNode { return { pos: [0, 0], - pasteFile: vi.fn(), - pasteFiles: vi.fn() + pasteFile: vi.fn<(file: File) => void>(), + pasteFiles: vi.fn<(files: File[]) => void>(), + ...options } } @@ -38,16 +45,31 @@ function createDataTransfer(files: File[] = []): DataTransfer { return dataTransfer } -const mockCanvas = { - current_node: null as LGraphNode | null, - graph: { - add: vi.fn(), - change: vi.fn() - } as Partial as LGraph, +interface MockGraph { + add: ReturnType + change: ReturnType +} + +interface MockCanvas { + current_node: LGraphNode | null + graph: MockGraph + graph_mouse: [number, number] + pasteFromClipboard: ReturnType + _deserializeItems: ReturnType +} + +const mockGraph: MockGraph = { + add: vi.fn(), + change: vi.fn() +} + +const mockCanvas: MockCanvas = { + current_node: null, + graph: mockGraph, graph_mouse: [100, 200], pasteFromClipboard: vi.fn(), _deserializeItems: vi.fn() -} as Partial as LGraphCanvas +} const mockCanvasStore = { canvas: mockCanvas, @@ -81,7 +103,7 @@ vi.mock('@/scripts/app', () => ({ vi.mock('@/lib/litegraph/src/litegraph', () => ({ LiteGraph: { - createNode: vi.fn() + createNode: vi.fn<(type: string) => LGraphNode | undefined>() } })) @@ -95,30 +117,38 @@ vi.mock('@/workbench/eventHelpers', () => ({ shouldIgnoreCopyPaste: vi.fn() })) +function asLGraphCanvas(canvas: MockCanvas): LGraphCanvas { + return Object.assign(Object.create(null), canvas) +} + +function asLGraphNode(node: MockPasteNode): LGraphNode { + return Object.assign(Object.create(null), node) +} + describe('pasteImageNode', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(mockCanvas.graph!.add).mockImplementation( - (node: LGraphNode | LGraphGroup) => node as LGraphNode - ) + mockGraph.add.mockImplementation((node: LGraphNode | LGraphGroup) => node) }) 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 createdNode = asLGraphNode(mockNode) + vi.mocked(LiteGraph.createNode).mockReturnValue(createdNode) const file = createImageFile() const dataTransfer = createDataTransfer([file]) - pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items) + pasteImageNode(asLGraphCanvas(mockCanvas), 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) + // Verify pos was set on the created node (not on mockNode since Object.assign copies) + expect(createdNode.pos).toEqual([100, 200]) + expect(mockGraph.add).toHaveBeenCalled() + expect(mockGraph.change).toHaveBeenCalled() + // pasteFile was called on the node returned by graph.add + const addedNode = mockGraph.add.mock.results[0].value + expect(addedNode.pasteFile).toHaveBeenCalledWith(file) }) it('should use existing image node when provided', () => { @@ -126,11 +156,7 @@ describe('pasteImageNode', () => { const file = createImageFile() const dataTransfer = createDataTransfer([file]) - pasteImageNode( - mockCanvas as unknown as LGraphCanvas, - dataTransfer.items, - mockNode as unknown as LGraphNode - ) + pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode) expect(mockNode.pasteFile).toHaveBeenCalledWith(file) expect(mockNode.pasteFiles).toHaveBeenCalledWith([file]) @@ -142,11 +168,7 @@ describe('pasteImageNode', () => { 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 - ) + pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode) expect(mockNode.pasteFile).toHaveBeenCalledWith(file1) expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2]) @@ -156,11 +178,7 @@ describe('pasteImageNode', () => { const mockNode = createMockNode() const dataTransfer = createDataTransfer() - pasteImageNode( - mockCanvas as unknown as LGraphCanvas, - dataTransfer.items, - mockNode as unknown as LGraphNode - ) + pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode) expect(mockNode.pasteFile).not.toHaveBeenCalled() expect(mockNode.pasteFiles).not.toHaveBeenCalled() @@ -172,11 +190,7 @@ describe('pasteImageNode', () => { 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 - ) + pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode) expect(mockNode.pasteFile).toHaveBeenCalledWith(imageFile) expect(mockNode.pasteFiles).toHaveBeenCalledWith([imageFile]) @@ -188,16 +202,12 @@ describe('usePaste', () => { vi.clearAllMocks() mockCanvas.current_node = null mockWorkspaceStore.shiftDown = false - vi.mocked(mockCanvas.graph!.add).mockImplementation( - (node: LGraphNode | LGraphGroup) => node as LGraphNode - ) + mockGraph.add.mockImplementation((node: LGraphNode | LGraphGroup) => node) }) it('should handle image paste', async () => { const mockNode = createMockNode() - vi.mocked(LiteGraph.createNode).mockReturnValue( - mockNode as unknown as LGraphNode - ) + vi.mocked(LiteGraph.createNode).mockReturnValue(asLGraphNode(mockNode)) usePaste() @@ -214,9 +224,7 @@ describe('usePaste', () => { it('should handle audio paste', async () => { const mockNode = createMockNode() - vi.mocked(LiteGraph.createNode).mockReturnValue( - mockNode as unknown as LGraphNode - ) + vi.mocked(LiteGraph.createNode).mockReturnValue(asLGraphNode(mockNode)) usePaste() @@ -261,12 +269,8 @@ describe('usePaste', () => { }) 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 + const mockNode = createMockNode({ is_selected: true }) + mockCanvas.current_node = asLGraphNode(mockNode) vi.mocked(isImageNode).mockReturnValue(true) usePaste() diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index 1809eb838..5a0620275 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -9,6 +9,12 @@ import { useWorkspaceStore } from '@/stores/workspaceStore' import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil' import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers' +/** A node that supports pasting files */ +interface PasteableNode { + pasteFile?(file: File): void + pasteFiles?(files: File[]): void +} + function pasteClipboardItems(data: DataTransfer): boolean { const rawData = data.getData('text/html') const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1] @@ -28,7 +34,7 @@ function pasteClipboardItems(data: DataTransfer): boolean { function pasteItemsOnNode( items: DataTransferItemList, - node: LGraphNode | null, + node: PasteableNode | null, contentType: string ): void { if (!node) return @@ -51,7 +57,7 @@ function pasteItemsOnNode( export function pasteImageNode( canvas: LGraphCanvas, items: DataTransferItemList, - imageNode: LGraphNode | null = null + imageNode: PasteableNode | null = null ): void { const { graph, diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index d87967434..c515a0456 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -1764,9 +1764,10 @@ export class GroupNodeHandler { static getGroupData( node: LGraphNodeConstructor ): GroupNodeConfig | undefined + static getGroupData(node: typeof LGraphNode): GroupNodeConfig | undefined static getGroupData(node: LGraphNode): GroupNodeConfig | undefined static getGroupData( - node: LGraphNode | LGraphNodeConstructor + node: LGraphNode | LGraphNodeConstructor | typeof LGraphNode ): GroupNodeConfig | undefined { // Check if this is a constructor (function) or an instance if (typeof node === 'function') { diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 8a0f77f86..ca73f5357 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -1,12 +1,11 @@ import { merge } from 'es-toolkit' import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants' -import { - type GroupNodeWorkflowData, - type LGraphNode, - type LGraphNodeConstructor, - LiteGraph +import type { + GroupNodeWorkflowData, + LGraphNode } from '@/lib/litegraph/src/litegraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { useToastStore } from '@/platform/updates/common/toastStore' import { type ComfyApp, app } from '../../scripts/app' @@ -16,6 +15,19 @@ import { DraggableList } from '../../scripts/ui/draggableList' import { GroupNodeConfig, GroupNodeHandler } from './groupNode' import './groupNodeManage.css' +/** Group node types have nodeData of type GroupNodeWorkflowData */ +interface GroupNodeType { + nodeData: GroupNodeWorkflowData +} + +type GroupNodeConstructor = typeof LGraphNode & GroupNodeType + +function hasNodeData( + nodeType: typeof LGraphNode | undefined +): nodeType is GroupNodeConstructor { + return nodeType != null && 'nodeData' in nodeType +} + const ORDER: unique symbol = Symbol('ORDER') interface NodeModification { @@ -49,7 +61,7 @@ export class ManageGroupDialog extends ComfyDialog { {} nodeItems: HTMLLIElement[] = [] app: ComfyApp - groupNodeType!: LGraphNodeConstructor + groupNodeType!: GroupNodeConstructor groupNodeDef: unknown groupData: ReturnType | null = null @@ -104,9 +116,14 @@ export class ManageGroupDialog extends ComfyDialog { } getGroupData() { - this.groupNodeType = LiteGraph.registered_node_types[ - `${PREFIX}${SEPARATOR}` + this.selectedGroup - ] as unknown as LGraphNodeConstructor + const nodeType = + LiteGraph.registered_node_types[ + `${PREFIX}${SEPARATOR}` + this.selectedGroup + ] + if (!hasNodeData(nodeType)) { + throw new Error(`Group node type not found: ${this.selectedGroup}`) + } + this.groupNodeType = nodeType this.groupNodeDef = this.groupNodeType.nodeData this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType) } diff --git a/src/extensions/core/uploadAudio.ts b/src/extensions/core/uploadAudio.ts index 75e45514d..83cd31cd4 100644 --- a/src/extensions/core/uploadAudio.ts +++ b/src/extensions/core/uploadAudio.ts @@ -1,3 +1,4 @@ +import type { IMediaRecorder } from 'extendable-media-recorder' import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recorder' import { useChainCallback } from '@/composables/functional/useChainCallback' @@ -256,7 +257,7 @@ app.registerExtension({ node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio) audioUIWidget.options.canvasOnly = false - let mediaRecorder: MediaRecorder | null = null + let mediaRecorder: IMediaRecorder | null = null let isRecording = false let audioChunks: Blob[] = [] let currentStream: MediaStream | null = null @@ -301,7 +302,7 @@ app.registerExtension({ mediaRecorder = new ExtendableMediaRecorder(currentStream, { mimeType: 'audio/wav' - }) as unknown as MediaRecorder + }) audioChunks = [] diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 01ed65566..d478412f2 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -148,7 +148,8 @@ export class LGraph 'widgets', 'inputNode', 'outputNode', - 'extra' + 'extra', + 'version' ]) id: UUID = zeroUuid @@ -701,14 +702,20 @@ export class LGraph // sort now by priority L.sort(function (A, B) { - const ctorA = A.constructor as { priority?: number } - const ctorB = B.constructor as { priority?: number } - const nodeA = A as unknown as { priority?: number } - const nodeB = B as unknown as { priority?: number } - const Ap = ctorA.priority || nodeA.priority || 0 - const Bp = ctorB.priority || nodeB.priority || 0 + const ctorA = A.constructor + const ctorB = B.constructor + const priorityA = + ('priority' in ctorA && typeof ctorA.priority === 'number' + ? ctorA.priority + : 0) || + ('priority' in A && typeof A.priority === 'number' ? A.priority : 0) + const priorityB = + ('priority' in ctorB && typeof ctorB.priority === 'number' + ? ctorB.priority + : 0) || + ('priority' in B && typeof B.priority === 'number' ? B.priority : 0) // if same priority, sort by order - return Ap == Bp ? A.order - B.order : Ap - Bp + return priorityA == priorityB ? A.order - B.order : priorityA - priorityB }) // save order number in the node, again... @@ -798,12 +805,9 @@ export class LGraph if (!nodes) return for (const node of nodes) { - const nodeRecord = node as unknown as Record< - string, - ((...args: unknown[]) => void) | undefined - > - const handler = nodeRecord[eventname] - if (!handler || node.mode != mode) continue + if (!(eventname in node) || node.mode != mode) continue + const handler = node[eventname as keyof typeof node] + if (typeof handler !== 'function') continue if (params === undefined) { handler.call(node) } else if (params && params.constructor === Array) { @@ -2197,7 +2201,7 @@ export class LGraph } protected _configureBase(data: ISerialisedGraph | SerialisableGraph): void { - const { id, extra } = data + const { id, extra, version } = data // Create a new graph ID if none is provided if (id) { @@ -2206,6 +2210,11 @@ export class LGraph this.id = createUuidv4() } + // Store the schema version from loaded data + if (typeof version === 'number') { + this.version = version + } + // Extra this.extra = extra ? structuredClone(extra) : {} @@ -2299,11 +2308,14 @@ export class LGraph const nodesData = data.nodes // copy all stored fields (legacy property assignment) - const thisRecord = this as unknown as Record - const dataRecord = data as unknown as Record - for (const i in dataRecord) { - if (LGraph.ConfigureProperties.has(i)) continue - thisRecord[i] = dataRecord[i] + // Unknown properties are stored in `extra` for backwards compat + for (const key in data) { + if (LGraph.ConfigureProperties.has(key)) continue + if (key in this) continue // Skip known properties + const value = data[key as keyof typeof data] + if (value !== undefined) { + this.extra[key] = value + } } // Subgraph definitions diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 62d5bdd20..d281aad66 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -833,10 +833,7 @@ export class LGraphCanvas implements CustomEventDispatcher if ('shiftKey' in e && e.shiftKey) { if (this.allow_searchbox) { - this.showSearchBox( - e as unknown as MouseEvent, - linkReleaseContext as IShowSearchOptions - ) + this.showSearchBox(e, linkReleaseContext as IShowSearchOptions) } } else if (this.linkConnector.state.connectingTo === 'input') { this.showConnectionMenu({ @@ -1385,7 +1382,7 @@ export class LGraphCanvas implements CustomEventDispatcher _menu: ContextMenu, node: LGraphNode ): void { - const property = item.property || 'title' + const property: keyof LGraphNode = item.property || 'title' const value = node[property] const title = document.createElement('span') @@ -1479,8 +1476,13 @@ export class LGraphCanvas implements CustomEventDispatcher } else if (item.type == 'Boolean') { value = Boolean(value) } - // Dynamic property assignment for user-defined node properties - ;(node as unknown as Record)[property] = value + // Set the node property - property is validated as keyof LGraphNode + if (property === 'title' && typeof value === 'string') { + node.title = value + } else if (property in node) { + // For other properties, use the properties bag + node.properties[property as string] = value + } dialog.remove() canvas.setDirty(true, true) } diff --git a/src/stores/workspace/searchBoxStore.ts b/src/stores/workspace/searchBoxStore.ts index 73ee3238f..a8711b7e1 100644 --- a/src/stores/workspace/searchBoxStore.ts +++ b/src/stores/workspace/searchBoxStore.ts @@ -6,6 +6,22 @@ import type NodeSearchBoxPopover from '@/components/searchbox/NodeSearchBoxPopov import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' +function createSyntheticCanvasPointerEvent( + clientX: number, + clientY: number +): CanvasPointerEvent { + const event = new PointerEvent('click', { clientX, clientY }) + return Object.assign(event, { + layerY: clientY, + canvasX: clientX, + canvasY: clientY, + deltaX: 0, + deltaY: 0, + safeOffsetX: clientX, + safeOffsetY: clientY + }) as CanvasPointerEvent +} + export const useSearchBoxStore = defineStore('searchBox', () => { const settingStore = useSettingStore() const { x, y } = useMouse() @@ -31,11 +47,8 @@ export const useSearchBoxStore = defineStore('searchBox', () => { return } if (!popoverRef.value) return - const event = Object.assign( - new MouseEvent('click', { clientX: x.value, clientY: y.value }), - { layerY: y.value } - ) - popoverRef.value.showSearchBox(event as unknown as CanvasPointerEvent) + const event = createSyntheticCanvasPointerEvent(x.value, y.value) + popoverRef.value.showSearchBox(event) } return {