fix: remove @ts-expect-error suppressions with proper type guards

This commit is contained in:
DrJKL
2026-01-11 23:54:49 -08:00
parent cf6637965d
commit 168af5310b
16 changed files with 490 additions and 387 deletions

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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', () => {

View File

@@ -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')
})
})
})

View File

@@ -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', () => {

View File

@@ -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()

View File

@@ -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,