mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
[refactor] Use Pinia store for file handler registration
- Create fileHandlerStore following ComfyUI's store patterns - Move all handler registrations to coreFileHandlers.ts - Clean up fileHandlers.ts to only contain implementations - Update app.ts to use the new store-based system - Remove duplicate isApiJson function - Update tests for new architecture This addresses review feedback about registration patterns while preserving the lazy evaluation fix for NaN parsing.
This commit is contained in:
118
src/extensions/core/coreFileHandlers.ts
Normal file
118
src/extensions/core/coreFileHandlers.ts
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
107
src/stores/fileHandlerStore.ts
Normal file
107
src/stores/fileHandlerStore.ts
Normal file
@@ -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<WorkflowFileMetadata>
|
||||
|
||||
/**
|
||||
* 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<FileHandlerDefinition[]>([])
|
||||
const mimeTypeMap = ref(new Map<string, FileHandlerDefinition>())
|
||||
const extensionMap = ref(new Map<string, FileHandlerDefinition>())
|
||||
|
||||
// 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
|
||||
}
|
||||
})
|
||||
@@ -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<WorkflowFileMetadata>
|
||||
|
||||
/**
|
||||
* Maps MIME types to file handlers for loading workflows from different file formats
|
||||
*/
|
||||
export const mimeTypeHandlers = new Map<string, WorkflowFileHandler>()
|
||||
|
||||
/**
|
||||
* Maps file extensions to file handlers for loading workflows
|
||||
* Used as a fallback when MIME type detection fails
|
||||
*/
|
||||
export const extensionHandlers = new Map<string, WorkflowFileHandler>()
|
||||
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<string, any>).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
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user