diff --git a/src/extensions/core/coreFileHandlers.ts b/src/extensions/core/coreFileHandlers.ts new file mode 100644 index 000000000..06ad2ddd3 --- /dev/null +++ b/src/extensions/core/coreFileHandlers.ts @@ -0,0 +1,118 @@ +/** + * Core file handlers registration for ComfyUI + * This file registers all built-in file handlers with the fileHandlerStore + */ +import { useFileHandlerStore } from '@/stores/fileHandlerStore' +import { + handlePngFile, + handleWebpFile, + handleSvgFile, + handleJsonFile, + handleMp3File, + handleOggFile, + handleFlacFile, + handleWebmFile, + handleMp4File, + handleGlbFile, + handleLatentFile +} from '@/utils/fileHandlers' + +/** + * Register all core file handlers with the store + */ +export function registerCoreFileHandlers() { + const fileHandlerStore = useFileHandlerStore() + + // Image handlers + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.png', + displayName: 'PNG Image', + mimeTypes: ['image/png'], + extensions: ['.png'], + handler: handlePngFile + }) + + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.webp', + displayName: 'WebP Image', + mimeTypes: ['image/webp'], + extensions: ['.webp'], + handler: handleWebpFile + }) + + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.svg', + displayName: 'SVG Image', + mimeTypes: ['image/svg+xml'], + extensions: ['.svg'], + handler: handleSvgFile + }) + + // Audio handlers + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.mp3', + displayName: 'MP3 Audio', + mimeTypes: ['audio/mpeg'], + extensions: ['.mp3'], + handler: handleMp3File + }) + + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.ogg', + displayName: 'OGG Audio', + mimeTypes: ['audio/ogg'], + extensions: ['.ogg'], + handler: handleOggFile + }) + + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.flac', + displayName: 'FLAC Audio', + mimeTypes: ['audio/flac', 'audio/x-flac'], + extensions: ['.flac'], + handler: handleFlacFile + }) + + // Video handlers + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.webm', + displayName: 'WebM Video', + mimeTypes: ['video/webm'], + extensions: ['.webm'], + handler: handleWebmFile + }) + + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.mp4', + displayName: 'MP4 Video', + mimeTypes: ['video/mp4', 'video/quicktime', 'video/x-m4v'], + extensions: ['.mp4', '.mov', '.m4v'], + handler: handleMp4File + }) + + // Model handlers + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.glb', + displayName: 'GLB 3D Model', + mimeTypes: ['model/gltf-binary'], + extensions: ['.glb'], + handler: handleGlbFile + }) + + // Data handlers + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.json', + displayName: 'JSON Data', + mimeTypes: ['application/json'], + extensions: ['.json'], + handler: handleJsonFile + }) + + fileHandlerStore.registerFileHandler({ + id: 'comfy.fileHandler.latent', + displayName: 'Latent/Safetensors', + mimeTypes: [], + extensions: ['.latent', '.safetensors'], + handler: handleLatentFile + }) +} \ No newline at end of file diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 5011587f8..b4691bd6d 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -54,7 +54,8 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy' import { ExtensionManager } from '@/types/extensionTypes' import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil' import { graphToPrompt } from '@/utils/executionUtil' -import { getFileHandler } from '@/utils/fileHandlers' +import { useFileHandlerStore } from '@/stores/fileHandlerStore' +import { registerCoreFileHandlers } from '@/extensions/core/coreFileHandlers' import { executeWidgetsCallback, fixLinkInputSlots, @@ -749,6 +750,9 @@ export class ComfyApp { await useWorkspaceStore().workflow.syncWorkflows() await useExtensionService().loadExtensions() + // Register core file handlers + registerCoreFileHandlers() + this.#addProcessKeyHandler() this.#addConfigureHandler() this.#addApiUpdateHandlers() @@ -1322,7 +1326,8 @@ export class ComfyApp { } const fileName = removeExt(file.name) - const handler = getFileHandler(file) + const fileHandlerStore = useFileHandlerStore() + const handler = fileHandlerStore.getHandlerForFile(file) if (!handler) { this.showErrorOnFileLoad(file) return diff --git a/src/stores/fileHandlerStore.ts b/src/stores/fileHandlerStore.ts new file mode 100644 index 000000000..7a23407db --- /dev/null +++ b/src/stores/fileHandlerStore.ts @@ -0,0 +1,107 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { + ComfyApiWorkflow, + ComfyWorkflowJSON +} from '@/schemas/comfyWorkflowSchema' + +/** + * Type for the raw file metadata with lazy parsing + */ +export type WorkflowFileMetadata = { + workflow?: () => ComfyWorkflowJSON | undefined + prompt?: () => ComfyApiWorkflow | undefined + parameters?: string + jsonTemplateData?: () => unknown +} + +/** + * Type for the file handler function + */ +export type WorkflowFileHandler = (file: File) => Promise + +/** + * Definition for a file handler including metadata + */ +export interface FileHandlerDefinition { + id: string + displayName: string + mimeTypes?: string[] + extensions?: string[] + handler: WorkflowFileHandler + priority?: number +} + +/** + * Store for managing file handlers that can load workflows from various file formats + */ +export const useFileHandlerStore = defineStore('fileHandler', () => { + // State + const handlers = ref([]) + const mimeTypeMap = ref(new Map()) + const extensionMap = ref(new Map()) + + // Actions + function registerFileHandler(definition: FileHandlerDefinition) { + const existing = handlers.value.find((h) => h.id === definition.id) + if (existing) { + console.warn(`File handler ${definition.id} already registered`) + return + } + + handlers.value.push(definition) + + // Update MIME type mappings + definition.mimeTypes?.forEach((mimeType) => { + const current = mimeTypeMap.value.get(mimeType) + if (!current || (definition.priority ?? 0) > (current.priority ?? 0)) { + mimeTypeMap.value.set(mimeType, definition) + } + }) + + // Update extension mappings + definition.extensions?.forEach((ext) => { + const current = extensionMap.value.get(ext) + if (!current || (definition.priority ?? 0) > (current.priority ?? 0)) { + extensionMap.value.set(ext, definition) + } + }) + } + + function getHandlerForFile(file: File): WorkflowFileHandler | null { + // Try MIME type first + if (file.type) { + const definition = mimeTypeMap.value.get(file.type) + if (definition) return definition.handler + } + + // Fall back to extension + if (file.name) { + const ext = '.' + file.name.split('.').pop()?.toLowerCase() + const definition = extensionMap.value.get(ext) + if (definition) return definition.handler + } + + return null + } + + // Getters + const registeredHandlers = computed(() => handlers.value) + const supportedMimeTypes = computed(() => + Array.from(mimeTypeMap.value.keys()) + ) + const supportedExtensions = computed(() => + Array.from(extensionMap.value.keys()) + ) + + return { + // State (read-only) + handlers: registeredHandlers, + supportedMimeTypes, + supportedExtensions, + + // Actions + registerFileHandler, + getHandlerForFile + } +}) \ No newline at end of file diff --git a/src/utils/fileHandlers.ts b/src/utils/fileHandlers.ts index 3e7b293d2..b899c95e9 100644 --- a/src/utils/fileHandlers.ts +++ b/src/utils/fileHandlers.ts @@ -1,19 +1,8 @@ /** - * Maps MIME types and file extensions to handler functions for extracting - * workflow data from various file formats. Uses supportedWorkflowFormats.ts - * as the source of truth for supported formats. + * File handlers for extracting workflow data from various file formats. + * This module contains only the handler implementations. + * Registration is handled through the fileHandlerStore. */ -import { - AUDIO_WORKFLOW_FORMATS, - DATA_WORKFLOW_FORMATS, - IMAGE_WORKFLOW_FORMATS, - MODEL_WORKFLOW_FORMATS, - VIDEO_WORKFLOW_FORMATS -} from '@/constants/supportedWorkflowFormats' -import type { - ComfyApiWorkflow, - ComfyWorkflowJSON -} from '@/schemas/comfyWorkflowSchema' import { getFromWebmFile } from '@/scripts/metadata/ebml' import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf' import { getFromIsobmffFile } from '@/scripts/metadata/isobmff' @@ -26,37 +15,12 @@ import { getPngMetadata, getWebpMetadata } from '@/scripts/pnginfo' - -/** - * Type for the raw file metadata with lazy parsing - */ -export type WorkflowFileMetadata = { - workflow?: () => ComfyWorkflowJSON | undefined - prompt?: () => ComfyApiWorkflow | undefined - parameters?: string - jsonTemplateData?: () => unknown -} - -/** - * Type for the file handler function - */ -export type WorkflowFileHandler = (file: File) => Promise - -/** - * Maps MIME types to file handlers for loading workflows from different file formats - */ -export const mimeTypeHandlers = new Map() - -/** - * Maps file extensions to file handlers for loading workflows - * Used as a fallback when MIME type detection fails - */ -export const extensionHandlers = new Map() +import type { WorkflowFileHandler } from '@/stores/fileHandlerStore' /** * Handler for PNG files */ -const handlePngFile: WorkflowFileHandler = async (file) => { +export const handlePngFile: WorkflowFileHandler = async (file) => { const pngInfo = await getPngMetadata(file) return { workflow: () => @@ -69,7 +33,7 @@ const handlePngFile: WorkflowFileHandler = async (file) => { /** * Handler for WebP files */ -const handleWebpFile: WorkflowFileHandler = async (file) => { +export const handleWebpFile: WorkflowFileHandler = async (file) => { const pngInfo = await getWebpMetadata(file) const workflow = pngInfo?.workflow || pngInfo?.Workflow const prompt = pngInfo?.prompt || pngInfo?.Prompt @@ -83,7 +47,7 @@ const handleWebpFile: WorkflowFileHandler = async (file) => { /** * Handler for SVG files */ -const handleSvgFile: WorkflowFileHandler = async (file) => { +export const handleSvgFile: WorkflowFileHandler = async (file) => { const svgInfo = await getSvgMetadata(file) return { workflow: () => svgInfo.workflow, @@ -94,7 +58,7 @@ const handleSvgFile: WorkflowFileHandler = async (file) => { /** * Handler for MP3 files */ -const handleMp3File: WorkflowFileHandler = async (file) => { +export const handleMp3File: WorkflowFileHandler = async (file) => { const { workflow, prompt } = await getMp3Metadata(file) return { workflow: () => workflow, @@ -105,7 +69,7 @@ const handleMp3File: WorkflowFileHandler = async (file) => { /** * Handler for OGG files */ -const handleOggFile: WorkflowFileHandler = async (file) => { +export const handleOggFile: WorkflowFileHandler = async (file) => { const { workflow, prompt } = await getOggMetadata(file) return { workflow: () => workflow, @@ -116,7 +80,7 @@ const handleOggFile: WorkflowFileHandler = async (file) => { /** * Handler for FLAC files */ -const handleFlacFile: WorkflowFileHandler = async (file) => { +export const handleFlacFile: WorkflowFileHandler = async (file) => { const pngInfo = await getFlacMetadata(file) const workflow = pngInfo?.workflow || pngInfo?.Workflow const prompt = pngInfo?.prompt || pngInfo?.Prompt @@ -130,7 +94,7 @@ const handleFlacFile: WorkflowFileHandler = async (file) => { /** * Handler for WebM files */ -const handleWebmFile: WorkflowFileHandler = async (file) => { +export const handleWebmFile: WorkflowFileHandler = async (file) => { const webmInfo = await getFromWebmFile(file) return { workflow: () => webmInfo.workflow, @@ -141,7 +105,7 @@ const handleWebmFile: WorkflowFileHandler = async (file) => { /** * Handler for MP4/MOV/M4V files */ -const handleMp4File: WorkflowFileHandler = async (file) => { +export const handleMp4File: WorkflowFileHandler = async (file) => { const mp4Info = await getFromIsobmffFile(file) return { workflow: () => mp4Info.workflow, @@ -152,7 +116,7 @@ const handleMp4File: WorkflowFileHandler = async (file) => { /** * Handler for GLB files */ -const handleGlbFile: WorkflowFileHandler = async (file) => { +export const handleGlbFile: WorkflowFileHandler = async (file) => { const gltfInfo = await getGltfBinaryMetadata(file) return { workflow: () => gltfInfo.workflow, @@ -163,7 +127,7 @@ const handleGlbFile: WorkflowFileHandler = async (file) => { /** * Handler for JSON files */ -const handleJsonFile: WorkflowFileHandler = async (file) => { +export const handleJsonFile: WorkflowFileHandler = async (file) => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => { @@ -186,7 +150,7 @@ const handleJsonFile: WorkflowFileHandler = async (file) => { /** * Handler for .latent and .safetensors files */ -const handleLatentFile: WorkflowFileHandler = async (file) => { +export const handleLatentFile: WorkflowFileHandler = async (file) => { const info = await getLatentMetadata(file) return { @@ -210,125 +174,3 @@ const handleLatentFile: WorkflowFileHandler = async (file) => { } } -/** - * Helper function to determine if a JSON object is in the API JSON format - */ -export function isApiJson(data: unknown) { - return ( - typeof data === 'object' && - data !== null && - Object.keys(data).length > 0 && - Object.values(data as Record).every((v) => v.class_type) - ) -} - -// Register image format handlers -IMAGE_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - if (mimeType === 'image/png') { - mimeTypeHandlers.set(mimeType, handlePngFile) - } else if (mimeType === 'image/webp') { - mimeTypeHandlers.set(mimeType, handleWebpFile) - } else if (mimeType === 'image/svg+xml') { - mimeTypeHandlers.set(mimeType, handleSvgFile) - } -}) - -IMAGE_WORKFLOW_FORMATS.extensions.forEach((ext) => { - if (ext === '.png') { - extensionHandlers.set(ext, handlePngFile) - } else if (ext === '.webp') { - extensionHandlers.set(ext, handleWebpFile) - } else if (ext === '.svg') { - extensionHandlers.set(ext, handleSvgFile) - } -}) - -// Register audio format handlers -AUDIO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - if (mimeType === 'audio/mpeg') { - mimeTypeHandlers.set(mimeType, handleMp3File) - } else if (mimeType === 'audio/ogg') { - mimeTypeHandlers.set(mimeType, handleOggFile) - } else if (mimeType === 'audio/flac' || mimeType === 'audio/x-flac') { - mimeTypeHandlers.set(mimeType, handleFlacFile) - } -}) - -AUDIO_WORKFLOW_FORMATS.extensions.forEach((ext) => { - if (ext === '.mp3') { - extensionHandlers.set(ext, handleMp3File) - } else if (ext === '.ogg') { - extensionHandlers.set(ext, handleOggFile) - } else if (ext === '.flac') { - extensionHandlers.set(ext, handleFlacFile) - } -}) - -// Register video format handlers -VIDEO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - if (mimeType === 'video/webm') { - mimeTypeHandlers.set(mimeType, handleWebmFile) - } else if ( - mimeType === 'video/mp4' || - mimeType === 'video/quicktime' || - mimeType === 'video/x-m4v' - ) { - mimeTypeHandlers.set(mimeType, handleMp4File) - } -}) - -VIDEO_WORKFLOW_FORMATS.extensions.forEach((ext) => { - if (ext === '.webm') { - extensionHandlers.set(ext, handleWebmFile) - } else if (ext === '.mp4' || ext === '.mov' || ext === '.m4v') { - extensionHandlers.set(ext, handleMp4File) - } -}) - -// Register 3D model format handlers -MODEL_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - if (mimeType === 'model/gltf-binary') { - mimeTypeHandlers.set(mimeType, handleGlbFile) - } -}) - -MODEL_WORKFLOW_FORMATS.extensions.forEach((ext) => { - if (ext === '.glb') { - extensionHandlers.set(ext, handleGlbFile) - } -}) - -// Register data format handlers -DATA_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - if (mimeType === 'application/json') { - mimeTypeHandlers.set(mimeType, handleJsonFile) - } -}) - -DATA_WORKFLOW_FORMATS.extensions.forEach((ext) => { - if (ext === '.json') { - extensionHandlers.set(ext, handleJsonFile) - } else if (ext === '.latent' || ext === '.safetensors') { - extensionHandlers.set(ext, handleLatentFile) - } -}) - -/** - * Gets the appropriate file handler for a given file based on mime type or extension - */ -export function getFileHandler(file: File): WorkflowFileHandler | null { - // First try to match by MIME type - if (file.type && mimeTypeHandlers.has(file.type)) { - return mimeTypeHandlers.get(file.type) || null - } - - // If no MIME type match, try to match by file extension - if (file.name) { - const extension = '.' + file.name.split('.').pop()?.toLowerCase() - if (extension && extensionHandlers.has(extension)) { - return extensionHandlers.get(extension) || null - } - } - - return null -} diff --git a/tests-ui/tests/utils/fileHandlers.test.ts b/tests-ui/tests/utils/fileHandlers.test.ts index 56a9f0bb3..8a3594b9d 100644 --- a/tests-ui/tests/utils/fileHandlers.test.ts +++ b/tests-ui/tests/utils/fileHandlers.test.ts @@ -1,17 +1,14 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' import { - AUDIO_WORKFLOW_FORMATS, - DATA_WORKFLOW_FORMATS, - IMAGE_WORKFLOW_FORMATS, - MODEL_WORKFLOW_FORMATS, - VIDEO_WORKFLOW_FORMATS -} from '../../../src/constants/supportedWorkflowFormats' + useFileHandlerStore, + type FileHandlerDefinition +} from '../../../src/stores/fileHandlerStore' import { - extensionHandlers, - getFileHandler, - isApiJson, - mimeTypeHandlers + handlePngFile, + handleWebpFile, + handleJsonFile } from '../../../src/utils/fileHandlers' // Mock the metadata functions @@ -59,121 +56,143 @@ vi.mock('../../../src/scripts/metadata/gltf', () => ({ .mockResolvedValue({ workflow: { test: 'workflow' } }) })) -describe('fileHandlers', () => { - describe('handler registrations', () => { - it('should register handlers for all image MIME types', () => { - IMAGE_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - expect(mimeTypeHandlers.has(mimeType)).toBe(true) - expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function') - }) +describe('File Handler Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + describe('Registration', () => { + it('should register file handlers correctly', () => { + const store = useFileHandlerStore() + + const definition: FileHandlerDefinition = { + id: 'test.handler', + displayName: 'Test Handler', + mimeTypes: ['image/png'], + extensions: ['.png'], + handler: handlePngFile + } + + store.registerFileHandler(definition) + + expect(store.handlers).toHaveLength(1) + expect(store.handlers[0].id).toBe('test.handler') + expect(store.supportedMimeTypes).toContain('image/png') + expect(store.supportedExtensions).toContain('.png') }) - it('should register handlers for all image extensions', () => { - IMAGE_WORKFLOW_FORMATS.extensions.forEach((ext) => { - expect(extensionHandlers.has(ext)).toBe(true) - expect(extensionHandlers.get(ext)).toBeTypeOf('function') - }) + it('should not register duplicate handlers', () => { + const store = useFileHandlerStore() + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const definition: FileHandlerDefinition = { + id: 'test.handler', + displayName: 'Test Handler', + mimeTypes: ['image/png'], + extensions: ['.png'], + handler: handlePngFile + } + + store.registerFileHandler(definition) + store.registerFileHandler(definition) // Try to register again + + expect(store.handlers).toHaveLength(1) + expect(consoleSpy).toHaveBeenCalledWith('File handler test.handler already registered') + + consoleSpy.mockRestore() }) - it('should register handlers for all audio MIME types', () => { - AUDIO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - expect(mimeTypeHandlers.has(mimeType)).toBe(true) - expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function') - }) - }) + it('should handle priority correctly', () => { + const store = useFileHandlerStore() - it('should register handlers for all audio extensions', () => { - AUDIO_WORKFLOW_FORMATS.extensions.forEach((ext) => { - expect(extensionHandlers.has(ext)).toBe(true) - expect(extensionHandlers.get(ext)).toBeTypeOf('function') + // Register low priority handler first + store.registerFileHandler({ + id: 'low.priority', + displayName: 'Low Priority', + mimeTypes: ['image/png'], + extensions: ['.png'], + handler: handlePngFile, + priority: 1 }) - }) - it('should register handlers for all video MIME types', () => { - VIDEO_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - expect(mimeTypeHandlers.has(mimeType)).toBe(true) - expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function') + // Register high priority handler + store.registerFileHandler({ + id: 'high.priority', + displayName: 'High Priority', + mimeTypes: ['image/png'], + extensions: ['.png'], + handler: handleWebpFile, + priority: 10 }) - }) - it('should register handlers for all video extensions', () => { - VIDEO_WORKFLOW_FORMATS.extensions.forEach((ext) => { - expect(extensionHandlers.has(ext)).toBe(true) - expect(extensionHandlers.get(ext)).toBeTypeOf('function') - }) - }) + const handler = store.getHandlerForFile( + new File([''], 'test.png', { type: 'image/png' }) + ) - it('should register handlers for all model MIME types', () => { - MODEL_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - expect(mimeTypeHandlers.has(mimeType)).toBe(true) - expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function') - }) - }) - - it('should register handlers for all model extensions', () => { - MODEL_WORKFLOW_FORMATS.extensions.forEach((ext) => { - expect(extensionHandlers.has(ext)).toBe(true) - expect(extensionHandlers.get(ext)).toBeTypeOf('function') - }) - }) - - it('should register handlers for all data MIME types', () => { - DATA_WORKFLOW_FORMATS.mimeTypes.forEach((mimeType) => { - expect(mimeTypeHandlers.has(mimeType)).toBe(true) - expect(mimeTypeHandlers.get(mimeType)).toBeTypeOf('function') - }) - }) - - it('should register handlers for all data extensions', () => { - DATA_WORKFLOW_FORMATS.extensions.forEach((ext) => { - expect(extensionHandlers.has(ext)).toBe(true) - expect(extensionHandlers.get(ext)).toBeTypeOf('function') - }) + expect(handler).toBe(handleWebpFile) // Should return high priority handler }) }) - describe('getFileHandler', () => { - it('should return handler based on MIME type', () => { + describe('File Handler Lookup', () => { + beforeEach(() => { + const store = useFileHandlerStore() + + // Register some test handlers + store.registerFileHandler({ + id: 'png.handler', + displayName: 'PNG Handler', + mimeTypes: ['image/png'], + extensions: ['.png'], + handler: handlePngFile + }) + + store.registerFileHandler({ + id: 'webp.handler', + displayName: 'WebP Handler', + mimeTypes: ['image/webp'], + extensions: ['.webp'], + handler: handleWebpFile + }) + + store.registerFileHandler({ + id: 'json.handler', + displayName: 'JSON Handler', + mimeTypes: ['application/json'], + extensions: ['.json'], + handler: handleJsonFile + }) + }) + + it('should find handler by MIME type', () => { + const store = useFileHandlerStore() const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - expect(handler).toBeTruthy() - expect(handler).toBeTypeOf('function') + + const handler = store.getHandlerForFile(file) + expect(handler).toBe(handlePngFile) }) - it('should return handler based on file extension when MIME type is not available', () => { + it('should find handler by extension when MIME type is missing', () => { + const store = useFileHandlerStore() const file = new File([''], 'test.png', { type: '' }) - const handler = getFileHandler(file) - expect(handler).toBeTruthy() - expect(handler).toBeTypeOf('function') + + const handler = store.getHandlerForFile(file) + expect(handler).toBe(handlePngFile) }) - it('should return null for unsupported file types', () => { - const file = new File([''], 'test.xyz', { type: 'application/unknown' }) - const handler = getFileHandler(file) + it('should return null for unsupported files', () => { + const store = useFileHandlerStore() + const file = new File([''], 'test.txt', { type: 'text/plain' }) + + const handler = store.getHandlerForFile(file) expect(handler).toBeNull() }) - it('should prioritize MIME type over file extension', () => { - const file = new File([''], 'test.txt', { type: 'image/png' }) - const handler = getFileHandler(file) - expect(handler).toBe(mimeTypeHandlers.get('image/png')) - }) - }) - - describe('isApiJson', () => { - it('should return true for valid API JSON', () => { - const apiJson = { - '1': { class_type: 'Node1', inputs: {} }, - '2': { class_type: 'Node2', inputs: {} } - } - expect(isApiJson(apiJson)).toBe(true) - }) - - it('should return false for non-API JSON', () => { - expect(isApiJson({})).toBe(false) - expect(isApiJson({ nodes: [] })).toBe(false) - expect(isApiJson(null)).toBe(false) - expect(isApiJson('string')).toBe(false) + it('should handle files with mixed case extensions', () => { + const store = useFileHandlerStore() + const file = new File([''], 'test.PNG', { type: '' }) + + const handler = store.getHandlerForFile(file) + expect(handler).toBe(handlePngFile) }) }) @@ -186,8 +205,7 @@ describe('fileHandlers', () => { }) const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - const result = await handler!(file) + const result = await handlePngFile(file) expect(result.workflow).toBeTypeOf('function') expect(result.prompt).toBeTypeOf('function') @@ -208,8 +226,7 @@ describe('fileHandlers', () => { }) const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - const result = await handler!(file) + const result = await handlePngFile(file) // workflow should parse successfully const workflowData = result.workflow!() @@ -229,8 +246,7 @@ describe('fileHandlers', () => { }) const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - const result = await handler!(file) + const result = await handlePngFile(file) // Both should throw when called expect(() => result.workflow!()).toThrow() @@ -242,53 +258,17 @@ describe('fileHandlers', () => { vi.mocked(getPngMetadata).mockResolvedValueOnce({}) const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - const result = await handler!(file) + const result = await handlePngFile(file) expect(result.workflow!()).toBeUndefined() expect(result.prompt!()).toBeUndefined() }) - - it('should handle undefined metadata fields', async () => { - const { getPngMetadata } = await import('../../../src/scripts/pnginfo') - vi.mocked(getPngMetadata).mockResolvedValueOnce({ - workflow: undefined as any, - prompt: undefined as any, - parameters: 'some parameters' - }) - - const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - const result = await handler!(file) - - expect(result.workflow!()).toBeUndefined() - expect(result.prompt!()).toBeUndefined() - expect(result.parameters).toBe('some parameters') - }) - }) - - describe('WebP handler variations', () => { - it('should handle case-sensitive workflow field names', async () => { - const { getWebpMetadata } = await import('../../../src/scripts/pnginfo') - vi.mocked(getWebpMetadata).mockResolvedValueOnce({ - Workflow: '{"uppercase": true}', - Prompt: '{"uppercase": true}' - }) - - const file = new File([''], 'test.webp', { type: 'image/webp' }) - const handler = getFileHandler(file) - const result = await handler!(file) - - expect(result.workflow!()).toEqual({ uppercase: true }) - expect(result.prompt!()).toEqual({ uppercase: true }) - }) }) describe('JSON handler edge cases', () => { it('should handle empty JSON file', async () => { const file = new File(['{}'], 'empty.json', { type: 'application/json' }) - const handler = getFileHandler(file) - const result = await handler!(file) + const result = await handleJsonFile(file) expect(result.jsonTemplateData!()).toEqual({}) }) @@ -297,9 +277,9 @@ describe('fileHandlers', () => { const file = new File(['{invalid json}'], 'bad.json', { type: 'application/json' }) - const handler = getFileHandler(file) - const result = await handler!(file) - + + const result = await handleJsonFile(file) + // Should throw when calling the lazy functions expect(() => result.jsonTemplateData!()).toThrow() expect(() => result.workflow!()).toThrow() @@ -323,8 +303,7 @@ describe('fileHandlers', () => { }) const file = new File([''], 'test.png', { type: 'image/png' }) - const handler = getFileHandler(file) - const result = await handler!(file) + const result = await handlePngFile(file) // JSON.parse should not have been called yet expect(parseCallCount).toBe(0) @@ -341,20 +320,4 @@ describe('fileHandlers', () => { JSON.parse = originalParse }) }) - - describe('File extension fallback', () => { - it('should use file extension when MIME type is empty', () => { - const file = new File([''], 'test.png', { type: '' }) - const handler = getFileHandler(file) - expect(handler).toBe(extensionHandlers.get('.png')) - }) - - it('should use file extension when MIME type is application/octet-stream', () => { - const file = new File([''], 'test.mp3', { - type: 'application/octet-stream' - }) - const handler = getFileHandler(file) - expect(handler).toBe(extensionHandlers.get('.mp3')) - }) - }) -}) +}) \ No newline at end of file