From 29220f65624490ebfdf5e62b17c787587a4bdf85 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:13:18 +0100 Subject: [PATCH] Road to No explicit any Part 8 (Group 3): Improve type safety in Group 3 test mocks (#8304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Eliminated all `as unknown as` type assertions from Group 3 test files - Created reusable factory functions in `litegraphTestUtils.ts` for better type safety - Improved test mock composition using `Partial` types with single `as` casts - Fixed LGraphNode tests to use proper API methods instead of direct property assignment ## Changes by Category ### New Factory Functions in `litegraphTestUtils.ts` - `createMockLGraphNodeWithArrayBoundingRect()` - Creates LGraphNode with proper boundingRect for position tests - `createMockFileList()` - Creates mock FileList with proper structure including `item()` method ### Test File Improvements **Composables:** - `useLoad3dDrag.test.ts` - Used `createMockFileList` factory - `useLoad3dViewer.test.ts` - Created local `MockSceneManager` interface with proper typing **LiteGraph Tests:** - `LGraphNode.test.ts` - Replaced direct `boundingRect` assignments with `updateArea()` calls - `LinkConnector.test.ts` - Improved mock composition with proper Partial types - `ToOutputRenderLink.test.ts` - Added `MockEvents` interface for type-safe event mocking - Updated integration and core tests to use new factory functions **Extension Tests:** - `contextMenuFilter.test.ts` - Updated menu factories to accept `(IContextMenuValue | null)[]` ## Type Safety Improvements - Zero `as unknown as` instances (was: multiple instances across 17 files) - All mocks use proper `Partial` composition with single `as T` casts - Improved IntelliSense and type checking in test files - Centralized mock creation reduces duplication and improves maintainability ## Test Plan - ✅ All TypeScript type checks pass - ✅ ESLint passes with no new errors - ✅ Pre-commit hooks (format, lint, typecheck) all pass - ✅ Knip unused export check passes - ✅ No behavioral changes to actual tests (only type improvements) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8304-Road-to-No-explicit-any-Improve-type-safety-in-Group-3-test-mocks-2f36d73d365081ab841de96e5f01306d) by [Unito](https://www.unito.io) --- src/composables/useFeatureFlags.test.ts | 17 +- src/composables/useLoad3d.test.ts | 167 +++++++++------ src/composables/useLoad3dDrag.test.ts | 19 +- src/composables/useLoad3dViewer.test.ts | 197 +++++++++++------- src/extensions/core/clipspace.ts | 11 +- .../core/contextMenuFilter.name.test.ts | 19 +- src/extensions/core/contextMenuFilter.test.ts | 32 +-- src/extensions/core/nodeTemplates.ts | 8 +- src/lib/litegraph/src/LGraphNode.test.ts | 52 ++--- .../src/LGraphNodeProperties.test.ts | 19 +- .../src/canvas/LinkConnector.core.test.ts | 11 +- .../canvas/LinkConnector.integration.test.ts | 108 +++++----- .../src/canvas/LinkConnector.test.ts | 56 +++-- ...nkConnectorSubgraphInputValidation.test.ts | 30 +-- .../src/canvas/ToOutputRenderLink.test.ts | 68 +++--- .../litegraph/src/contextMenuCompat.test.ts | 24 ++- src/utils/__tests__/litegraphTestUtils.ts | 123 ++++++++++- 17 files changed, 606 insertions(+), 355 deletions(-) diff --git a/src/composables/useFeatureFlags.test.ts b/src/composables/useFeatureFlags.test.ts index eddb57b65..c2b3634f7 100644 --- a/src/composables/useFeatureFlags.test.ts +++ b/src/composables/useFeatureFlags.test.ts @@ -30,8 +30,7 @@ describe('useFeatureFlags', () => { it('should access supportsPreviewMetadata', () => { vi.mocked(api.getServerFeature).mockImplementation( (path, defaultValue) => { - if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) - return true as any + if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) return true return defaultValue } ) @@ -46,8 +45,7 @@ describe('useFeatureFlags', () => { it('should access maxUploadSize', () => { vi.mocked(api.getServerFeature).mockImplementation( (path, defaultValue) => { - if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) - return 209715200 as any // 200MB + if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) return 209715200 // 200MB return defaultValue } ) @@ -62,7 +60,7 @@ describe('useFeatureFlags', () => { it('should access supportsManagerV4', () => { vi.mocked(api.getServerFeature).mockImplementation( (path, defaultValue) => { - if (path === ServerFeatureFlag.MANAGER_SUPPORTS_V4) return true as any + if (path === ServerFeatureFlag.MANAGER_SUPPORTS_V4) return true return defaultValue } ) @@ -76,7 +74,7 @@ describe('useFeatureFlags', () => { it('should return undefined when features are not available and no default provided', () => { vi.mocked(api.getServerFeature).mockImplementation( - (_path, defaultValue) => defaultValue as any + (_path, defaultValue) => defaultValue ) const { flags } = useFeatureFlags() @@ -90,7 +88,7 @@ describe('useFeatureFlags', () => { it('should create reactive computed for custom feature flags', () => { vi.mocked(api.getServerFeature).mockImplementation( (path, defaultValue) => { - if (path === 'custom.feature') return 'custom-value' as any + if (path === 'custom.feature') return 'custom-value' return defaultValue } ) @@ -108,7 +106,7 @@ describe('useFeatureFlags', () => { it('should handle nested paths', () => { vi.mocked(api.getServerFeature).mockImplementation( (path, defaultValue) => { - if (path === 'extension.custom.nested.feature') return true as any + if (path === 'extension.custom.nested.feature') return true return defaultValue } ) @@ -122,8 +120,7 @@ describe('useFeatureFlags', () => { it('should work with ServerFeatureFlag enum', () => { vi.mocked(api.getServerFeature).mockImplementation( (path, defaultValue) => { - if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) - return 104857600 as any + if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) return 104857600 return defaultValue } ) diff --git a/src/composables/useLoad3d.test.ts b/src/composables/useLoad3d.test.ts index 833e95efd..dafc19931 100644 --- a/src/composables/useLoad3d.test.ts +++ b/src/composables/useLoad3d.test.ts @@ -1,11 +1,19 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick, ref } from 'vue' +import { nextTick, ref, shallowRef } from 'vue' import { nodeToLoad3dMap, useLoad3d } from '@/composables/useLoad3d' import Load3d from '@/extensions/core/load3d/Load3d' import Load3dUtils from '@/extensions/core/load3d/Load3dUtils' +import type { Size } from '@/lib/litegraph/src/interfaces' +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { IWidget } from '@/lib/litegraph/src/types/widgets' import { useToastStore } from '@/platform/updates/common/toastStore' import { api } from '@/scripts/api' +import { + createMockCanvasPointerEvent, + createMockLGraphNode +} from '@/utils/__tests__/litegraphTestUtils' vi.mock('@/extensions/core/load3d/Load3d', () => ({ default: vi.fn() @@ -36,15 +44,15 @@ vi.mock('@/i18n', () => ({ })) describe('useLoad3d', () => { - let mockLoad3d: any - let mockNode: any - let mockToastStore: any + let mockLoad3d: Partial + let mockNode: LGraphNode + let mockToastStore: ReturnType beforeEach(() => { vi.clearAllMocks() nodeToLoad3dMap.clear() - mockNode = { + mockNode = createMockLGraphNode({ properties: { 'Scene Config': { showGrid: true, @@ -68,18 +76,21 @@ describe('useLoad3d', () => { 'Resource Folder': '' }, widgets: [ - { name: 'width', value: 512 }, - { name: 'height', value: 512 } + { name: 'width', value: 512, type: 'number' } as IWidget, + { name: 'height', value: 512, type: 'number' } as IWidget ], graph: { setDirtyCanvas: vi.fn() - }, + } as Partial as LGraph, flags: {}, - onMouseEnter: null, - onMouseLeave: null, - onResize: null, - onDrawBackground: null - } + onMouseEnter: undefined, + onMouseLeave: undefined, + onResize: undefined, + onDrawBackground: undefined + }) + + const mockCanvas = document.createElement('canvas') + mockCanvas.hidden = false mockLoad3d = { toggleGrid: vi.fn(), @@ -114,19 +125,20 @@ describe('useLoad3d', () => { removeEventListener: vi.fn(), remove: vi.fn(), renderer: { - domElement: { - hidden: false - } - } + domElement: mockCanvas + } as Partial as Load3d['renderer'] } - vi.mocked(Load3d).mockImplementation(function () { + vi.mocked(Load3d).mockImplementation(function (this: Load3d) { Object.assign(this, mockLoad3d) + return this }) mockToastStore = { addAlert: vi.fn() - } + } as Partial> as ReturnType< + typeof useToastStore + > vi.mocked(useToastStore).mockReturnValue(mockToastStore) }) @@ -208,14 +220,14 @@ describe('useLoad3d', () => { expect(mockNode.onDrawBackground).toBeDefined() // Test the handlers - mockNode.onMouseEnter() + mockNode.onMouseEnter?.(createMockCanvasPointerEvent(0, 0)) expect(mockLoad3d.refreshViewport).toHaveBeenCalled() expect(mockLoad3d.updateStatusMouseOnNode).toHaveBeenCalledWith(true) - mockNode.onMouseLeave() + mockNode.onMouseLeave?.(createMockCanvasPointerEvent(0, 0)) expect(mockLoad3d.updateStatusMouseOnNode).toHaveBeenCalledWith(false) - mockNode.onResize() + mockNode.onResize?.([512, 512] as Size) expect(mockLoad3d.handleResize).toHaveBeenCalled() }) @@ -226,13 +238,17 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) mockNode.flags.collapsed = true - mockNode.onDrawBackground() + mockNode.onDrawBackground?.({} as CanvasRenderingContext2D) - expect(mockLoad3d.renderer.domElement.hidden).toBe(true) + expect(mockLoad3d.renderer!.domElement.hidden).toBe(true) }) it('should load model if model_file widget exists', async () => { - mockNode.widgets.push({ name: 'model_file', value: 'test.glb' }) + mockNode.widgets!.push({ + name: 'model_file', + value: 'test.glb', + type: 'text' + } as IWidget) vi.mocked(Load3dUtils.splitFilePath).mockReturnValue([ 'subfolder', 'test.glb' @@ -255,8 +271,12 @@ describe('useLoad3d', () => { }) it('should restore camera state after loading model', async () => { - mockNode.widgets.push({ name: 'model_file', value: 'test.glb' }) - mockNode.properties['Camera Config'].state = { + mockNode.widgets!.push({ + name: 'model_file', + value: 'test.glb', + type: 'text' + } as IWidget) + ;(mockNode.properties!['Camera Config'] as { state: unknown }).state = { position: { x: 1, y: 2, z: 3 }, target: { x: 0, y: 0, z: 0 } } @@ -312,13 +332,13 @@ describe('useLoad3d', () => { it('should handle missing container or node', async () => { const composable = useLoad3d(mockNode) - await composable.initializeLoad3d(null as any) + await composable.initializeLoad3d(null!) expect(Load3d).not.toHaveBeenCalled() }) it('should accept ref as parameter', () => { - const nodeRef = ref(mockNode) + const nodeRef = shallowRef(mockNode) const composable = useLoad3d(nodeRef) expect(composable.sceneConfig.value.backgroundColor).toBe('#000000') @@ -370,9 +390,9 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) - mockLoad3d.toggleGrid.mockClear() - mockLoad3d.setBackgroundColor.mockClear() - mockLoad3d.setBackgroundImage.mockClear() + vi.mocked(mockLoad3d.toggleGrid!).mockClear() + vi.mocked(mockLoad3d.setBackgroundColor!).mockClear() + vi.mocked(mockLoad3d.setBackgroundImage!).mockClear() composable.sceneConfig.value = { showGrid: false, @@ -403,8 +423,8 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) await nextTick() - mockLoad3d.setUpDirection.mockClear() - mockLoad3d.setMaterialMode.mockClear() + vi.mocked(mockLoad3d.setUpDirection!).mockClear() + vi.mocked(mockLoad3d.setMaterialMode!).mockClear() composable.modelConfig.value.upDirection = '+y' composable.modelConfig.value.materialMode = 'wireframe' @@ -426,8 +446,8 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) await nextTick() - mockLoad3d.toggleCamera.mockClear() - mockLoad3d.setFOV.mockClear() + vi.mocked(mockLoad3d.toggleCamera!).mockClear() + vi.mocked(mockLoad3d.setFOV!).mockClear() composable.cameraConfig.value.cameraType = 'orthographic' composable.cameraConfig.value.fov = 90 @@ -449,7 +469,7 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) await nextTick() - mockLoad3d.setLightIntensity.mockClear() + vi.mocked(mockLoad3d.setLightIntensity!).mockClear() composable.lightConfig.value.intensity = 10 await nextTick() @@ -589,7 +609,7 @@ describe('useLoad3d', () => { }) it('should use resource folder for upload', async () => { - mockNode.properties['Resource Folder'] = 'subfolder' + mockNode.properties!['Resource Folder'] = 'subfolder' vi.mocked(Load3dUtils.uploadFile).mockResolvedValue('uploaded-image.jpg') const composable = useLoad3d(mockNode) @@ -641,7 +661,9 @@ describe('useLoad3d', () => { }) it('should handle export errors', async () => { - mockLoad3d.exportModel.mockRejectedValueOnce(new Error('Export failed')) + vi.mocked(mockLoad3d.exportModel!).mockRejectedValueOnce( + new Error('Export failed') + ) const composable = useLoad3d(mockNode) const containerRef = document.createElement('div') @@ -719,12 +741,12 @@ describe('useLoad3d', () => { }) it('should handle materialModeChange event', async () => { - let materialModeHandler: any + let materialModeHandler: ((mode: string) => void) | undefined - mockLoad3d.addEventListener.mockImplementation( - (event: string, handler: any) => { + vi.mocked(mockLoad3d.addEventListener!).mockImplementation( + (event: string, handler: unknown) => { if (event === 'materialModeChange') { - materialModeHandler = handler + materialModeHandler = handler as (mode: string) => void } } ) @@ -734,21 +756,21 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) - materialModeHandler('wireframe') + materialModeHandler?.('wireframe') expect(composable.modelConfig.value.materialMode).toBe('wireframe') }) it('should handle loading events', async () => { - let modelLoadingStartHandler: any - let modelLoadingEndHandler: any + let modelLoadingStartHandler: (() => void) | undefined + let modelLoadingEndHandler: (() => void) | undefined - mockLoad3d.addEventListener.mockImplementation( - (event: string, handler: any) => { + vi.mocked(mockLoad3d.addEventListener!).mockImplementation( + (event: string, handler: unknown) => { if (event === 'modelLoadingStart') { - modelLoadingStartHandler = handler + modelLoadingStartHandler = handler as () => void } else if (event === 'modelLoadingEnd') { - modelLoadingEndHandler = handler + modelLoadingEndHandler = handler as () => void } } ) @@ -758,22 +780,22 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) - modelLoadingStartHandler() + modelLoadingStartHandler?.() expect(composable.loading.value).toBe(true) expect(composable.loadingMessage.value).toBe('load3d.loadingModel') - modelLoadingEndHandler() + modelLoadingEndHandler?.() expect(composable.loading.value).toBe(false) expect(composable.loadingMessage.value).toBe('') }) it('should handle recordingStatusChange event', async () => { - let recordingStatusHandler: any + let recordingStatusHandler: ((status: boolean) => void) | undefined - mockLoad3d.addEventListener.mockImplementation( - (event: string, handler: any) => { + vi.mocked(mockLoad3d.addEventListener!).mockImplementation( + (event: string, handler: unknown) => { if (event === 'recordingStatusChange') { - recordingStatusHandler = handler + recordingStatusHandler = handler as (status: boolean) => void } } ) @@ -783,7 +805,7 @@ describe('useLoad3d', () => { await composable.initializeLoad3d(containerRef) - recordingStatusHandler(false) + recordingStatusHandler?.(false) expect(composable.isRecording.value).toBe(false) expect(composable.recordingDuration.value).toBe(10) @@ -814,10 +836,11 @@ describe('useLoad3d', () => { describe('getModelUrl', () => { it('should handle http URLs directly', async () => { - mockNode.widgets.push({ + mockNode.widgets!.push({ name: 'model_file', - value: 'http://example.com/model.glb' - }) + value: 'http://example.com/model.glb', + type: 'text' + } as IWidget) const composable = useLoad3d(mockNode) const containerRef = document.createElement('div') @@ -830,7 +853,11 @@ describe('useLoad3d', () => { }) it('should construct URL for local files', async () => { - mockNode.widgets.push({ name: 'model_file', value: 'models/test.glb' }) + mockNode.widgets!.push({ + name: 'model_file', + value: 'models/test.glb', + type: 'text' + } as IWidget) vi.mocked(Load3dUtils.splitFilePath).mockReturnValue([ 'models', 'test.glb' @@ -860,7 +887,9 @@ describe('useLoad3d', () => { }) it('should use output type for preview mode', async () => { - mockNode.widgets = [{ name: 'model_file', value: 'test.glb' }] // No width/height widgets + mockNode.widgets = [ + { name: 'model_file', value: 'test.glb', type: 'text' } as IWidget + ] // No width/height widgets vi.mocked(Load3dUtils.splitFilePath).mockReturnValue(['', 'test.glb']) vi.mocked(Load3dUtils.getResourceURL).mockReturnValue( '/api/view/test.glb' @@ -894,10 +923,10 @@ describe('useLoad3d', () => { }) it('should handle missing configurations', async () => { - delete mockNode.properties['Scene Config'] - delete mockNode.properties['Model Config'] - delete mockNode.properties['Camera Config'] - delete mockNode.properties['Light Config'] + delete mockNode.properties!['Scene Config'] + delete mockNode.properties!['Model Config'] + delete mockNode.properties!['Camera Config'] + delete mockNode.properties!['Light Config'] const composable = useLoad3d(mockNode) const containerRef = document.createElement('div') @@ -909,7 +938,11 @@ describe('useLoad3d', () => { }) it('should handle background image with existing config', async () => { - mockNode.properties['Scene Config'].backgroundImage = 'existing.jpg' + ;( + mockNode.properties!['Scene Config'] as { + backgroundImage: string + } + ).backgroundImage = 'existing.jpg' const composable = useLoad3d(mockNode) const containerRef = document.createElement('div') diff --git a/src/composables/useLoad3dDrag.test.ts b/src/composables/useLoad3dDrag.test.ts index f35c5c736..a682f5af8 100644 --- a/src/composables/useLoad3dDrag.test.ts +++ b/src/composables/useLoad3dDrag.test.ts @@ -3,6 +3,7 @@ import { ref } from 'vue' import { useLoad3dDrag } from '@/composables/useLoad3dDrag' import { useToastStore } from '@/platform/updates/common/toastStore' +import { createMockFileList } from '@/utils/__tests__/litegraphTestUtils' vi.mock('@/platform/updates/common/toastStore', () => ({ useToastStore: vi.fn() @@ -19,22 +20,22 @@ function createMockDragEvent( const files = options.files || [] const types = options.hasFiles ? ['Files'] : [] - const dataTransfer = { + const dataTransfer: Partial = { types, - files, + files: createMockFileList(files), dropEffect: 'none' as DataTransfer['dropEffect'] } - const event = { + const event: Partial = { type, - dataTransfer - } as unknown as DragEvent + dataTransfer: dataTransfer as DataTransfer + } - return event + return event as DragEvent } describe('useLoad3dDrag', () => { - let mockToastStore: any + let mockToastStore: ReturnType let mockOnModelDrop: (file: File) => void | Promise beforeEach(() => { @@ -42,7 +43,9 @@ describe('useLoad3dDrag', () => { mockToastStore = { addAlert: vi.fn() - } + } as Partial> as ReturnType< + typeof useToastStore + > vi.mocked(useToastStore).mockReturnValue(mockToastStore) mockOnModelDrop = vi.fn() diff --git a/src/composables/useLoad3dViewer.test.ts b/src/composables/useLoad3dViewer.test.ts index 77a8e50a0..757e1400d 100644 --- a/src/composables/useLoad3dViewer.test.ts +++ b/src/composables/useLoad3dViewer.test.ts @@ -4,8 +4,11 @@ import { nextTick } from 'vue' import { useLoad3dViewer } from '@/composables/useLoad3dViewer' import Load3d from '@/extensions/core/load3d/Load3d' import Load3dUtils from '@/extensions/core/load3d/Load3dUtils' +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { useToastStore } from '@/platform/updates/common/toastStore' import { useLoad3dService } from '@/services/load3dService' +import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils' vi.mock('@/services/load3dService', () => ({ useLoad3dService: vi.fn() @@ -29,17 +32,32 @@ vi.mock('@/extensions/core/load3d/Load3d', () => ({ default: vi.fn() })) +function createMockSceneManager(): Load3d['sceneManager'] { + const mock: Partial = { + scene: {} as Load3d['sceneManager']['scene'], + backgroundScene: {} as Load3d['sceneManager']['backgroundScene'], + backgroundCamera: {} as Load3d['sceneManager']['backgroundCamera'], + currentBackgroundColor: '#282828', + gridHelper: { visible: true } as Load3d['sceneManager']['gridHelper'], + getCurrentBackgroundInfo: vi.fn().mockReturnValue({ + type: 'color', + value: '#282828' + }) + } + return mock as Load3d['sceneManager'] +} + describe('useLoad3dViewer', () => { - let mockLoad3d: any - let mockSourceLoad3d: any - let mockLoad3dService: any - let mockToastStore: any - let mockNode: any + let mockLoad3d: Partial + let mockSourceLoad3d: Partial + let mockLoad3dService: ReturnType + let mockToastStore: ReturnType + let mockNode: LGraphNode beforeEach(() => { vi.clearAllMocks() - mockNode = { + mockNode = createMockLGraphNode({ properties: { 'Scene Config': { backgroundColor: '#282828', @@ -62,9 +80,9 @@ describe('useLoad3dViewer', () => { }, graph: { setDirtyCanvas: vi.fn() - }, + } as Partial as LGraph, widgets: [] - } as any + }) mockLoad3d = { setBackgroundColor: vi.fn(), @@ -97,24 +115,17 @@ describe('useLoad3dViewer', () => { zoom: 1, cameraType: 'perspective' }), - sceneManager: { - currentBackgroundColor: '#282828', - gridHelper: { visible: true }, - getCurrentBackgroundInfo: vi.fn().mockReturnValue({ - type: 'color', - value: '#282828' - }) - }, + sceneManager: createMockSceneManager(), lightingManager: { lights: [null, { intensity: 1 }] - }, + } as Load3d['lightingManager'], cameraManager: { perspectiveCamera: { fov: 75 } - }, + } as Load3d['cameraManager'], modelManager: { currentUpDirection: 'original', materialMode: 'original' - }, + } as Load3d['modelManager'], setBackgroundImage: vi.fn().mockResolvedValue(undefined), setBackgroundRenderMode: vi.fn(), forceRender: vi.fn() @@ -128,12 +139,16 @@ describe('useLoad3dViewer', () => { copyLoad3dState: vi.fn().mockResolvedValue(undefined), handleViewportRefresh: vi.fn(), getLoad3d: vi.fn().mockReturnValue(mockSourceLoad3d) - } + } as Partial> as ReturnType< + typeof useLoad3dService + > vi.mocked(useLoad3dService).mockReturnValue(mockLoad3dService) mockToastStore = { addAlert: vi.fn() - } + } as Partial> as ReturnType< + typeof useToastStore + > vi.mocked(useToastStore).mockReturnValue(mockToastStore) }) @@ -160,7 +175,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) expect(Load3d).toHaveBeenCalledWith(containerRef, { width: undefined, @@ -184,16 +199,20 @@ describe('useLoad3dViewer', () => { }) it('should handle background image during initialization', async () => { - mockSourceLoad3d.sceneManager.getCurrentBackgroundInfo.mockReturnValue({ + vi.mocked( + mockSourceLoad3d.sceneManager!.getCurrentBackgroundInfo + ).mockReturnValue({ type: 'image', value: '' }) - mockNode.properties['Scene Config'].backgroundImage = 'test-image.jpg' + ;( + mockNode.properties!['Scene Config'] as Record + ).backgroundImage = 'test-image.jpg' const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) expect(viewer.backgroundImage.value).toBe('test-image.jpg') expect(viewer.hasBackgroundImage.value).toBe(true) @@ -207,7 +226,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) expect(mockToastStore.addAlert).toHaveBeenCalledWith( 'toastMessages.failedToInitializeLoad3dViewer' @@ -220,7 +239,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.backgroundColor.value = '#ff0000' await nextTick() @@ -232,7 +251,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.showGrid.value = false await nextTick() @@ -244,7 +263,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.cameraType.value = 'orthographic' await nextTick() @@ -256,7 +275,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.fov.value = 90 await nextTick() @@ -268,7 +287,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.lightIntensity.value = 2 await nextTick() @@ -280,7 +299,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.backgroundImage.value = 'new-bg.jpg' await nextTick() @@ -293,7 +312,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.upDirection.value = '+y' await nextTick() @@ -305,7 +324,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.materialMode.value = 'wireframe' await nextTick() @@ -314,14 +333,16 @@ describe('useLoad3dViewer', () => { }) it('should handle watcher errors gracefully', async () => { - mockLoad3d.setBackgroundColor.mockImplementationOnce(function () { - throw new Error('Color update failed') - }) + vi.mocked(mockLoad3d.setBackgroundColor!).mockImplementationOnce( + function () { + throw new Error('Color update failed') + } + ) const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.backgroundColor.value = '#ff0000' await nextTick() @@ -337,7 +358,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) await viewer.exportModel('glb') @@ -345,12 +366,14 @@ describe('useLoad3dViewer', () => { }) it('should handle export errors', async () => { - mockLoad3d.exportModel.mockRejectedValueOnce(new Error('Export failed')) + vi.mocked(mockLoad3d.exportModel!).mockRejectedValueOnce( + new Error('Export failed') + ) const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) await viewer.exportModel('glb') @@ -373,7 +396,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.handleResize() @@ -384,7 +407,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.handleMouseEnter() @@ -395,7 +418,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.handleMouseLeave() @@ -408,22 +431,35 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) - - mockNode.properties['Scene Config'].backgroundColor = '#ff0000' - mockNode.properties['Scene Config'].showGrid = false + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) + ;( + mockNode.properties!['Scene Config'] as Record + ).backgroundColor = '#ff0000' + ;( + mockNode.properties!['Scene Config'] as Record + ).showGrid = false viewer.restoreInitialState() - expect(mockNode.properties['Scene Config'].backgroundColor).toBe( - '#282828' - ) - expect(mockNode.properties['Scene Config'].showGrid).toBe(true) - expect(mockNode.properties['Camera Config'].cameraType).toBe( - 'perspective' - ) - expect(mockNode.properties['Camera Config'].fov).toBe(75) - expect(mockNode.properties['Light Config'].intensity).toBe(1) + expect( + (mockNode.properties!['Scene Config'] as Record) + .backgroundColor + ).toBe('#282828') + expect( + (mockNode.properties!['Scene Config'] as Record) + .showGrid + ).toBe(true) + expect( + (mockNode.properties!['Camera Config'] as Record) + .cameraType + ).toBe('perspective') + expect( + (mockNode.properties!['Camera Config'] as Record).fov + ).toBe(75) + expect( + (mockNode.properties!['Light Config'] as Record) + .intensity + ).toBe(1) }) }) @@ -432,7 +468,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.backgroundColor.value = '#ff0000' viewer.showGrid.value = false @@ -440,23 +476,27 @@ describe('useLoad3dViewer', () => { const result = await viewer.applyChanges() expect(result).toBe(true) - expect(mockNode.properties['Scene Config'].backgroundColor).toBe( - '#ff0000' - ) - expect(mockNode.properties['Scene Config'].showGrid).toBe(false) + expect( + (mockNode.properties!['Scene Config'] as Record) + .backgroundColor + ).toBe('#ff0000') + expect( + (mockNode.properties!['Scene Config'] as Record) + .showGrid + ).toBe(false) expect(mockLoad3dService.copyLoad3dState).toHaveBeenCalledWith( mockLoad3d, mockSourceLoad3d ) expect(mockSourceLoad3d.forceRender).toHaveBeenCalled() - expect(mockNode.graph.setDirtyCanvas).toHaveBeenCalledWith(true, true) + expect(mockNode.graph!.setDirtyCanvas).toHaveBeenCalledWith(true, true) }) it('should handle background image during apply', async () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.backgroundImage.value = 'new-bg.jpg' @@ -481,7 +521,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.refreshViewport() @@ -498,7 +538,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) const file = new File([''], 'test.jpg', { type: 'image/jpeg' }) await viewer.handleBackgroundImageUpdate(file) @@ -515,7 +555,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) const file = new File([''], 'test.jpg', { type: 'image/jpeg' }) await viewer.handleBackgroundImageUpdate(file) @@ -527,7 +567,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.backgroundImage.value = 'existing.jpg' viewer.hasBackgroundImage.value = true @@ -546,7 +586,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) const file = new File([''], 'test.jpg', { type: 'image/jpeg' }) await viewer.handleBackgroundImageUpdate(file) @@ -562,7 +602,7 @@ describe('useLoad3dViewer', () => { const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) viewer.cleanup() @@ -580,33 +620,36 @@ describe('useLoad3dViewer', () => { it('should handle missing container ref', async () => { const viewer = useLoad3dViewer(mockNode) - await viewer.initializeViewer(null as any, mockSourceLoad3d) + await viewer.initializeViewer(null!, mockSourceLoad3d as Load3d) expect(Load3d).not.toHaveBeenCalled() }) it('should handle orthographic camera', async () => { - mockSourceLoad3d.getCurrentCameraType.mockReturnValue('orthographic') + vi.mocked(mockSourceLoad3d.getCurrentCameraType!).mockReturnValue( + 'orthographic' + ) mockSourceLoad3d.cameraManager = { perspectiveCamera: { fov: 75 } - } - delete mockNode.properties['Camera Config'].cameraType + } as Partial as Load3d['cameraManager'] + delete (mockNode.properties!['Camera Config'] as Record) + .cameraType const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) expect(viewer.cameraType.value).toBe('orthographic') }) it('should handle missing lights', async () => { - mockSourceLoad3d.lightingManager.lights = [] + mockSourceLoad3d.lightingManager!.lights = [] const viewer = useLoad3dViewer(mockNode) const containerRef = document.createElement('div') - await viewer.initializeViewer(containerRef, mockSourceLoad3d) + await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d) expect(viewer.lightIntensity.value).toBe(1) // Default value }) diff --git a/src/extensions/core/clipspace.ts b/src/extensions/core/clipspace.ts index bc644d8d7..2fe7378de 100644 --- a/src/extensions/core/clipspace.ts +++ b/src/extensions/core/clipspace.ts @@ -47,8 +47,9 @@ export class ClipspaceDialog extends ComfyDialog { if (ClipspaceDialog.instance) { const self = ClipspaceDialog.instance // allow reconstruct controls when copying from non-image to image content. + const imgSettings = self.createImgSettings() const children = $el('div.comfy-modal-content', [ - self.createImgSettings(), + ...(imgSettings ? [imgSettings] : []), ...self.createButtons() ]) @@ -103,7 +104,7 @@ export class ClipspaceDialog extends ComfyDialog { return buttons } - createImgSettings() { + createImgSettings(): HTMLTableElement | null { if (ComfyApp.clipspace?.imgs) { const combo_items = [] const imgs = ComfyApp.clipspace.imgs @@ -167,14 +168,14 @@ export class ClipspaceDialog extends ComfyDialog { return $el('table', {}, [row1, row2, row3]) } else { - return [] + return null } } - createImgPreview() { + createImgPreview(): HTMLImageElement | null { if (ComfyApp.clipspace?.imgs) { return $el('img', { id: 'clipspace_preview', ondragstart: () => false }) - } else return [] + } else return null } override show() { diff --git a/src/extensions/core/contextMenuFilter.name.test.ts b/src/extensions/core/contextMenuFilter.name.test.ts index 1fb0b5fd5..2ee4f6a21 100644 --- a/src/extensions/core/contextMenuFilter.name.test.ts +++ b/src/extensions/core/contextMenuFilter.name.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from 'vitest' import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat' +import type { + IContextMenuValue, + LGraphNode +} from '@/lib/litegraph/src/litegraph' import { LGraphCanvas } from '@/lib/litegraph/src/litegraph' /** @@ -18,11 +22,12 @@ describe('Context Menu Extension Name in Warnings', () => { // Extension monkey-patches the method const original = LGraphCanvas.prototype.getCanvasMenuOptions - LGraphCanvas.prototype.getCanvasMenuOptions = function (...args: any[]) { - const items = (original as any).apply(this, args) - items.push({ content: 'My Custom Menu Item', callback: () => {} }) - return items - } + LGraphCanvas.prototype.getCanvasMenuOptions = + function (): (IContextMenuValue | null)[] { + const items = original.call(this) + items.push({ content: 'My Custom Menu Item', callback: () => {} }) + return items + } // Clear extension (happens after setup completes) legacyMenuCompat.setCurrentExtension(null) @@ -49,8 +54,8 @@ describe('Context Menu Extension Name in Warnings', () => { // Extension monkey-patches the method const original = LGraphCanvas.prototype.getNodeMenuOptions - LGraphCanvas.prototype.getNodeMenuOptions = function (...args: any[]) { - const items = (original as any).apply(this, args) + LGraphCanvas.prototype.getNodeMenuOptions = function (node: LGraphNode) { + const items = original.call(this, node) items.push({ content: 'My Node Menu Item', callback: () => {} }) return items } diff --git a/src/extensions/core/contextMenuFilter.test.ts b/src/extensions/core/contextMenuFilter.test.ts index 5cd9d3664..72a2d7604 100644 --- a/src/extensions/core/contextMenuFilter.test.ts +++ b/src/extensions/core/contextMenuFilter.test.ts @@ -7,6 +7,10 @@ import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useExtensionService } from '@/services/extensionService' import { useExtensionStore } from '@/stores/extensionStore' import type { ComfyExtension } from '@/types/comfy' +import { + createMockCanvas, + createMockLGraphNode +} from '@/utils/__tests__/litegraphTestUtils' describe('Context Menu Extension API', () => { let mockCanvas: LGraphCanvas @@ -35,7 +39,7 @@ describe('Context Menu Extension API', () => { // Mock extensions const createCanvasMenuExtension = ( name: string, - items: IContextMenuValue[] + items: (IContextMenuValue | null)[] ): ComfyExtension => ({ name, getCanvasMenuItems: () => items @@ -54,16 +58,16 @@ describe('Context Menu Extension API', () => { extensionStore = useExtensionStore() extensionService = useExtensionService() - mockCanvas = { + mockCanvas = createMockCanvas({ graph_mouse: [100, 100], selectedItems: new Set() - } as unknown as LGraphCanvas + }) - mockNode = { + mockNode = createMockLGraphNode({ id: 1, type: 'TestNode', pos: [0, 0] - } as unknown as LGraphNode + }) }) describe('collectCanvasMenuItems', () => { @@ -79,7 +83,7 @@ describe('Context Menu Extension API', () => { const items: IContextMenuValue[] = extensionService .invokeExtensions('getCanvasMenuItems', mockCanvas) - .flat() + .flat() as IContextMenuValue[] expect(items).toHaveLength(3) expect(items[0]).toMatchObject({ content: 'Canvas Item 1' }) @@ -99,7 +103,7 @@ describe('Context Menu Extension API', () => { ] } }, - null as unknown as IContextMenuValue, + null, { content: 'After Separator', callback: () => {} } ]) @@ -107,7 +111,7 @@ describe('Context Menu Extension API', () => { const items: IContextMenuValue[] = extensionService .invokeExtensions('getCanvasMenuItems', mockCanvas) - .flat() + .flat() as IContextMenuValue[] expect(items).toHaveLength(3) expect(items[0].content).toBe('Menu with Submenu') @@ -129,7 +133,7 @@ describe('Context Menu Extension API', () => { const items: IContextMenuValue[] = extensionService .invokeExtensions('getCanvasMenuItems', mockCanvas) - .flat() + .flat() as IContextMenuValue[] expect(items).toHaveLength(1) expect(items[0].content).toBe('Canvas Item 1') @@ -146,11 +150,11 @@ describe('Context Menu Extension API', () => { // Collect items multiple times (simulating repeated menu opens) const items1: IContextMenuValue[] = extensionService .invokeExtensions('getCanvasMenuItems', mockCanvas) - .flat() + .flat() as IContextMenuValue[] const items2: IContextMenuValue[] = extensionService .invokeExtensions('getCanvasMenuItems', mockCanvas) - .flat() + .flat() as IContextMenuValue[] // Both collections should have the same items (no duplication) expect(items1).toHaveLength(2) @@ -180,7 +184,7 @@ describe('Context Menu Extension API', () => { const items: IContextMenuValue[] = extensionService .invokeExtensions('getNodeMenuItems', mockNode) - .flat() + .flat() as IContextMenuValue[] expect(items).toHaveLength(3) expect(items[0]).toMatchObject({ content: 'Node Item 1' }) @@ -205,7 +209,7 @@ describe('Context Menu Extension API', () => { const items: IContextMenuValue[] = extensionService .invokeExtensions('getNodeMenuItems', mockNode) - .flat() + .flat() as IContextMenuValue[] expect(items[0].content).toBe('Node Menu with Submenu') expect(items[0].submenu?.options).toHaveLength(2) @@ -222,7 +226,7 @@ describe('Context Menu Extension API', () => { const items: IContextMenuValue[] = extensionService .invokeExtensions('getNodeMenuItems', mockNode) - .flat() + .flat() as IContextMenuValue[] expect(items).toHaveLength(1) expect(items[0].content).toBe('Node Item 1') diff --git a/src/extensions/core/nodeTemplates.ts b/src/extensions/core/nodeTemplates.ts index 040b54526..532fbc410 100644 --- a/src/extensions/core/nodeTemplates.ts +++ b/src/extensions/core/nodeTemplates.ts @@ -32,9 +32,13 @@ import { GroupNodeConfig, GroupNodeHandler } from './groupNode' const id = 'Comfy.NodeTemplates' const file = 'comfy.templates.json' +interface NodeTemplate { + name: string + data: string +} + class ManageTemplates extends ComfyDialog { - // @ts-expect-error fixme ts strict error - templates: any[] + templates: NodeTemplate[] = [] draggedEl: HTMLElement | null saveVisualCue: number | null emptyImg: HTMLImageElement diff --git a/src/lib/litegraph/src/LGraphNode.test.ts b/src/lib/litegraph/src/LGraphNode.test.ts index 98ef533f6..6682d274c 100644 --- a/src/lib/litegraph/src/LGraphNode.test.ts +++ b/src/lib/litegraph/src/LGraphNode.test.ts @@ -14,6 +14,11 @@ import { } from '@/lib/litegraph/src/litegraph' import { test } from './__fixtures__/testExtensions' +import { createMockLGraphNodeWithArrayBoundingRect } from '@/utils/__tests__/litegraphTestUtils' + +interface NodeConstructorWithSlotOffset { + slot_start_y?: number +} function getMockISerialisedNode( data: Partial @@ -297,16 +302,10 @@ describe('LGraphNode', () => { describe('getInputPos and getOutputPos', () => { test('should handle collapsed nodes correctly', () => { - const node = new LGraphNode('TestNode') as unknown as Omit< - LGraphNode, - 'boundingRect' - > & { boundingRect: Float64Array } + const node = createMockLGraphNodeWithArrayBoundingRect('TestNode') node.pos = [100, 100] node.size = [100, 100] - node.boundingRect[0] = 100 - node.boundingRect[1] = 100 - node.boundingRect[2] = 100 - node.boundingRect[3] = 100 + node.updateArea() node.configure( getMockISerialisedNode({ id: 1, @@ -366,16 +365,10 @@ describe('LGraphNode', () => { }) test('should detect input slots correctly', () => { - const node = new LGraphNode('TestNode') as unknown as Omit< - LGraphNode, - 'boundingRect' - > & { boundingRect: Float64Array } + const node = createMockLGraphNodeWithArrayBoundingRect('TestNode') node.pos = [100, 100] node.size = [100, 100] - node.boundingRect[0] = 100 - node.boundingRect[1] = 100 - node.boundingRect[2] = 200 - node.boundingRect[3] = 200 + node.updateArea() node.configure( getMockISerialisedNode({ id: 1, @@ -398,16 +391,10 @@ describe('LGraphNode', () => { }) test('should detect output slots correctly', () => { - const node = new LGraphNode('TestNode') as unknown as Omit< - LGraphNode, - 'boundingRect' - > & { boundingRect: Float64Array } + const node = createMockLGraphNodeWithArrayBoundingRect('TestNode') node.pos = [100, 100] node.size = [100, 100] - node.boundingRect[0] = 100 - node.boundingRect[1] = 100 - node.boundingRect[2] = 200 - node.boundingRect[3] = 200 + node.updateArea() node.configure( getMockISerialisedNode({ id: 1, @@ -431,16 +418,10 @@ describe('LGraphNode', () => { }) test('should prioritize input slots over output slots', () => { - const node = new LGraphNode('TestNode') as unknown as Omit< - LGraphNode, - 'boundingRect' - > & { boundingRect: Float64Array } + const node = createMockLGraphNodeWithArrayBoundingRect('TestNode') node.pos = [100, 100] node.size = [100, 100] - node.boundingRect[0] = 100 - node.boundingRect[1] = 100 - node.boundingRect[2] = 200 - node.boundingRect[3] = 200 + node.updateArea() node.configure( getMockISerialisedNode({ id: 1, @@ -632,7 +613,8 @@ describe('LGraphNode', () => { } node.inputs = [inputSlot, inputSlot2] const slotIndex = 0 - const nodeOffsetY = (node.constructor as any).slot_start_y || 0 + const nodeOffsetY = + (node.constructor as NodeConstructorWithSlotOffset).slot_start_y || 0 const expectedY = 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY const expectedX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5 @@ -644,7 +626,7 @@ describe('LGraphNode', () => { }) test('should return default vertical position including slot_start_y when defined', () => { - ;(node.constructor as any).slot_start_y = 25 + ;(node.constructor as NodeConstructorWithSlotOffset).slot_start_y = 25 node.flags.collapsed = false node.inputs = [inputSlot] const slotIndex = 0 @@ -653,7 +635,7 @@ describe('LGraphNode', () => { 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY const expectedX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5 expect(node.getInputSlotPos(inputSlot)).toEqual([expectedX, expectedY]) - delete (node.constructor as any).slot_start_y + delete (node.constructor as NodeConstructorWithSlotOffset).slot_start_y }) test('should not overwrite onMouseDown prototype', () => { expect(Object.prototype.hasOwnProperty.call(node, 'onMouseDown')).toEqual( diff --git a/src/lib/litegraph/src/LGraphNodeProperties.test.ts b/src/lib/litegraph/src/LGraphNodeProperties.test.ts index aca6fe391..1c523945c 100644 --- a/src/lib/litegraph/src/LGraphNodeProperties.test.ts +++ b/src/lib/litegraph/src/LGraphNodeProperties.test.ts @@ -1,22 +1,25 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it } from 'vitest' import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties' +import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { + createMockLGraph, + createMockLGraphNode +} from '@/utils/__tests__/litegraphTestUtils' describe('LGraphNodeProperties', () => { - let mockNode: any - let mockGraph: any + let mockNode: LGraphNode + let mockGraph: LGraph beforeEach(() => { - mockGraph = { - trigger: vi.fn() - } + mockGraph = createMockLGraph() - mockNode = { + mockNode = createMockLGraphNode({ id: 123, title: 'Test Node', flags: {}, graph: mockGraph - } + }) }) describe('property tracking', () => { diff --git a/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts b/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts index 65b468940..4b1d29cca 100644 --- a/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts +++ b/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts @@ -17,6 +17,10 @@ import { LinkDirection } from '@/lib/litegraph/src/litegraph' import type { ConnectingLink } from '@/lib/litegraph/src/interfaces' +import { + createMockNodeInputSlot, + createMockNodeOutputSlot +} from '@/utils/__tests__/litegraphTestUtils' interface TestContext { network: LinkNetwork & { add(node: LGraphNode): void } @@ -136,7 +140,7 @@ describe('LinkConnector', () => { connector.state.connectingTo = 'input' expect(() => { - connector.moveInputLink(network, { link: 1 } as any) + connector.moveInputLink(network, createMockNodeInputSlot({ link: 1 })) }).toThrow('Already dragging links.') }) }) @@ -174,7 +178,10 @@ describe('LinkConnector', () => { connector.state.connectingTo = 'output' expect(() => { - connector.moveOutputLink(network, { links: [1] } as any) + connector.moveOutputLink( + network, + createMockNodeOutputSlot({ links: [1] }) + ) }).toThrow('Already dragging links.') }) }) diff --git a/src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts b/src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts index 3c4368c5a..671709345 100644 --- a/src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts +++ b/src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts @@ -12,6 +12,10 @@ import { LGraphNode, LLink, LinkConnector } from '@/lib/litegraph/src/litegraph' import { test as baseTest } from '../__fixtures__/testExtensions' import type { ConnectingLink } from '@/lib/litegraph/src/interfaces' +import { + createMockCanvasPointerEvent, + createMockCanvasRenderingContext2D +} from '@/utils/__tests__/litegraphTestUtils' interface TestContext { graph: LGraph @@ -35,9 +39,9 @@ const test = baseTest.extend({ }, graph: async ({ reroutesComplexGraph }, use) => { - const ctx = vi.fn(() => ({ measureText: vi.fn(() => ({ width: 10 })) })) + const mockCtx = createMockCanvasRenderingContext2D() for (const node of reroutesComplexGraph.nodes) { - node.updateArea(ctx() as unknown as CanvasRenderingContext2D) + node.updateArea(mockCtx) } await use(reroutesComplexGraph) }, @@ -186,10 +190,10 @@ const test = baseTest.extend({ }) function mockedNodeTitleDropEvent(node: LGraphNode): CanvasPointerEvent { - return { - canvasX: node.pos[0] + node.size[0] / 2, - canvasY: node.pos[1] + 16 - } as any + return createMockCanvasPointerEvent( + node.pos[0] + node.size[0] / 2, + node.pos[1] + 16 + ) } function mockedInputDropEvent( @@ -197,10 +201,7 @@ function mockedInputDropEvent( slot: number ): CanvasPointerEvent { const pos = node.getInputPos(slot) - return { - canvasX: pos[0], - canvasY: pos[1] - } as any + return createMockCanvasPointerEvent(pos[0], pos[1]) } function mockedOutputDropEvent( @@ -208,10 +209,7 @@ function mockedOutputDropEvent( slot: number ): CanvasPointerEvent { const pos = node.getOutputPos(slot) - return { - canvasX: pos[0], - canvasY: pos[1] - } as any + return createMockCanvasPointerEvent(pos[0], pos[1]) } describe('LinkConnector Integration', () => { @@ -239,7 +237,7 @@ describe('LinkConnector Integration', () => { const canvasX = disconnectedNode.pos[0] + disconnectedNode.size[0] / 2 const canvasY = disconnectedNode.pos[1] + 16 - const dropEvent = { canvasX, canvasY } as any + const dropEvent = createMockCanvasPointerEvent(canvasX, canvasY) // Drop links, ensure reset has not been run connector.dropLinks(graph, dropEvent) @@ -281,7 +279,7 @@ describe('LinkConnector Integration', () => { const canvasX = disconnectedNode.pos[0] + disconnectedNode.size[0] / 2 const canvasY = disconnectedNode.pos[1] + 16 - const dropEvent = { canvasX, canvasY } as any + const dropEvent = createMockCanvasPointerEvent(canvasX, canvasY) connector.dropLinks(graph, dropEvent) connector.reset() @@ -422,7 +420,7 @@ describe('LinkConnector Integration', () => { const canvasX = disconnectedNode.pos[0] + disconnectedNode.size[0] / 2 const canvasY = disconnectedNode.pos[1] + 16 - const dropEvent = { canvasX, canvasY } as any + const dropEvent = createMockCanvasPointerEvent(canvasX, canvasY) connector.dropLinks(graph, dropEvent) connector.reset() @@ -473,9 +471,10 @@ describe('LinkConnector Integration', () => { expect(floatingLink).toBeInstanceOf(LLink) const floatingReroute = LLink.getReroutes(graph, floatingLink)[0] - const canvasX = floatingReroute.pos[0] - const canvasY = floatingReroute.pos[1] - const dropEvent = { canvasX, canvasY } as any + const dropEvent = createMockCanvasPointerEvent( + floatingReroute.pos[0], + floatingReroute.pos[1] + ) connector.dropLinks(graph, dropEvent) connector.reset() @@ -554,7 +553,10 @@ describe('LinkConnector Integration', () => { const manyOutputsNode = graph.getNodeById(4)! const canvasX = floatingReroute.pos[0] const canvasY = floatingReroute.pos[1] - const floatingRerouteEvent = { canvasX, canvasY } as any + const floatingRerouteEvent = createMockCanvasPointerEvent( + canvasX, + canvasY + ) connector.moveOutputLink(graph, manyOutputsNode.outputs[0]) connector.dropLinks(graph, floatingRerouteEvent) @@ -579,7 +581,7 @@ describe('LinkConnector Integration', () => { const canvasX = reroute7.pos[0] const canvasY = reroute7.pos[1] - const reroute7Event = { canvasX, canvasY } as any + const reroute7Event = createMockCanvasPointerEvent(canvasX, canvasY) const toSortedRerouteChain = (linkIds: number[]) => linkIds @@ -698,7 +700,7 @@ describe('LinkConnector Integration', () => { const canvasY = disconnectedNode.pos[1] connector.dragFromReroute(graph, floatingReroute) - connector.dropLinks(graph, { canvasX, canvasY } as any) + connector.dropLinks(graph, createMockCanvasPointerEvent(canvasX, canvasY)) connector.reset() expect(graph.floatingLinks.size).toBe(0) @@ -716,7 +718,7 @@ describe('LinkConnector Integration', () => { const canvasY = reroute8.pos[1] connector.dragFromReroute(graph, floatingReroute) - connector.dropLinks(graph, { canvasX, canvasY } as any) + connector.dropLinks(graph, createMockCanvasPointerEvent(canvasX, canvasY)) connector.reset() expect(graph.floatingLinks.size).toBe(0) @@ -801,10 +803,10 @@ describe('LinkConnector Integration', () => { connector.moveOutputLink(graph, floatingOutNode.outputs[0]) const manyOutputsNode = graph.getNodeById(4)! - const dropEvent = { - canvasX: manyOutputsNode.pos[0], - canvasY: manyOutputsNode.pos[1] - } as any + const dropEvent = createMockCanvasPointerEvent( + manyOutputsNode.pos[0], + manyOutputsNode.pos[1] + ) connector.dropLinks(graph, dropEvent) connector.reset() @@ -818,9 +820,11 @@ describe('LinkConnector Integration', () => { connector.moveOutputLink(graph, manyOutputsNode.outputs[0]) const disconnectedNode = graph.getNodeById(9)! - dropEvent.canvasX = disconnectedNode.pos[0] - dropEvent.canvasY = disconnectedNode.pos[1] - connector.dropLinks(graph, dropEvent) + const dropEvent2 = createMockCanvasPointerEvent( + disconnectedNode.pos[0], + disconnectedNode.pos[1] + ) + connector.dropLinks(graph, dropEvent2) connector.reset() const newOutput = disconnectedNode.outputs[0] @@ -951,10 +955,10 @@ describe('LinkConnector Integration', () => { const targetReroute = graph.reroutes.get(targetRerouteId)! const nextLinkIds = getNextLinkIds(targetReroute.linkIds) - const dropEvent = { - canvasX: targetReroute.pos[0], - canvasY: targetReroute.pos[1] - } as any + const dropEvent = createMockCanvasPointerEvent( + targetReroute.pos[0], + targetReroute.pos[1] + ) connector.dragNewFromOutput( graph, @@ -1094,10 +1098,10 @@ describe('LinkConnector Integration', () => { connector.dragFromReroute(graph, fromReroute) - const dropEvent = { - canvasX: toReroute.pos[0], - canvasY: toReroute.pos[1] - } as any + const dropEvent = createMockCanvasPointerEvent( + toReroute.pos[0], + toReroute.pos[1] + ) connector.dropLinks(graph, dropEvent) connector.reset() @@ -1167,10 +1171,10 @@ describe('LinkConnector Integration', () => { const fromReroute = graph.reroutes.get(from)! const toReroute = graph.reroutes.get(to)! - const dropEvent = { - canvasX: toReroute.pos[0], - canvasY: toReroute.pos[1] - } as any + const dropEvent = createMockCanvasPointerEvent( + toReroute.pos[0], + toReroute.pos[1] + ) connector.dragFromReroute(graph, fromReroute) connector.dropLinks(graph, dropEvent) @@ -1204,10 +1208,10 @@ describe('LinkConnector Integration', () => { const node = graph.getNodeById(nodeId)! const input = node.inputs[0] const reroute = graph.getReroute(rerouteId)! - const dropEvent = { - canvasX: reroute.pos[0], - canvasY: reroute.pos[1] - } as any + const dropEvent = createMockCanvasPointerEvent( + reroute.pos[0], + reroute.pos[1] + ) connector.dragNewFromInput(graph, node, input) connector.dropLinks(graph, dropEvent) @@ -1234,7 +1238,7 @@ describe('LinkConnector Integration', () => { const node = graph.getNodeById(nodeId)! const reroute = graph.getReroute(rerouteId)! - const dropEvent = { canvasX: node.pos[0], canvasY: node.pos[1] } as any + const dropEvent = createMockCanvasPointerEvent(node.pos[0], node.pos[1]) connector.dragFromReroute(graph, reroute) connector.dropLinks(graph, dropEvent) @@ -1262,10 +1266,10 @@ describe('LinkConnector Integration', () => { const node = graph.getNodeById(nodeId)! const reroute = graph.getReroute(rerouteId)! const inputPos = node.getInputPos(0) - const dropOnInputEvent = { - canvasX: inputPos[0], - canvasY: inputPos[1] - } as any + const dropOnInputEvent = createMockCanvasPointerEvent( + inputPos[0], + inputPos[1] + ) connector.dragFromReroute(graph, reroute) connector.dropLinks(graph, dropOnInputEvent) diff --git a/src/lib/litegraph/src/canvas/LinkConnector.test.ts b/src/lib/litegraph/src/canvas/LinkConnector.test.ts index 29786bdf1..9ec8e3dfb 100644 --- a/src/lib/litegraph/src/canvas/LinkConnector.test.ts +++ b/src/lib/litegraph/src/canvas/LinkConnector.test.ts @@ -1,23 +1,46 @@ // TODO: Fix these tests after migration import { beforeEach, describe, expect, test, vi } from 'vitest' -import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph' -// We don't strictly need RenderLink interface import for the mock import { LinkConnector } from '@/lib/litegraph/src/litegraph' +import { + createMockCanvasPointerEvent, + createMockLGraphNode, + createMockLinkNetwork, + createMockNodeInputSlot, + createMockNodeOutputSlot +} from '@/utils/__tests__/litegraphTestUtils' // Mocks const mockSetConnectingLinks = vi.fn() +type RenderLinkItem = LinkConnector['renderLinks'][number] + // Mock a structure that has the needed method -function mockRenderLinkImpl(canConnect: boolean) { - return { - canConnectToInput: vi.fn().mockReturnValue(canConnect) - // Add other properties if they become necessary for tests +function mockRenderLinkImpl(canConnect: boolean): RenderLinkItem { + const partial: Partial = { + toType: 'output', + fromPos: [0, 0], + fromSlotIndex: 0, + fromDirection: 0, + network: createMockLinkNetwork(), + node: createMockLGraphNode(), + fromSlot: createMockNodeOutputSlot(), + dragDirection: 0, + canConnectToInput: vi.fn().mockReturnValue(canConnect), + canConnectToOutput: vi.fn().mockReturnValue(false), + canConnectToReroute: vi.fn().mockReturnValue(false), + connectToInput: vi.fn(), + connectToOutput: vi.fn(), + connectToSubgraphInput: vi.fn(), + connectToRerouteOutput: vi.fn(), + connectToSubgraphOutput: vi.fn(), + connectToRerouteInput: vi.fn() } + return partial as RenderLinkItem } -const mockNode = {} as LGraphNode -const mockInput = {} as INodeInputSlot +const mockNode = createMockLGraphNode() +const mockInput = createMockNodeInputSlot() describe.skip('LinkConnector', () => { let connector: LinkConnector @@ -37,8 +60,7 @@ describe.skip('LinkConnector', () => { test('should return true if at least one render link can connect', () => { const link1 = mockRenderLinkImpl(false) const link2 = mockRenderLinkImpl(true) - // Cast to any to satisfy the push requirement, as we only need the canConnectToInput method - connector.renderLinks.push(link1 as any, link2 as any) + connector.renderLinks.push(link1, link2) expect(connector.isInputValidDrop(mockNode, mockInput)).toBe(true) expect(link1.canConnectToInput).toHaveBeenCalledWith(mockNode, mockInput) expect(link2.canConnectToInput).toHaveBeenCalledWith(mockNode, mockInput) @@ -47,7 +69,7 @@ describe.skip('LinkConnector', () => { test('should return false if no render links can connect', () => { const link1 = mockRenderLinkImpl(false) const link2 = mockRenderLinkImpl(false) - connector.renderLinks.push(link1 as any, link2 as any) + connector.renderLinks.push(link1, link2) expect(connector.isInputValidDrop(mockNode, mockInput)).toBe(false) expect(link1.canConnectToInput).toHaveBeenCalledWith(mockNode, mockInput) expect(link2.canConnectToInput).toHaveBeenCalledWith(mockNode, mockInput) @@ -57,7 +79,7 @@ describe.skip('LinkConnector', () => { const link1 = mockRenderLinkImpl(false) const link2 = mockRenderLinkImpl(true) // This one can connect const link3 = mockRenderLinkImpl(false) - connector.renderLinks.push(link1 as any, link2 as any, link3 as any) + connector.renderLinks.push(link1, link2, link3) expect(connector.isInputValidDrop(mockNode, mockInput)).toBe(true) @@ -88,7 +110,10 @@ describe.skip('LinkConnector', () => { test('should call the listener when the event is dispatched before reset', () => { const listener = vi.fn() - const eventData = { renderLinks: [], event: {} as any } // Mock event data + const eventData = { + renderLinks: [], + event: createMockCanvasPointerEvent(0, 0) + } connector.listenUntilReset('before-drop-links', listener) connector.events.dispatch('before-drop-links', eventData) @@ -120,7 +145,10 @@ describe.skip('LinkConnector', () => { test('should not call the listener after reset is dispatched', () => { const listener = vi.fn() - const eventData = { renderLinks: [], event: {} as any } + const eventData = { + renderLinks: [], + event: createMockCanvasPointerEvent(0, 0) + } connector.listenUntilReset('before-drop-links', listener) // Dispatch reset first diff --git a/src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts b/src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts index 3d9740c4f..8c2922005 100644 --- a/src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts +++ b/src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts @@ -9,10 +9,20 @@ import { LLink } from '@/lib/litegraph/src/litegraph' import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink' -import type { NodeInputSlot } from '@/lib/litegraph/src/litegraph' +import type { + CanvasPointerEvent, + NodeInputSlot +} from '@/lib/litegraph/src/litegraph' import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' import { createTestSubgraph } from '../subgraph/__fixtures__/subgraphHelpers' +import { + createMockCanvasPointerEvent, + createMockNodeInputSlot +} from '@/utils/__tests__/litegraphTestUtils' + +type MockPointerEvent = CanvasPointerEvent +type MockRenderLink = ToOutputRenderLink describe('LinkConnector SubgraphInput connection validation', () => { let connector: LinkConnector @@ -206,10 +216,7 @@ describe('LinkConnector SubgraphInput connection validation', () => { connector.state.connectingTo = 'output' // Create mock event - const mockEvent = { - canvasX: 100, - canvasY: 100 - } as any + const mockEvent: MockPointerEvent = createMockCanvasPointerEvent(100, 100) // Mock the getSlotInPosition to return the subgraph input const mockGetSlotInPosition = vi.fn().mockReturnValue(subgraph.inputs[0]) @@ -256,10 +263,7 @@ describe('LinkConnector SubgraphInput connection validation', () => { connector.state.connectingTo = 'output' // Create mock event - const mockEvent = { - canvasX: 100, - canvasY: 100 - } as any + const mockEvent: MockPointerEvent = createMockCanvasPointerEvent(100, 100) // Mock the getSlotInPosition to return the subgraph input const mockGetSlotInPosition = vi.fn().mockReturnValue(subgraph.inputs[0]) @@ -342,12 +346,12 @@ describe('LinkConnector SubgraphInput connection validation', () => { }) // Create a mock render link without the method - const mockLink = { - fromSlot: { type: 'number' } + const mockLink: Partial = { + fromSlot: createMockNodeInputSlot({ type: 'number' }) // No canConnectToSubgraphInput method - } as any + } - connector.renderLinks.push(mockLink) + connector.renderLinks.push(mockLink as MockRenderLink) const subgraphInput = subgraph.inputs[0] diff --git a/src/lib/litegraph/src/canvas/ToOutputRenderLink.test.ts b/src/lib/litegraph/src/canvas/ToOutputRenderLink.test.ts index 73bfec657..7c9706ee6 100644 --- a/src/lib/litegraph/src/canvas/ToOutputRenderLink.test.ts +++ b/src/lib/litegraph/src/canvas/ToOutputRenderLink.test.ts @@ -4,23 +4,30 @@ import { LinkDirection, ToOutputRenderLink } from '@/lib/litegraph/src/litegraph' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import { + createMockLGraphNode, + createMockLinkNetwork, + createMockNodeInputSlot, + createMockNodeOutputSlot +} from '@/utils/__tests__/litegraphTestUtils' describe('ToOutputRenderLink', () => { describe('connectToOutput', () => { it('should return early if inputNode is null', () => { // Setup - const mockNetwork = {} - const mockFromSlot = {} - const mockNode = { - id: 'test-id', + const mockNetwork = createMockLinkNetwork() + const mockFromSlot = createMockNodeInputSlot() + const mockNode = createMockLGraphNode({ inputs: [mockFromSlot], getInputPos: vi.fn().mockReturnValue([0, 0]) - } + }) const renderLink = new ToOutputRenderLink( - mockNetwork as any, - mockNode as any, - mockFromSlot as any, + mockNetwork, + mockNode, + mockFromSlot, undefined, LinkDirection.CENTER ) @@ -30,18 +37,21 @@ describe('ToOutputRenderLink', () => { value: null }) - const mockTargetNode = { + const mockTargetNode = createMockLGraphNode({ connectSlots: vi.fn() - } - const mockEvents = { + }) + const mockEvents: Partial> = { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), dispatch: vi.fn() } // Act renderLink.connectToOutput( - mockTargetNode as any, - {} as any, - mockEvents as any + mockTargetNode, + createMockNodeOutputSlot(), + mockEvents as CustomEventTarget ) // Assert @@ -51,35 +61,37 @@ describe('ToOutputRenderLink', () => { it('should create connection and dispatch event when inputNode exists', () => { // Setup - const mockNetwork = {} - const mockFromSlot = {} - const mockNode = { - id: 'test-id', + const mockNetwork = createMockLinkNetwork() + const mockFromSlot = createMockNodeInputSlot() + const mockNode = createMockLGraphNode({ inputs: [mockFromSlot], getInputPos: vi.fn().mockReturnValue([0, 0]) - } + }) const renderLink = new ToOutputRenderLink( - mockNetwork as any, - mockNode as any, - mockFromSlot as any, + mockNetwork, + mockNode, + mockFromSlot, undefined, LinkDirection.CENTER ) const mockNewLink = { id: 'new-link' } - const mockTargetNode = { + const mockTargetNode = createMockLGraphNode({ connectSlots: vi.fn().mockReturnValue(mockNewLink) - } - const mockEvents = { + }) + const mockEvents: Partial> = { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), dispatch: vi.fn() } // Act renderLink.connectToOutput( - mockTargetNode as any, - {} as any, - mockEvents as any + mockTargetNode, + createMockNodeOutputSlot(), + mockEvents as CustomEventTarget ) // Assert diff --git a/src/lib/litegraph/src/contextMenuCompat.test.ts b/src/lib/litegraph/src/contextMenuCompat.test.ts index fc4c7d156..246551dc9 100644 --- a/src/lib/litegraph/src/contextMenuCompat.test.ts +++ b/src/lib/litegraph/src/contextMenuCompat.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat' import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph' import { LGraphCanvas } from '@/lib/litegraph/src/litegraph' +import { createMockCanvas } from '@/utils/__tests__/litegraphTestUtils' describe('contextMenuCompat', () => { let originalGetCanvasMenuOptions: typeof LGraphCanvas.prototype.getCanvasMenuOptions @@ -13,11 +14,11 @@ describe('contextMenuCompat', () => { originalGetCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions // Create mock canvas - mockCanvas = { + mockCanvas = createMockCanvas({ constructor: { prototype: LGraphCanvas.prototype - } - } as unknown as LGraphCanvas + } as typeof LGraphCanvas + } as Partial) // Clear console warnings vi.spyOn(console, 'warn').mockImplementation(() => {}) @@ -54,11 +55,12 @@ describe('contextMenuCompat', () => { // Simulate extension monkey-patching const original = LGraphCanvas.prototype.getCanvasMenuOptions - LGraphCanvas.prototype.getCanvasMenuOptions = function (...args: any[]) { - const items = (original as any).apply(this, args) - items.push({ content: 'Custom Item', callback: () => {} }) - return items - } + LGraphCanvas.prototype.getCanvasMenuOptions = + function (): (IContextMenuValue | null)[] { + const items = original.call(this) + items.push({ content: 'Custom Item', callback: () => {} }) + return items + } // Should have logged a warning with extension name expect(warnSpy).toHaveBeenCalledWith( @@ -83,8 +85,10 @@ describe('contextMenuCompat', () => { legacyMenuCompat.install(LGraphCanvas.prototype, methodName) legacyMenuCompat.setCurrentExtension('test.extension') - const patchFunction = function (this: LGraphCanvas, ...args: any[]) { - const items = (originalGetCanvasMenuOptions as any).apply(this, args) + const patchFunction = function ( + this: LGraphCanvas + ): (IContextMenuValue | null)[] { + const items = originalGetCanvasMenuOptions.call(this) items.push({ content: 'Custom', callback: () => {} }) return items } diff --git a/src/utils/__tests__/litegraphTestUtils.ts b/src/utils/__tests__/litegraphTestUtils.ts index 562994ae8..f73d0d91d 100644 --- a/src/utils/__tests__/litegraphTestUtils.ts +++ b/src/utils/__tests__/litegraphTestUtils.ts @@ -1,11 +1,17 @@ -import type { Positionable } from '@/lib/litegraph/src/interfaces' +import type { + INodeInputSlot, + INodeOutputSlot, + Positionable +} from '@/lib/litegraph/src/interfaces' import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' import type { + CanvasPointerEvent, + LGraph, LGraphCanvas, LGraphGroup, - LGraphNode + LinkNetwork } from '@/lib/litegraph/src/litegraph' -import { LGraphEventMode } from '@/lib/litegraph/src/litegraph' +import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph' import { vi } from 'vitest' /** @@ -84,3 +90,114 @@ export function createMockCanvas( ...overrides } as LGraphCanvas } + +/** + * Creates a mock LGraph with trigger function + */ +export function createMockLGraph(overrides: Partial = {}): LGraph { + return { + trigger: vi.fn(), + ...overrides + } as LGraph +} + +/** + * Creates a mock CanvasPointerEvent + */ +export function createMockCanvasPointerEvent( + canvasX: number, + canvasY: number, + overrides: Partial = {} +): CanvasPointerEvent { + return { + canvasX, + canvasY, + ...overrides + } as CanvasPointerEvent +} + +/** + * Creates a mock CanvasRenderingContext2D + */ +export function createMockCanvasRenderingContext2D( + overrides: Partial = {} +): CanvasRenderingContext2D { + const partial: Partial = { + measureText: vi.fn(() => ({ width: 10 }) as TextMetrics), + ...overrides + } + return partial as CanvasRenderingContext2D +} + +/** + * Creates a mock LinkNetwork + */ +export function createMockLinkNetwork( + overrides: Partial = {} +): LinkNetwork { + return { + ...overrides + } as LinkNetwork +} + +/** + * Creates a mock INodeInputSlot + */ +export function createMockNodeInputSlot( + overrides: Partial = {} +): INodeInputSlot { + return { + ...overrides + } as INodeInputSlot +} + +/** + * Creates a mock INodeOutputSlot + */ +export function createMockNodeOutputSlot( + overrides: Partial = {} +): INodeOutputSlot { + return { + ...overrides + } as INodeOutputSlot +} + +/** + * Creates a real LGraphNode instance (not a lightweight mock) with its boundingRect + * property represented as a Float64Array for testing position methods. + * + * Use createMockLGraphNodeWithArrayBoundingRect when: + * - Tests rely on Float64Array boundingRect behavior + * - Tests call position-related methods like updateArea() + * - Tests need actual LGraphNode implementation details + * + * Use createMockLGraphNode when: + * - Tests only need simple/mock-only behavior + * - Tests don't depend on boundingRect being a Float64Array + * - A lightweight mock with minimal properties is sufficient + * + * @param name - The node name/type to pass to the LGraphNode constructor + * @returns A fully constructed LGraphNode instance with Float64Array boundingRect + */ +export function createMockLGraphNodeWithArrayBoundingRect( + name: string +): LGraphNode { + const node = new LGraphNode(name) + // The actual node has a Float64Array boundingRect, we just need to type it correctly + return node +} + +/** + * Creates a mock FileList from an array of files + */ +export function createMockFileList(files: File[]): FileList { + const fileList = { + ...files, + length: files.length, + item: (index: number) => files[index] ?? null, + [Symbol.iterator]: function* () { + yield* files + } + } + return fileList as FileList +}