Compare commits

...

3 Commits

Author SHA1 Message Date
bymyself
148bde4532 [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.
2025-07-04 16:50:14 -07:00
bymyself
e76cbba85e Fix no_workflow.webp test by using early returns
Ensure error toast displays when no valid workflow data exists in file
2025-07-04 16:50:14 -07:00
bymyself
f035cbcbd5 [bugfix] Fix PNG loading with NaN values in metadata
Restore lazy evaluation for metadata parsing to handle PNG files with NaN values in prompt field. Uses the original if-else loading logic to prevent parsing errors when workflow is valid but prompt contains invalid JSON.

Fixes #4199
2025-07-04 16:50:14 -07:00
5 changed files with 793 additions and 167 deletions

View 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
})
}

View File

@@ -30,12 +30,6 @@ import {
isComboInputSpecV1,
isComboInputSpecV2
} from '@/schemas/nodeDefSchema'
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
import { getMp3Metadata } from '@/scripts/metadata/mp3'
import { getOggMetadata } from '@/scripts/metadata/ogg'
import { getSvgMetadata } from '@/scripts/metadata/svg'
import { useDialogService } from '@/services/dialogService'
import { useExtensionService } from '@/services/extensionService'
import { useLitegraphService } from '@/services/litegraphService'
@@ -60,6 +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 { useFileHandlerStore } from '@/stores/fileHandlerStore'
import { registerCoreFileHandlers } from '@/extensions/core/coreFileHandlers'
import {
executeWidgetsCallback,
fixLinkInputSlots,
@@ -74,13 +70,7 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { type ComfyApi, PromptExecutionError, api } from './api'
import { defaultGraph } from './defaultGraph'
import {
getFlacMetadata,
getLatentMetadata,
getPngMetadata,
getWebpMetadata,
importA1111
} from './pnginfo'
import { importA1111 } from './pnginfo'
import { $el, ComfyUI } from './ui'
import { ComfyAppMenu } from './ui/menu/index'
import { clone } from './utils'
@@ -760,6 +750,9 @@ export class ComfyApp {
await useWorkspaceStore().workflow.syncWorkflows()
await useExtensionService().loadExtensions()
// Register core file handlers
registerCoreFileHandlers()
this.#addProcessKeyHandler()
this.#addConfigureHandler()
this.#addApiUpdateHandlers()
@@ -1332,163 +1325,72 @@ export class ComfyApp {
return f.substring(0, p)
}
const fileName = removeExt(file.name)
if (file.type === 'image/png') {
const pngInfo = await getPngMetadata(file)
if (pngInfo?.workflow) {
await this.loadGraphData(
JSON.parse(pngInfo.workflow),
true,
true,
fileName
)
} else if (pngInfo?.prompt) {
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName)
} else if (pngInfo?.parameters) {
// Note: Not putting this in `importA1111` as it is mostly not used
// by external callers, and `importA1111` has no access to `app`.
useWorkflowService().beforeLoadNewGraph()
importA1111(this.graph, pngInfo.parameters)
useWorkflowService().afterLoadNewGraph(
fileName,
this.graph.serialize() as unknown as ComfyWorkflowJSON
)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'image/webp') {
const pngInfo = await getWebpMetadata(file)
// Support loading workflows from that webp custom node.
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/mpeg') {
const { workflow, prompt } = await getMp3Metadata(file)
if (workflow) {
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/ogg') {
const { workflow, prompt } = await getOggMetadata(file)
if (workflow) {
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/flac' || file.type === 'audio/x-flac') {
const pngInfo = await getFlacMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'video/webm') {
const webmInfo = await getFromWebmFile(file)
if (webmInfo.workflow) {
this.loadGraphData(webmInfo.workflow, true, true, fileName)
} else if (webmInfo.prompt) {
this.loadApiJson(webmInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (
file.type === 'video/mp4' ||
file.name?.endsWith('.mp4') ||
file.name?.endsWith('.mov') ||
file.name?.endsWith('.m4v') ||
file.type === 'video/quicktime' ||
file.type === 'video/x-m4v'
) {
const mp4Info = await getFromIsobmffFile(file)
if (mp4Info.workflow) {
this.loadGraphData(mp4Info.workflow, true, true, fileName)
} else if (mp4Info.prompt) {
this.loadApiJson(mp4Info.prompt, fileName)
}
} else if (file.type === 'image/svg+xml' || file.name?.endsWith('.svg')) {
const svgInfo = await getSvgMetadata(file)
if (svgInfo.workflow) {
this.loadGraphData(svgInfo.workflow, true, true, fileName)
} else if (svgInfo.prompt) {
this.loadApiJson(svgInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (
file.type === 'model/gltf-binary' ||
file.name?.endsWith('.glb')
) {
const gltfInfo = await getGltfBinaryMetadata(file)
if (gltfInfo.workflow) {
this.loadGraphData(gltfInfo.workflow, true, true, fileName)
} else if (gltfInfo.prompt) {
this.loadApiJson(gltfInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (
file.type === 'application/json' ||
file.name?.endsWith('.json')
) {
const reader = new FileReader()
reader.onload = async () => {
const readerResult = reader.result as string
const jsonContent = JSON.parse(readerResult)
if (jsonContent?.templates) {
this.loadTemplateData(jsonContent)
} else if (this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent, fileName)
} else {
await this.loadGraphData(
JSON.parse(readerResult),
true,
true,
fileName
)
}
}
reader.readAsText(file)
} else if (
file.name?.endsWith('.latent') ||
file.name?.endsWith('.safetensors')
) {
const info = await getLatentMetadata(file)
// TODO define schema to LatentMetadata
// @ts-expect-error
if (info.workflow) {
await this.loadGraphData(
// @ts-expect-error
JSON.parse(info.workflow),
true,
true,
fileName
)
// @ts-expect-error
} else if (info.prompt) {
// @ts-expect-error
this.loadApiJson(JSON.parse(info.prompt))
} else {
this.showErrorOnFileLoad(file)
}
} else {
const fileHandlerStore = useFileHandlerStore()
const handler = fileHandlerStore.getHandlerForFile(file)
if (!handler) {
this.showErrorOnFileLoad(file)
return
}
const metadata = await handler(file)
// Special handling for JSON files
if (metadata.jsonTemplateData) {
const jsonContent = metadata.jsonTemplateData()
if (
jsonContent &&
typeof jsonContent === 'object' &&
'templates' in jsonContent
) {
this.loadTemplateData(jsonContent as any)
return
} else if (this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent as ComfyApiWorkflow, fileName)
return
} else {
// Regular workflow JSON
await this.loadGraphData(
jsonContent as ComfyWorkflowJSON,
true,
true,
fileName
)
return
}
}
// Try to load workflow first
if (metadata.workflow) {
const workflowData = metadata.workflow()
if (workflowData) {
await this.loadGraphData(workflowData, true, true, fileName)
return
}
}
// If no workflow, try prompt
if (metadata.prompt) {
const promptData = metadata.prompt()
if (promptData) {
this.loadApiJson(promptData, fileName)
return
}
}
// If no workflow or prompt, try A1111 parameters
if (metadata.parameters) {
useWorkflowService().beforeLoadNewGraph()
importA1111(this.graph, metadata.parameters)
useWorkflowService().afterLoadNewGraph(
fileName,
this.graph.serialize() as unknown as ComfyWorkflowJSON
)
return
}
// No valid data found
this.showErrorOnFileLoad(file)
}
isApiJson(data: unknown) {

View 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
}
})

176
src/utils/fileHandlers.ts Normal file
View File

@@ -0,0 +1,176 @@
/**
* File handlers for extracting workflow data from various file formats.
* This module contains only the handler implementations.
* Registration is handled through the fileHandlerStore.
*/
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
import { getMp3Metadata } from '@/scripts/metadata/mp3'
import { getOggMetadata } from '@/scripts/metadata/ogg'
import { getSvgMetadata } from '@/scripts/metadata/svg'
import {
getFlacMetadata,
getLatentMetadata,
getPngMetadata,
getWebpMetadata
} from '@/scripts/pnginfo'
import type { WorkflowFileHandler } from '@/stores/fileHandlerStore'
/**
* Handler for PNG files
*/
export const handlePngFile: WorkflowFileHandler = async (file) => {
const pngInfo = await getPngMetadata(file)
return {
workflow: () =>
pngInfo?.workflow ? JSON.parse(pngInfo.workflow) : undefined,
prompt: () => (pngInfo?.prompt ? JSON.parse(pngInfo.prompt) : undefined),
parameters: pngInfo?.parameters
}
}
/**
* Handler for WebP files
*/
export const handleWebpFile: WorkflowFileHandler = async (file) => {
const pngInfo = await getWebpMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
return {
workflow: () => (workflow ? JSON.parse(workflow) : undefined),
prompt: () => (prompt ? JSON.parse(prompt) : undefined)
}
}
/**
* Handler for SVG files
*/
export const handleSvgFile: WorkflowFileHandler = async (file) => {
const svgInfo = await getSvgMetadata(file)
return {
workflow: () => svgInfo.workflow,
prompt: () => svgInfo.prompt
}
}
/**
* Handler for MP3 files
*/
export const handleMp3File: WorkflowFileHandler = async (file) => {
const { workflow, prompt } = await getMp3Metadata(file)
return {
workflow: () => workflow,
prompt: () => prompt
}
}
/**
* Handler for OGG files
*/
export const handleOggFile: WorkflowFileHandler = async (file) => {
const { workflow, prompt } = await getOggMetadata(file)
return {
workflow: () => workflow,
prompt: () => prompt
}
}
/**
* Handler for FLAC files
*/
export const handleFlacFile: WorkflowFileHandler = async (file) => {
const pngInfo = await getFlacMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
const prompt = pngInfo?.prompt || pngInfo?.Prompt
return {
workflow: () => (workflow ? JSON.parse(workflow) : undefined),
prompt: () => (prompt ? JSON.parse(prompt) : undefined)
}
}
/**
* Handler for WebM files
*/
export const handleWebmFile: WorkflowFileHandler = async (file) => {
const webmInfo = await getFromWebmFile(file)
return {
workflow: () => webmInfo.workflow,
prompt: () => webmInfo.prompt
}
}
/**
* Handler for MP4/MOV/M4V files
*/
export const handleMp4File: WorkflowFileHandler = async (file) => {
const mp4Info = await getFromIsobmffFile(file)
return {
workflow: () => mp4Info.workflow,
prompt: () => mp4Info.prompt
}
}
/**
* Handler for GLB files
*/
export const handleGlbFile: WorkflowFileHandler = async (file) => {
const gltfInfo = await getGltfBinaryMetadata(file)
return {
workflow: () => gltfInfo.workflow,
prompt: () => gltfInfo.prompt
}
}
/**
* Handler for JSON files
*/
export const handleJsonFile: WorkflowFileHandler = async (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
try {
const readerResult = reader.result as string
resolve({
workflow: () => JSON.parse(readerResult),
prompt: () => JSON.parse(readerResult),
jsonTemplateData: () => JSON.parse(readerResult)
})
} catch (error) {
reject(error)
}
}
reader.onerror = () => reject(reader.error)
reader.readAsText(file)
})
}
/**
* Handler for .latent and .safetensors files
*/
export const handleLatentFile: WorkflowFileHandler = async (file) => {
const info = await getLatentMetadata(file)
return {
workflow: () => {
if (
info &&
typeof info === 'object' &&
'workflow' in info &&
info.workflow
) {
return JSON.parse(info.workflow as string)
}
return undefined
},
prompt: () => {
if (info && typeof info === 'object' && 'prompt' in info && info.prompt) {
return JSON.parse(info.prompt as string)
}
return undefined
}
}
}

View File

@@ -0,0 +1,323 @@
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import {
useFileHandlerStore,
type FileHandlerDefinition
} from '../../../src/stores/fileHandlerStore'
import {
handlePngFile,
handleWebpFile,
handleJsonFile
} from '../../../src/utils/fileHandlers'
// Mock the metadata functions
vi.mock('../../../src/scripts/pnginfo', () => ({
getPngMetadata: vi.fn().mockResolvedValue({
workflow: '{"test": "workflow"}',
prompt: '{"test": "prompt"}'
}),
getWebpMetadata: vi
.fn()
.mockResolvedValue({ workflow: '{"test": "workflow"}' }),
getFlacMetadata: vi
.fn()
.mockResolvedValue({ workflow: '{"test": "workflow"}' }),
getLatentMetadata: vi
.fn()
.mockResolvedValue({ workflow: '{"test": "workflow"}' })
}))
vi.mock('../../../src/scripts/metadata/svg', () => ({
getSvgMetadata: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
}))
vi.mock('../../../src/scripts/metadata/mp3', () => ({
getMp3Metadata: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
}))
vi.mock('../../../src/scripts/metadata/ogg', () => ({
getOggMetadata: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
}))
vi.mock('../../../src/scripts/metadata/ebml', () => ({
getFromWebmFile: vi.fn().mockResolvedValue({ workflow: { test: 'workflow' } })
}))
vi.mock('../../../src/scripts/metadata/isobmff', () => ({
getFromIsobmffFile: vi
.fn()
.mockResolvedValue({ workflow: { test: 'workflow' } })
}))
vi.mock('../../../src/scripts/metadata/gltf', () => ({
getGltfBinaryMetadata: vi
.fn()
.mockResolvedValue({ workflow: { test: 'workflow' } })
}))
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 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 handle priority correctly', () => {
const store = useFileHandlerStore()
// Register low priority handler first
store.registerFileHandler({
id: 'low.priority',
displayName: 'Low Priority',
mimeTypes: ['image/png'],
extensions: ['.png'],
handler: handlePngFile,
priority: 1
})
// Register high priority handler
store.registerFileHandler({
id: 'high.priority',
displayName: 'High Priority',
mimeTypes: ['image/png'],
extensions: ['.png'],
handler: handleWebpFile,
priority: 10
})
const handler = store.getHandlerForFile(
new File([''], 'test.png', { type: 'image/png' })
)
expect(handler).toBe(handleWebpFile) // Should return high priority handler
})
})
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 = store.getHandlerForFile(file)
expect(handler).toBe(handlePngFile)
})
it('should find handler by extension when MIME type is missing', () => {
const store = useFileHandlerStore()
const file = new File([''], 'test.png', { type: '' })
const handler = store.getHandlerForFile(file)
expect(handler).toBe(handlePngFile)
})
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 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)
})
})
describe('PNG handler with NaN values', () => {
it('should return lazy functions that parse on demand', async () => {
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
vi.mocked(getPngMetadata).mockResolvedValueOnce({
workflow: '{"valid": "json"}',
prompt: '{"invalid": NaN}'
})
const file = new File([''], 'test.png', { type: 'image/png' })
const result = await handlePngFile(file)
expect(result.workflow).toBeTypeOf('function')
expect(result.prompt).toBeTypeOf('function')
// workflow should parse successfully
expect(result.workflow!()).toEqual({ valid: 'json' })
// prompt should throw on parse due to NaN
expect(() => result.prompt!()).toThrow()
})
it('should handle valid workflow with invalid prompt JSON', async () => {
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
vi.mocked(getPngMetadata).mockResolvedValueOnce({
workflow: '{"nodes": [{"id": 1, "type": "TestNode"}]}',
prompt:
'{"1": {"inputs": {"seed": 123}, "class_type": "TestNode"}, "error": NaN}'
})
const file = new File([''], 'test.png', { type: 'image/png' })
const result = await handlePngFile(file)
// workflow should parse successfully
const workflowData = result.workflow!()
expect(workflowData).toEqual({
nodes: [{ id: 1, type: 'TestNode' }]
})
// prompt should throw due to invalid JSON
expect(() => result.prompt!()).toThrow('Unexpected token')
})
it('should handle both fields containing NaN', async () => {
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
vi.mocked(getPngMetadata).mockResolvedValueOnce({
workflow: '{"value": NaN}',
prompt: '{"error": NaN}'
})
const file = new File([''], 'test.png', { type: 'image/png' })
const result = await handlePngFile(file)
// Both should throw when called
expect(() => result.workflow!()).toThrow()
expect(() => result.prompt!()).toThrow()
})
it('should handle missing metadata gracefully', async () => {
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
vi.mocked(getPngMetadata).mockResolvedValueOnce({})
const file = new File([''], 'test.png', { type: 'image/png' })
const result = await handlePngFile(file)
expect(result.workflow!()).toBeUndefined()
expect(result.prompt!()).toBeUndefined()
})
})
describe('JSON handler edge cases', () => {
it('should handle empty JSON file', async () => {
const file = new File(['{}'], 'empty.json', { type: 'application/json' })
const result = await handleJsonFile(file)
expect(result.jsonTemplateData!()).toEqual({})
})
it('should handle malformed JSON', async () => {
const file = new File(['{invalid json}'], 'bad.json', {
type: 'application/json'
})
const result = await handleJsonFile(file)
// Should throw when calling the lazy functions
expect(() => result.jsonTemplateData!()).toThrow()
expect(() => result.workflow!()).toThrow()
expect(() => result.prompt!()).toThrow()
})
})
describe('Lazy evaluation behavior', () => {
it('should not parse until functions are called', async () => {
const { getPngMetadata } = await import('../../../src/scripts/pnginfo')
const originalParse = JSON.parse
let parseCallCount = 0
JSON.parse = vi.fn((text: string) => {
parseCallCount++
return originalParse(text)
})
vi.mocked(getPngMetadata).mockResolvedValueOnce({
workflow: '{"test": "data"}',
prompt: '{"test": "prompt"}'
})
const file = new File([''], 'test.png', { type: 'image/png' })
const result = await handlePngFile(file)
// JSON.parse should not have been called yet
expect(parseCallCount).toBe(0)
// Call workflow function
result.workflow!()
expect(parseCallCount).toBe(1)
// Call prompt function
result.prompt!()
expect(parseCallCount).toBe(2)
// Restore original JSON.parse
JSON.parse = originalParse
})
})
})