refactor: eliminate unsafe type assertions from Group 2 test files (#8258)

## Summary
Improved type safety in test files by eliminating unsafe type assertions
and adopting official testing patterns. Reduced unsafe `as unknown as`
type assertions and eliminated all `null!` assertions.

## Changes
- **Adopted @pinia/testing patterns**
- Replaced manual Pinia store mocking with `createTestingPinia()` in
`useSelectionState.test.ts`
  - Eliminated ~120 lines of mock boilerplate
- Created `createMockSettingStore()` helper to replace duplicated store
mocks in `useCoreCommands.test.ts`

- **Eliminated unsafe null assertions**
- Created explicit `MockMaskEditorStore` interface with proper nullable
types in `useCanvasTools.test.ts`
- Replaced `null!` initializations with `null` and used `!` at point of
use or `?.` for optional chaining

- **Made partial mock intent explicit**
- Updated test utilities in `litegraphTestUtils.ts` to use explicit
`Partial<T>` typing
- Changed cast pattern from `as T` to `as Partial<T> as T` to show
incomplete mock intent
- Applied to `createMockLGraphNode()`, `createMockPositionable()`, and
`createMockLGraphGroup()`

- **Created centralized mock utilities** in
`src/utils/__tests__/litegraphTestUtils.ts`
- `createMockLGraphNode()`, `createMockPositionable()`,
`createMockLGraphGroup()`, `createMockSubgraphNode()`
  - Updated 8+ test files to use centralized utilities
- Used union types `Partial<T> | Record<string, unknown>` for flexible
mock creation

## Results
-  0 typecheck errors
-  0 lint errors  
-  All tests passing in modified files
-  Eliminated all `null!` assertions
-  Reduced unsafe double-cast patterns significantly

## Files Modified (18)
- `src/components/graph/SelectionToolbox.test.ts`
-
`src/components/graph/selectionToolbox/{BypassButton,ColorPickerButton,ExecuteButton}.test.ts`
- `src/components/sidebar/tabs/queue/ResultGallery.test.ts`
- `src/composables/canvas/useSelectedLiteGraphItems.test.ts`
- `src/composables/graph/{useGraphHierarchy,useSelectionState}.test.ts`
-
`src/composables/maskeditor/{useCanvasHistory,useCanvasManager,useCanvasTools,useCanvasTransform}.test.ts`
- `src/composables/node/{useNodePricing,useWatchWidget}.test.ts`
- `src/composables/{useBrowserTabTitle,useCoreCommands}.test.ts`
- `src/utils/__tests__/litegraphTestUtils.ts`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8258-refactor-eliminate-unsafe-type-assertions-from-Group-2-test-files-2f16d73d365081549c65fd546cc7c765)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
This commit is contained in:
Johnpaul Chiwetelu
2026-01-24 05:10:35 +01:00
committed by GitHub
parent 6b6b467e68
commit b1d8bf0b13
24 changed files with 785 additions and 658 deletions

View File

