From 3cde17587af037155d549d3862ecbc37ee778c21 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 20 Jan 2026 01:40:16 +0100 Subject: [PATCH] test: remove any types from useLoad3d.test.ts Replace all 10 `any` types with proper TypeScript types: - Use IWidget for widget mocks with correct type properties - Use Size for onResize handler parameter - Use CanvasRenderingContext2D for onDrawBackground - Use CanvasPointerEvent for mouse event handlers - Use Partial for partial mock objects (Load3d, LGraph, ToastStore) - Use shallowRef for LGraphNode ref typing - Replace type hacks (as unknown as never) with proper types All tests passing, 0 typecheck errors. --- src/composables/useLoad3d.test.ts | 171 ++++++++++++++++++------------ 1 file changed, 105 insertions(+), 66 deletions(-) diff --git a/src/composables/useLoad3d.test.ts b/src/composables/useLoad3d.test.ts index 833e95efd..c90bfe893 100644 --- a/src/composables/useLoad3d.test.ts +++ b/src/composables/useLoad3d.test.ts @@ -1,9 +1,14 @@ 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 { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import type { IWidget } from '@/lib/litegraph/src/types/widgets' import { useToastStore } from '@/platform/updates/common/toastStore' import { api } from '@/scripts/api' @@ -36,9 +41,18 @@ vi.mock('@/i18n', () => ({ })) describe('useLoad3d', () => { - let mockLoad3d: any - let mockNode: any - let mockToastStore: any + let mockLoad3d: Partial + let mockNode: LGraphNode + let mockToastStore: ReturnType + + const createMockPointerEvent = (): CanvasPointerEvent => { + return { + canvasX: 0, + canvasY: 0, + deltaX: 0, + deltaY: 0 + } as CanvasPointerEvent + } beforeEach(() => { vi.clearAllMocks() @@ -68,18 +82,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 + } as unknown as LGraphNode + + const mockCanvas = document.createElement('canvas') + mockCanvas.hidden = false mockLoad3d = { toggleGrid: vi.fn(), @@ -114,19 +131,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 +226,14 @@ describe('useLoad3d', () => { expect(mockNode.onDrawBackground).toBeDefined() // Test the handlers - mockNode.onMouseEnter() + mockNode.onMouseEnter?.(createMockPointerEvent()) expect(mockLoad3d.refreshViewport).toHaveBeenCalled() expect(mockLoad3d.updateStatusMouseOnNode).toHaveBeenCalledWith(true) - mockNode.onMouseLeave() + mockNode.onMouseLeave?.(createMockPointerEvent()) expect(mockLoad3d.updateStatusMouseOnNode).toHaveBeenCalledWith(false) - mockNode.onResize() + mockNode.onResize?.([512, 512] as Size) expect(mockLoad3d.handleResize).toHaveBeenCalled() }) @@ -226,13 +244,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 +277,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 +338,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 +396,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 +429,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 +452,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 +475,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 +615,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 +667,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 +747,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 +762,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 +786,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 +811,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 +842,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 +859,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 +893,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 +929,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 +944,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')