Files
ComfyUI_frontend/src/composables/useLoad3dDrag.test.ts
Terry Jia 9ecbb3af27 Feat/3d dropdown (#8765)
## Summary
Add mesh_upload and upload_subfolder to combo input schema so
WidgetSelect detects mesh uploads generically instead of hardcoding node
type checks. Inject these flags in load3dLazy.ts so they are available
before THREE.js loads.

Also unify SUPPORTED_EXTENSIONS_ACCEPT across load3d and dropdown, pass
uploadSubfolder prop through to WidgetSelectDropdown for correct upload
path, and update error message to list all supported extensions.

replacement for https://github.com/Comfy-Org/ComfyUI_frontend/pull/7975

(We should include thumbnail but not yet, will do it later)

## Screenshots (if applicable)


https://github.com/user-attachments/assets/2cb4b1da-af4f-439b-9786-3ac780c2480d

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8765-Feat-3d-dropdown-3036d73d365081d8a10ee19d3ed7d295)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Kelly Yang <124ykl@gmail.com>
2026-02-10 15:36:57 -05:00

272 lines
7.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import { SUPPORTED_EXTENSIONS } from '@/extensions/core/load3d/constants'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { createMockFileList } from '@/utils/__tests__/litegraphTestUtils'
vi.mock('@/platform/updates/common/toastStore', () => ({
useToastStore: vi.fn()
}))
vi.mock('@/i18n', () => ({
t: vi.fn((key) => key)
}))
function createMockDragEvent(
type: string,
options: { hasFiles?: boolean; files?: File[] } = {}
): DragEvent {
const files = options.files || []
const types = options.hasFiles ? ['Files'] : []
const dataTransfer: Partial<DataTransfer> = {
types,
files: createMockFileList(files),
dropEffect: 'none' as DataTransfer['dropEffect']
}
const event: Partial<DragEvent> = {
type,
dataTransfer: dataTransfer as DataTransfer
}
return event as DragEvent
}
describe('useLoad3dDrag', () => {
let mockToastStore: ReturnType<typeof useToastStore>
let mockOnModelDrop: (file: File) => void | Promise<void>
beforeEach(() => {
vi.clearAllMocks()
mockToastStore = {
addAlert: vi.fn()
} as Partial<ReturnType<typeof useToastStore>> as ReturnType<
typeof useToastStore
>
vi.mocked(useToastStore).mockReturnValue(mockToastStore)
mockOnModelDrop = vi.fn()
})
it('should initialize with default state', () => {
const { isDragging, dragMessage } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
expect(isDragging.value).toBe(false)
expect(dragMessage.value).toBe('')
})
describe('handleDragOver', () => {
it('should set isDragging to true when files are being dragged', () => {
const { isDragging, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
const event = createMockDragEvent('dragover', { hasFiles: true })
handleDragOver(event)
expect(isDragging.value).toBe(true)
expect(event.dataTransfer!.dropEffect).toBe('copy')
})
it('should not set isDragging when disabled', () => {
const disabled = ref(true)
const { isDragging, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop,
disabled
})
const event = createMockDragEvent('dragover', { hasFiles: true })
handleDragOver(event)
expect(isDragging.value).toBe(false)
})
it('should not set isDragging when no files are being dragged', () => {
const { isDragging, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
const event = createMockDragEvent('dragover', { hasFiles: false })
handleDragOver(event)
expect(isDragging.value).toBe(false)
})
})
describe('handleDragLeave', () => {
it('should reset isDragging to false', () => {
const { isDragging, handleDragLeave, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
// First set isDragging to true
const dragOverEvent = createMockDragEvent('dragover', { hasFiles: true })
handleDragOver(dragOverEvent)
expect(isDragging.value).toBe(true)
// Then test dragleave
handleDragLeave()
expect(isDragging.value).toBe(false)
})
})
describe('handleDrop', () => {
it('should call onModelDrop with valid model file', async () => {
const { handleDrop } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
const modelFile = new File([], 'model.glb', { type: 'model/gltf-binary' })
const event = createMockDragEvent('drop', {
hasFiles: true,
files: [modelFile]
})
await handleDrop(event)
expect(mockOnModelDrop).toHaveBeenCalledWith(modelFile)
})
it('should show error toast for unsupported file types', async () => {
const { handleDrop } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
const invalidFile = new File([], 'image.png', { type: 'image/png' })
const event = createMockDragEvent('drop', {
hasFiles: true,
files: [invalidFile]
})
await handleDrop(event)
expect(mockOnModelDrop).not.toHaveBeenCalled()
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
'load3d.unsupportedFileType'
)
})
it('should not call onModelDrop when disabled', async () => {
const disabled = ref(true)
const { handleDrop } = useLoad3dDrag({
onModelDrop: mockOnModelDrop,
disabled
})
const modelFile = new File([], 'model.glb', { type: 'model/gltf-binary' })
const event = createMockDragEvent('drop', {
hasFiles: true,
files: [modelFile]
})
await handleDrop(event)
expect(mockOnModelDrop).not.toHaveBeenCalled()
})
it('should reset isDragging after drop', async () => {
const { isDragging, handleDrop, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
// Set isDragging to true
const dragOverEvent = createMockDragEvent('dragover', { hasFiles: true })
handleDragOver(dragOverEvent)
expect(isDragging.value).toBe(true)
// Drop the file
const modelFile = new File([], 'model.glb', { type: 'model/gltf-binary' })
const dropEvent = createMockDragEvent('drop', {
hasFiles: true,
files: [modelFile]
})
await handleDrop(dropEvent)
expect(isDragging.value).toBe(false)
})
it('should support all valid 3D model extensions', async () => {
const { handleDrop } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
const extensions = [...SUPPORTED_EXTENSIONS]
for (const ext of extensions) {
vi.mocked(mockOnModelDrop).mockClear()
const modelFile = new File([], `model${ext}`)
const event = createMockDragEvent('drop', {
hasFiles: true,
files: [modelFile]
})
await handleDrop(event)
expect(mockOnModelDrop).toHaveBeenCalledWith(modelFile)
}
})
it('should handle empty file list', async () => {
const { handleDrop } = useLoad3dDrag({
onModelDrop: mockOnModelDrop
})
const event = createMockDragEvent('drop', {
hasFiles: true,
files: []
})
await handleDrop(event)
expect(mockOnModelDrop).not.toHaveBeenCalled()
expect(mockToastStore.addAlert).not.toHaveBeenCalled()
})
})
describe('disabled option', () => {
it('should work with reactive disabled ref', () => {
const disabled = ref(false)
const { isDragging, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop,
disabled
})
const event = createMockDragEvent('dragover', { hasFiles: true })
// Should work when disabled is false
handleDragOver(event)
expect(isDragging.value).toBe(true)
// Reset
isDragging.value = false
// Should not work when disabled is true
disabled.value = true
handleDragOver(event)
expect(isDragging.value).toBe(false)
})
it('should work with plain boolean', () => {
const { isDragging, handleDragOver } = useLoad3dDrag({
onModelDrop: mockOnModelDrop,
disabled: false
})
const event = createMockDragEvent('dragover', { hasFiles: true })
handleDragOver(event)
expect(isDragging.value).toBe(true)
})
})
})