@@ -1,23 +1,37 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
import * as measure from '@/lib/litegraph/src/measure'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import {
createMockLGraphNode,
createMockLGraphGroup
} from '@/utils/__tests__/litegraphTestUtils'
import { useGraphHierarchy } from './useGraphHierarchy'
vi.mock('@/renderer/core/canvas/canvasStore')
function createMockNode(overrides: Partial<LGraphNode> = {}): LGraphNode {
return {
...createMockLGraphNode(),
boundingRect: new Rectangle(100, 100, 50, 50),
...overrides
} as LGraphNode
}
function createMockGroup(overrides: Partial<LGraphGroup> = {}): LGraphGroup {
return createMockLGraphGroup(overrides)
}
describe('useGraphHierarchy', () => {
let mockCanvasStore: ReturnType<typeof useCanvasStore>
let mockCanvasStore: Partial<ReturnType<typeof useCanvasStore>>
let mockNode: LGraphNode
let mockGroups: LGraphGroup[]
beforeEach(() => {
mockNode = {
boundingRect: [100, 100, 50, 50]
} as unknown as LGraphNode
mockNode = createMockNode()
mockGroups = []
mockCanvasStore = {
@@ -25,10 +39,21 @@ describe('useGraphHierarchy', () => {
graph: {
groups: mockGroups
}
}
} as any
},
$id: 'canvas',
$state: {},
$patch: vi.fn(),
$reset: vi.fn(),
$subscribe: vi.fn(),
$onAction: vi.fn(),
$dispose: vi.fn(),
_customProperties: new Set(),
_p: {}
} as unknown as Partial<ReturnType<typeof useCanvasStore>>
vi.mocked(useCanvasStore).mockReturnValue(mockCanvasStore)
vi.mocked(useCanvasStore).mockReturnValue(
mockCanvasStore as ReturnType<typeof useCanvasStore>
)
})
describe('findParentGroup', () => {
@@ -41,9 +66,9 @@ describe('useGraphHierarchy', () => {
})
it('returns null when node is not in any group', () => {
const group = {
boundingRect: [0, 0, 50, 50]
} as unknown as LGraphGroup
const group = createMockGroup({
boundingRect: new Rectangle(0, 0, 50, 50)
})
mockGroups.push(group)
vi.spyOn(measure, 'containsCentre').mockReturnValue(false)
@@ -55,9 +80,9 @@ describe('useGraphHierarchy', () => {
})
it('returns the only group when node is in exactly one group', () => {
const group = {
boundingRect: [0, 0, 200, 200]
} as unknown as LGraphGroup
const group = createMockGroup({
boundingRect: new Rectangle(0, 0, 200, 200)
})
mockGroups.push(group)
vi.spyOn(measure, 'containsCentre').mockReturnValue(true)
@@ -69,12 +94,12 @@ describe('useGraphHierarchy', () => {
})
it('returns the smallest group when node is in multiple groups', () => {
const largeGroup = {
boundingRect: [0, 0, 300, 300]
} as unknown as LGraphGroup
const smallGroup = {
boundingRect: [50, 50, 100, 100]
} as unknown as LGraphGroup
const largeGroup = createMockGroup({
boundingRect: new Rectangle(0, 0, 300, 300)
})
const smallGroup = createMockGroup({
boundingRect: new Rectangle(50, 50, 100, 100)
})
mockGroups.push(largeGroup, smallGroup)
vi.spyOn(measure, 'containsCentre').mockReturnValue(true)
@@ -87,12 +112,12 @@ describe('useGraphHierarchy', () => {
})
it('returns the inner group when one group contains another', () => {
const outerGroup = {
boundingRect: [0, 0, 300, 300]
} as unknown as LGraphGroup
const innerGroup = {
boundingRect: [50, 50, 100, 100]
} as unknown as LGraphGroup
const outerGroup = createMockGroup({
boundingRect: new Rectangle(0, 0, 300, 300)
})
const innerGroup = createMockGroup({
boundingRect: new Rectangle(50, 50, 100, 100)
})
mockGroups.push(outerGroup, innerGroup)
vi.spyOn(measure, 'containsCentre').mockReturnValue(true)
@@ -113,7 +138,7 @@ describe('useGraphHierarchy', () => {
})
it('handles null canvas gracefully', () => {
mockCanvasStore.canvas = null as any
mockCanvasStore.canvas = null
const { findParentGroup } = useGraphHierarchy()
const result = findParentGroup(mockNode)
@@ -122,7 +147,7 @@ describe('useGraphHierarchy', () => {
})
it('handles null graph gracefully', () => {
mockCanvasStore.canvas!.graph = null as any
mockCanvasStore.canvas!.graph = null
const { findParentGroup } = useGraphHierarchy()
const result = findParentGroup(mockNode)

View File

@@ -1,55 +1,19 @@
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 { 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'
import {
createMockLGraphNode,
createMockPositionable
} from '@/utils/__tests__/litegraphTestUtils'
// Test interfaces
interface TestNodeConfig {
type?: string
mode?: LGraphEventMode
flags?: { collapsed?: boolean }
pinned?: boolean
removable?: boolean
}
interface TestNode {
type: string
mode: LGraphEventMode
flags?: { collapsed?: boolean }
pinned?: boolean
removable?: boolean
isSubgraphNode: () => boolean
}
type MockedItem = TestNode | { type: string; isNode: boolean }
// 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()
}))
// Mock composables
vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({
useNodeLibrarySidebarTab: vi.fn()
}))
@@ -63,102 +27,28 @@ 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
}
// Mock comment/connection objects with additional properties
const mockComment = {
...createMockPositionable({ id: 999 }),
type: 'comment',
isNode: false
}
const mockConnection = {
...createMockPositionable({ id: 1000 }),
type: 'connection',
isNode: false
}
// Mock comment/connection objects
const mockComment = { type: 'comment', isNode: false }
const mockConnection = { type: 'connection', isNode: false }
describe('useSelectionState', () => {
// Mock store instances
let mockSelectedItems: Ref<MockedItem[]>
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
// 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)
// Create testing Pinia instance
setActivePinia(
createTestingPinia({
createSpy: vi.fn
})
)
// Setup mock composables
vi.mocked(useNodeLibrarySidebarTab).mockReturnValue({
@@ -166,7 +56,7 @@ describe('useSelectionState', () => {
title: 'Node Library',
type: 'custom',
render: () => null
} as any)
} as ReturnType<typeof useNodeLibrarySidebarTab>)
// Setup mock utility functions
vi.mocked(isLGraphNode).mockImplementation((item: unknown) => {
@@ -177,8 +67,8 @@ describe('useSelectionState', () => {
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(filterOutputNodes).mockImplementation((nodes) =>
nodes.filter((n) => n.type === 'OutputNode')
)
})
@@ -189,10 +79,10 @@ describe('useSelectionState', () => {
})
test('should return true when items selected', () => {
// Update the mock data before creating the composable
const node1 = createTestNode()
const node2 = createTestNode()
mockSelectedItems.value = [node1, node2]
const canvasStore = useCanvasStore()
const node1 = createMockLGraphNode({ id: 1 })
const node2 = createMockLGraphNode({ id: 2 })
canvasStore.$state.selectedItems = [node1, node2]
const { hasAnySelection } = useSelectionState()
expect(hasAnySelection.value).toBe(true)
@@ -201,9 +91,13 @@ describe('useSelectionState', () => {
describe('Node Type Filtering', () => {
test('should pick only LGraphNodes from mixed selections', () => {
// Update the mock data before creating the composable
const graphNode = createTestNode()
mockSelectedItems.value = [graphNode, mockComment, mockConnection]
const canvasStore = useCanvasStore()
const graphNode = createMockLGraphNode({ id: 3 })
canvasStore.$state.selectedItems = [
graphNode,
mockComment,
mockConnection
]
const { selectedNodes } = useSelectionState()
expect(selectedNodes.value).toHaveLength(1)
@@ -213,9 +107,12 @@ describe('useSelectionState', () => {
describe('Node State Computation', () => {
test('should detect bypassed nodes', () => {
// Update the mock data before creating the composable
const bypassedNode = createTestNode({ mode: LGraphEventMode.BYPASS })
mockSelectedItems.value = [bypassedNode]
const canvasStore = useCanvasStore()
const bypassedNode = createMockLGraphNode({
id: 4,
mode: LGraphEventMode.BYPASS
})
canvasStore.$state.selectedItems = [bypassedNode]
const { selectedNodes } = useSelectionState()
const isBypassed = selectedNodes.value.some(
@@ -225,10 +122,13 @@ describe('useSelectionState', () => {
})
test('should detect pinned/collapsed states', () => {
// Update the mock data before creating the composable
const pinnedNode = createTestNode({ pinned: true })
const collapsedNode = createTestNode({ flags: { collapsed: true } })
mockSelectedItems.value = [pinnedNode, collapsedNode]
const canvasStore = useCanvasStore()
const pinnedNode = createMockLGraphNode({ id: 5, pinned: true })
const collapsedNode = createMockLGraphNode({
id: 6,
flags: { collapsed: true }
})
canvasStore.$state.selectedItems = [pinnedNode, collapsedNode]
const { selectedNodes } = useSelectionState()
const isPinned = selectedNodes.value.some((n) => n.pinned === true)
@@ -244,9 +144,9 @@ describe('useSelectionState', () => {
})
test('should provide non-reactive state computation', () => {
// Update the mock data before creating the composable
const node = createTestNode({ pinned: true })
mockSelectedItems.value = [node]
const canvasStore = useCanvasStore()
const node = createMockLGraphNode({ id: 7, pinned: true })
canvasStore.$state.selectedItems = [node]
const { selectedNodes } = useSelectionState()
const isPinned = selectedNodes.value.some((n) => n.pinned === true)
@@ -262,7 +162,7 @@ describe('useSelectionState', () => {
expect(isBypassed).toBe(false)
// Test with empty selection using new composable instance
mockSelectedItems.value = []
canvasStore.$state.selectedItems = []
const { selectedNodes: newSelectedNodes } = useSelectionState()
const newIsPinned = newSelectedNodes.value.some((n) => n.pinned === true)
expect(newIsPinned).toBe(false)