Files
ComfyUI_frontend/src/composables/maskeditor/useImageLoader.test.ts
Johnpaul Chiwetelu b1d8bf0b13 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>
2026-01-24 05:10:35 +01:00

216 lines
5.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useImageLoader } from '@/composables/maskeditor/useImageLoader'
type MockStore = {
imgCanvas: HTMLCanvasElement | null
maskCanvas: HTMLCanvasElement | null
rgbCanvas: HTMLCanvasElement | null
imgCtx: CanvasRenderingContext2D | null
maskCtx: CanvasRenderingContext2D | null
image: HTMLImageElement | null
}
type MockDataStore = {
inputData: {
baseLayer: { image: HTMLImageElement }
maskLayer: { image: HTMLImageElement }
paintLayer: { image: HTMLImageElement } | null
} | null
}
const mockCanvasManager = {
invalidateCanvas: vi.fn().mockResolvedValue(undefined),
updateMaskColor: vi.fn().mockResolvedValue(undefined)
}
const mockStore: MockStore = {
imgCanvas: null,
maskCanvas: null,
rgbCanvas: null,
imgCtx: null,
maskCtx: null,
image: null
}
const mockDataStore: MockDataStore = {
inputData: null
}
vi.mock('@/stores/maskEditorStore', () => ({
useMaskEditorStore: vi.fn(() => mockStore)
}))
vi.mock('@/stores/maskEditorDataStore', () => ({
useMaskEditorDataStore: vi.fn(() => mockDataStore)
}))
vi.mock('@/composables/maskeditor/useCanvasManager', () => ({
useCanvasManager: vi.fn(() => mockCanvasManager)
}))
vi.mock('@vueuse/core', () => ({
createSharedComposable: <T extends (...args: unknown[]) => unknown>(fn: T) =>
fn
}))
describe('useImageLoader', () => {
let mockBaseImage: HTMLImageElement
let mockMaskImage: HTMLImageElement
let mockPaintImage: HTMLImageElement
beforeEach(() => {
vi.clearAllMocks()
mockBaseImage = {
width: 512,
height: 512
} as HTMLImageElement
mockMaskImage = {
width: 512,
height: 512
} as HTMLImageElement
mockPaintImage = {
width: 512,
height: 512
} as HTMLImageElement
mockStore.imgCtx = {
clearRect: vi.fn()
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
mockStore.maskCtx = {
clearRect: vi.fn()
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
mockStore.imgCanvas = {
width: 0,
height: 0
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
mockStore.maskCanvas = {
width: 0,
height: 0
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
mockStore.rgbCanvas = {
width: 0,
height: 0
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
mockDataStore.inputData = {
baseLayer: { image: mockBaseImage },
maskLayer: { image: mockMaskImage },
paintLayer: { image: mockPaintImage }
}
})
describe('loadImages', () => {
it('should load images successfully', async () => {
const loader = useImageLoader()
const result = await loader.loadImages()
expect(result).toBe(mockBaseImage)
expect(mockStore.image).toBe(mockBaseImage)
})
it('should set canvas dimensions', async () => {
const loader = useImageLoader()
await loader.loadImages()
expect(mockStore.maskCanvas?.width).toBe(512)
expect(mockStore.maskCanvas?.height).toBe(512)
expect(mockStore.rgbCanvas?.width).toBe(512)
expect(mockStore.rgbCanvas?.height).toBe(512)
})
it('should clear canvas contexts', async () => {
const loader = useImageLoader()
await loader.loadImages()
expect(mockStore.imgCtx?.clearRect).toHaveBeenCalledWith(0, 0, 0, 0)
expect(mockStore.maskCtx?.clearRect).toHaveBeenCalledWith(0, 0, 0, 0)
})
it('should call canvasManager methods', async () => {
const loader = useImageLoader()
await loader.loadImages()
expect(mockCanvasManager.invalidateCanvas).toHaveBeenCalledWith(
mockBaseImage,
mockMaskImage,
mockPaintImage
)
expect(mockCanvasManager.updateMaskColor).toHaveBeenCalled()
})
it('should handle missing paintLayer', async () => {
mockDataStore.inputData = {
baseLayer: { image: mockBaseImage },
maskLayer: { image: mockMaskImage },
paintLayer: null
}
const loader = useImageLoader()
await loader.loadImages()
expect(mockCanvasManager.invalidateCanvas).toHaveBeenCalledWith(
mockBaseImage,
mockMaskImage,
null
)
})
it('should throw error when no input data', async () => {
mockDataStore.inputData = null
const loader = useImageLoader()
await expect(loader.loadImages()).rejects.toThrow(
'No input data available in dataStore'
)
})
it('should throw error when canvas elements missing', async () => {
mockStore.imgCanvas = null
const loader = useImageLoader()
await expect(loader.loadImages()).rejects.toThrow(
'Canvas elements or contexts not available'
)
})
it('should throw error when contexts missing', async () => {
mockStore.imgCtx = null
const loader = useImageLoader()
await expect(loader.loadImages()).rejects.toThrow(
'Canvas elements or contexts not available'
)
})
it('should handle different image dimensions', async () => {
mockBaseImage.width = 1024
mockBaseImage.height = 768
const loader = useImageLoader()
await loader.loadImages()
expect(mockStore.maskCanvas?.width).toBe(1024)
expect(mockStore.maskCanvas?.height).toBe(768)
expect(mockStore.rgbCanvas?.width).toBe(1024)
expect(mockStore.rgbCanvas?.height).toBe(768)
})
})
})