mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 01:50:08 +00:00
fix: remove @ts-expect-error suppressions with proper type guards
This commit is contained in:
@@ -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<typeof vi.fn>
|
||||
getFormFactor: () => string
|
||||
}
|
||||
|
||||
function createMockSystemStats(
|
||||
overrides: Partial<SystemStats['system']> = {}
|
||||
): 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<typeof useSystemStatsStore>
|
||||
)
|
||||
@@ -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')]
|
||||
|
||||
@@ -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<typeof ContextMenu> | 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<InstanceType<typeof ContextMenu>>()
|
||||
const colorPickerMenu = ref<InstanceType<typeof ColorPickerMenu>>()
|
||||
const contextMenu =
|
||||
useTemplateRef<InstanceType<typeof ContextMenu>>('contextMenu')
|
||||
const colorPickerMenu =
|
||||
useTemplateRef<InstanceType<typeof ColorPickerMenu>>('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()
|
||||
|
||||
@@ -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<typeof CurrentUserButton>
|
||||
|
||||
// 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<typeof CurrentUserButton>
|
||||
>
|
||||
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<typeof CurrentUserButton>
|
||||
>
|
||||
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()
|
||||
|
||||
@@ -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<typeof useCanvasStore>
|
||||
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 }
|
||||
|
||||
|
||||
@@ -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<MockedItem[]>
|
||||
|
||||
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)
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<MockCanvasStyle>
|
||||
}
|
||||
|
||||
interface MockContext {
|
||||
drawImage: ReturnType<typeof vi.fn>
|
||||
getImageData?: ReturnType<typeof vi.fn>
|
||||
putImageData?: ReturnType<typeof vi.fn>
|
||||
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<MockCanvasStyle> } | 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<MockCanvasStyle> } {
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>): 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<LGraph> as LGraph,
|
||||
interface MockGraph {
|
||||
add: ReturnType<typeof vi.fn>
|
||||
change: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
interface MockCanvas {
|
||||
current_node: LGraphNode | null
|
||||
graph: MockGraph
|
||||
graph_mouse: [number, number]
|
||||
pasteFromClipboard: ReturnType<typeof vi.fn>
|
||||
_deserializeItems: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
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<LGraphCanvas> 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<LGraphNode> as LGraphNode
|
||||
mockCanvas.current_node = mockNode
|
||||
const mockNode = createMockNode({ is_selected: true })
|
||||
mockCanvas.current_node = asLGraphNode(mockNode)
|
||||
vi.mocked(isImageNode).mockReturnValue(true)
|
||||
|
||||
usePaste()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1764,9 +1764,10 @@ export class GroupNodeHandler {
|
||||
static getGroupData(
|
||||
node: LGraphNodeConstructor<LGraphNode>
|
||||
): GroupNodeConfig | undefined
|
||||
static getGroupData(node: typeof LGraphNode): GroupNodeConfig | undefined
|
||||
static getGroupData(node: LGraphNode): GroupNodeConfig | undefined
|
||||
static getGroupData(
|
||||
node: LGraphNode | LGraphNodeConstructor<LGraphNode>
|
||||
node: LGraphNode | LGraphNodeConstructor<LGraphNode> | typeof LGraphNode
|
||||
): GroupNodeConfig | undefined {
|
||||
// Check if this is a constructor (function) or an instance
|
||||
if (typeof node === 'function') {
|
||||
|
||||
@@ -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<HTMLDialogElement> {
|
||||
{}
|
||||
nodeItems: HTMLLIElement[] = []
|
||||
app: ComfyApp
|
||||
groupNodeType!: LGraphNodeConstructor<LGraphNode>
|
||||
groupNodeType!: GroupNodeConstructor
|
||||
groupNodeDef: unknown
|
||||
groupData: ReturnType<typeof GroupNodeHandler.getGroupData> | null = null
|
||||
|
||||
@@ -104,9 +116,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
}
|
||||
|
||||
getGroupData() {
|
||||
this.groupNodeType = LiteGraph.registered_node_types[
|
||||
`${PREFIX}${SEPARATOR}` + this.selectedGroup
|
||||
] as unknown as LGraphNodeConstructor<LGraphNode>
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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<string, unknown>
|
||||
const dataRecord = data as unknown as Record<string, unknown>
|
||||
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
|
||||
|
||||
@@ -833,10 +833,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
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<LGraphCanvasEventMap>
|
||||
_menu: ContextMenu<string>,
|
||||
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<LGraphCanvasEventMap>
|
||||
} else if (item.type == 'Boolean') {
|
||||
value = Boolean(value)
|
||||
}
|
||||
// Dynamic property assignment for user-defined node properties
|
||||
;(node as unknown as Record<string, NodeProperty>)[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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user