Compare commits

...

1 Commits

Author SHA1 Message Date
huang47
e2fe1c3f26 test: cover metadata parser and manager pack composables 2026-06-30 22:37:04 -07:00
3 changed files with 697 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
import { getDataFromJSON } from '@/scripts/metadata/json'
import { getMp3Metadata } from '@/scripts/metadata/mp3'
import { getOggMetadata } from '@/scripts/metadata/ogg'
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
import { getSvgMetadata } from '@/scripts/metadata/svg'
import {
getAvifMetadata,
getFlacMetadata,
getLatentMetadata,
getPngMetadata,
getWebpMetadata
} from '@/scripts/pnginfo'
vi.mock('@/scripts/metadata/ebml', () => ({ getFromWebmFile: vi.fn() }))
vi.mock('@/scripts/metadata/gltf', () => ({ getGltfBinaryMetadata: vi.fn() }))
vi.mock('@/scripts/metadata/isobmff', () => ({ getFromIsobmffFile: vi.fn() }))
vi.mock('@/scripts/metadata/json', () => ({ getDataFromJSON: vi.fn() }))
vi.mock('@/scripts/metadata/mp3', () => ({ getMp3Metadata: vi.fn() }))
vi.mock('@/scripts/metadata/ogg', () => ({ getOggMetadata: vi.fn() }))
vi.mock('@/scripts/metadata/svg', () => ({ getSvgMetadata: vi.fn() }))
vi.mock('@/scripts/pnginfo', () => ({
getAvifMetadata: vi.fn(),
getFlacMetadata: vi.fn(),
getLatentMetadata: vi.fn(),
getPngMetadata: vi.fn(),
getWebpMetadata: vi.fn()
}))
function file(type: string, name = 'file') {
return new File(['data'], name, { type })
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('getWorkflowDataFromFile', () => {
it('routes png/avif/mp3/ogg/webm to their parsers and returns the result', async () => {
vi.mocked(getPngMetadata).mockResolvedValue({ a: 1 } as never)
expect(await getWorkflowDataFromFile(file('image/png'))).toEqual({ a: 1 })
expect(getPngMetadata).toHaveBeenCalled()
await getWorkflowDataFromFile(file('image/avif'))
expect(getAvifMetadata).toHaveBeenCalled()
await getWorkflowDataFromFile(file('audio/mpeg'))
expect(getMp3Metadata).toHaveBeenCalled()
await getWorkflowDataFromFile(file('audio/ogg'))
expect(getOggMetadata).toHaveBeenCalled()
await getWorkflowDataFromFile(file('video/webm'))
expect(getFromWebmFile).toHaveBeenCalled()
})
it('extracts workflow/prompt from webp, preferring lowercase keys', async () => {
vi.mocked(getWebpMetadata).mockResolvedValue({
workflow: 'wf',
prompt: 'pr'
} as never)
expect(await getWorkflowDataFromFile(file('image/webp'))).toEqual({
workflow: 'wf',
prompt: 'pr'
})
})
it('falls back to capitalized webp keys when lowercase are absent', async () => {
vi.mocked(getWebpMetadata).mockResolvedValue({
Workflow: 'WF',
Prompt: 'PR'
} as never)
expect(await getWorkflowDataFromFile(file('image/webp'))).toEqual({
workflow: 'WF',
prompt: 'PR'
})
})
it('handles both flac mime types and extracts workflow/prompt', async () => {
vi.mocked(getFlacMetadata).mockResolvedValue({ workflow: 'w' } as never)
expect(await getWorkflowDataFromFile(file('audio/flac'))).toEqual({
workflow: 'w',
prompt: undefined
})
expect(await getWorkflowDataFromFile(file('audio/x-flac'))).toEqual({
workflow: 'w',
prompt: undefined
})
})
it('falls back to capitalized flac keys when lowercase are absent', async () => {
vi.mocked(getFlacMetadata).mockResolvedValue({
Workflow: 'WF',
Prompt: 'PR'
} as never)
expect(await getWorkflowDataFromFile(file('audio/flac'))).toEqual({
workflow: 'WF',
prompt: 'PR'
})
})
it('routes isobmff by mime type and by file extension', async () => {
await getWorkflowDataFromFile(file('video/mp4'))
await getWorkflowDataFromFile(file('video/quicktime'))
await getWorkflowDataFromFile(file('video/x-m4v'))
await getWorkflowDataFromFile(file('', 'clip.mp4'))
await getWorkflowDataFromFile(file('', 'clip.mov'))
await getWorkflowDataFromFile(file('', 'clip.m4v'))
expect(getFromIsobmffFile).toHaveBeenCalledTimes(6)
})
it('routes svg and gltf by mime type or extension', async () => {
await getWorkflowDataFromFile(file('image/svg+xml'))
await getWorkflowDataFromFile(file('', 'icon.svg'))
expect(getSvgMetadata).toHaveBeenCalledTimes(2)
await getWorkflowDataFromFile(file('model/gltf-binary'))
await getWorkflowDataFromFile(file('', 'model.glb'))
expect(getGltfBinaryMetadata).toHaveBeenCalledTimes(2)
})
it('routes latent/safetensors and json by extension or mime type', async () => {
await getWorkflowDataFromFile(file('', 'x.latent'))
await getWorkflowDataFromFile(file('', 'x.safetensors'))
expect(getLatentMetadata).toHaveBeenCalledTimes(2)
await getWorkflowDataFromFile(file('application/json'))
await getWorkflowDataFromFile(file('', 'x.json'))
expect(getDataFromJSON).toHaveBeenCalledTimes(2)
})
it('returns undefined for an unrecognized file', async () => {
expect(
await getWorkflowDataFromFile(file('application/zip', 'a.zip'))
).toBe(undefined)
})
})

View File

@@ -0,0 +1,277 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { components } from '@/types/comfyRegistryTypes'
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
type NodePack = components['schemas']['Node']
type CompatibilityCheck = {
hasConflict: boolean
conflicts: ConflictDetail[]
}
const { managerStore, showDialog, checkNodeCompatibility } = vi.hoisted(() => ({
managerStore: {
installPack: { call: vi.fn(), clear: vi.fn() },
isPackInstalling: vi.fn((_id?: string) => false),
isPackInstalled: vi.fn((_id?: string) => false)
},
showDialog: vi.fn(),
checkNodeCompatibility: vi.fn(
(): CompatibilityCheck => ({ hasConflict: false, conflicts: [] })
)
}))
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
useComfyManagerStore: () => managerStore
}))
vi.mock(
'@/workbench/extensions/manager/composables/useNodeConflictDialog',
() => ({
useNodeConflictDialog: () => ({ show: showDialog })
})
)
vi.mock(
'@/workbench/extensions/manager/composables/useConflictDetection',
() => ({
useConflictDetection: () => ({ checkNodeCompatibility })
})
)
function pack(over: Partial<NodePack> = {}): NodePack {
return { id: 'pack-a', name: 'Pack A', ...over } as NodePack
}
function conflict(overrides: Partial<ConflictDetail> = {}): ConflictDetail {
return {
type: 'os',
current_value: 'linux',
required_value: 'darwin',
...overrides
}
}
beforeEach(() => {
managerStore.installPack.call.mockReset().mockResolvedValue(undefined)
managerStore.installPack.clear.mockReset()
managerStore.isPackInstalling.mockReset().mockReturnValue(false)
managerStore.isPackInstalled.mockReset().mockReturnValue(false)
showDialog.mockReset()
checkNodeCompatibility.mockReset().mockReturnValue({
hasConflict: false,
conflicts: []
})
})
describe('usePackInstall', () => {
it('reports isInstalling when any pack is installing', () => {
managerStore.isPackInstalling.mockImplementation(
(id?: string) => id === 'pack-b'
)
const { isInstalling } = usePackInstall(() => [
pack(),
pack({ id: 'pack-b' })
])
expect(isInstalling.value).toBe(true)
})
it('reports not installing for an empty or idle pack list', () => {
expect(usePackInstall(() => []).isInstalling.value).toBe(false)
expect(
usePackInstall(() => undefined as unknown as NodePack[]).isInstalling
.value
).toBe(false)
expect(usePackInstall(() => [pack()]).isInstalling.value).toBe(false)
})
it('installs each pack and clears the command afterward', async () => {
const { performInstallation } = usePackInstall(() => [])
await performInstallation([
pack({
id: 'a',
latest_version: { version: '1.2.0' }
} as Partial<NodePack>),
pack({ id: 'b', publisher: { name: 'Unclaimed' } } as Partial<NodePack>)
])
expect(managerStore.installPack.call).toHaveBeenCalledTimes(2)
expect(managerStore.installPack.call).toHaveBeenCalledWith(
expect.objectContaining({ id: 'a', selected_version: '1.2.0' })
)
expect(managerStore.installPack.call).toHaveBeenCalledWith(
expect.objectContaining({ id: 'b', selected_version: 'nightly' })
)
expect(managerStore.installPack.clear).toHaveBeenCalled()
})
it('installAllPacks installs only the not-yet-installed packs', async () => {
managerStore.isPackInstalled.mockImplementation(
(id?: string) => id === 'installed'
)
const { installAllPacks } = usePackInstall(() => [
pack({ id: 'installed' }),
pack({ id: 'fresh' })
])
await installAllPacks()
expect(managerStore.installPack.call).toHaveBeenCalledTimes(1)
expect(managerStore.installPack.call).toHaveBeenCalledWith(
expect.objectContaining({ id: 'fresh' })
)
})
it('installAllPacks returns early for empty or already installed packs', async () => {
await usePackInstall(() => []).installAllPacks()
managerStore.isPackInstalled.mockReturnValue(true)
await usePackInstall(() => [pack({ id: 'installed' })]).installAllPacks()
expect(managerStore.installPack.call).not.toHaveBeenCalled()
expect(managerStore.installPack.clear).not.toHaveBeenCalled()
})
it('installAllPacks opens the conflict dialog instead of installing when conflicted', async () => {
const osConflict = conflict()
checkNodeCompatibility.mockReturnValue({
hasConflict: true,
conflicts: [osConflict]
})
const { installAllPacks } = usePackInstall(
() => [pack({ id: 'x' })],
() => true,
() => [osConflict]
)
await installAllPacks()
expect(showDialog).toHaveBeenCalledTimes(1)
expect(showDialog).toHaveBeenCalledWith(
expect.objectContaining({
conflictedPackages: [
expect.objectContaining({
package_id: 'x',
package_name: 'Pack A',
has_conflict: true,
conflicts: [osConflict],
is_compatible: false
})
]
})
)
expect(managerStore.installPack.call).not.toHaveBeenCalled()
})
it('installAllPacks stops when conflict details are unavailable', async () => {
const { installAllPacks } = usePackInstall(
() => [pack({ id: 'x' })],
() => true
)
await installAllPacks()
expect(showDialog).not.toHaveBeenCalled()
expect(managerStore.installPack.call).not.toHaveBeenCalled()
})
it('conflict dialog payload falls back for unnamed package data', async () => {
checkNodeCompatibility.mockReturnValue({
hasConflict: true,
conflicts: [conflict()]
})
const { installAllPacks } = usePackInstall(
() => [pack({ id: undefined, name: undefined })],
() => true,
() => [conflict()]
)
await installAllPacks()
expect(showDialog).toHaveBeenCalledWith(
expect.objectContaining({
conflictedPackages: [
expect.objectContaining({
package_id: '',
package_name: ''
})
]
})
)
})
it('conflict dialog action installs only packs still missing', async () => {
checkNodeCompatibility.mockReturnValue({
hasConflict: false,
conflicts: []
})
managerStore.isPackInstalled.mockImplementation(
(id?: string) => id === 'installed'
)
const { installAllPacks } = usePackInstall(
() => [pack({ id: 'installed' }), pack({ id: 'fresh' })],
() => true,
() => [conflict()]
)
await installAllPacks()
const [{ onButtonClick }] = showDialog.mock.calls[0]
await onButtonClick()
expect(managerStore.installPack.call).toHaveBeenCalledTimes(1)
expect(managerStore.installPack.call).toHaveBeenCalledWith(
expect.objectContaining({ id: 'fresh' })
)
expect(managerStore.installPack.clear).toHaveBeenCalledTimes(1)
})
it('conflict dialog action returns when every pack is already installed', async () => {
managerStore.isPackInstalled.mockReturnValue(true)
const { installAllPacks } = usePackInstall(
() => [pack({ id: 'installed' })],
() => true,
() => [conflict()]
)
await installAllPacks()
const [{ onButtonClick }] = showDialog.mock.calls[0]
await onButtonClick()
expect(managerStore.installPack.call).not.toHaveBeenCalled()
expect(managerStore.installPack.clear).not.toHaveBeenCalled()
})
it('clears the command when payload validation rejects', async () => {
const { performInstallation } = usePackInstall(() => [])
await expect(
performInstallation([pack({ id: undefined })])
).rejects.toThrow('manager.packInstall.nodeIdRequired')
expect(managerStore.installPack.call).not.toHaveBeenCalled()
expect(managerStore.installPack.clear).toHaveBeenCalledTimes(1)
})
it('leaves command cleanup in finally when one install fails', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
managerStore.installPack.call
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error('failed'))
const { performInstallation } = usePackInstall(() => [])
await performInstallation([pack({ id: 'a' }), pack({ id: 'b' })])
expect(consoleError).toHaveBeenCalledWith(
'[usePackInstall] Some installations failed:',
[expect.any(Error)]
)
expect(managerStore.installPack.clear).toHaveBeenCalledTimes(1)
} finally {
consoleError.mockRestore()
}
})
})

View File

@@ -0,0 +1,279 @@
import type * as VueUse from '@vueuse/core'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { useManagerDisplayPacks } from '@/workbench/extensions/manager/composables/useManagerDisplayPacks'
type NodePack = components['schemas']['Node']
const { state } = vi.hoisted(() => ({
state: {
installed: [] as NodePack[],
workflow: [] as NodePack[],
installedLoading: false,
workflowLoading: false,
installedReady: true,
workflowReady: true,
startFetchInstalled: vi.fn(),
startFetchWorkflowPacks: vi.fn(),
installedIds: new Set<string>(),
installedVersions: {} as Record<string, string>,
conflicts: [] as { package_id: string }[]
}
}))
vi.mock('@vueuse/core', async (orig) => ({
...(await orig<typeof VueUse>()),
whenever: (source: unknown, callback?: () => void) => {
if (typeof source === 'function' && source() && callback) {
callback()
}
}
}))
vi.mock('@/services/gateway/registrySearchGateway', () => ({
useRegistrySearchGateway: () => ({
getSortValue: (pack: NodePack, field: string) =>
(pack as Record<string, unknown>)[field],
getSortableFields: () => [{ id: 'name', direction: 'asc' }]
})
}))
vi.mock(
'@/workbench/extensions/manager/composables/nodePack/useInstalledPacks',
() => ({
useInstalledPacks: () => ({
startFetchInstalled: state.startFetchInstalled,
filterInstalledPack: (packs: NodePack[]) =>
packs.filter((p) => state.installedIds.has(p.id ?? '')),
installedPacks: ref(state.installed),
isLoading: ref(state.installedLoading),
isReady: ref(state.installedReady)
})
})
)
vi.mock(
'@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks',
() => ({
useWorkflowPacks: () => ({
startFetchWorkflowPacks: state.startFetchWorkflowPacks,
filterWorkflowPack: (packs: NodePack[]) => packs,
workflowPacks: ref(state.workflow),
isLoading: ref(state.workflowLoading),
isReady: ref(state.workflowReady)
})
})
)
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
useComfyManagerStore: () => ({
isPackInstalled: (id: string | undefined) =>
state.installedIds.has(id ?? ''),
getInstalledPackVersion: (id: string) => state.installedVersions[id]
})
}))
vi.mock('@/workbench/extensions/manager/stores/conflictDetectionStore', () => ({
useConflictDetectionStore: () => ({
get conflictedPackages() {
return state.conflicts
}
})
}))
function pack(id: string, latestVersion?: string): NodePack {
return {
id,
name: id,
latest_version: latestVersion ? { version: latestVersion } : undefined
} as NodePack
}
function display(
tab: ManagerTab,
searchResults: NodePack[] = [],
query = '',
sortField = ''
) {
return useManagerDisplayPacks(
ref(tab),
ref(searchResults),
ref(query),
ref(sortField)
)
}
beforeEach(() => {
state.installed = []
state.workflow = []
state.installedLoading = false
state.workflowLoading = false
state.installedReady = true
state.workflowReady = true
state.startFetchInstalled.mockReset()
state.startFetchWorkflowPacks.mockReset()
state.installedIds = new Set()
state.installedVersions = {}
state.conflicts = []
})
describe('useManagerDisplayPacks', () => {
it('All tab returns the raw search results', () => {
const results = [pack('a'), pack('b')]
expect(display(ManagerTab.All, results).displayPacks.value).toEqual(results)
})
it('NotInstalled tab excludes installed packs', () => {
state.installedIds = new Set(['a'])
const { displayPacks } = display(ManagerTab.NotInstalled, [
pack('a'),
pack('b')
])
expect(displayPacks.value.map((p) => p.id)).toEqual(['b'])
})
it('AllInstalled tab shows installed packs when not searching', () => {
state.installed = [pack('x'), pack('y')]
const { displayPacks } = display(ManagerTab.AllInstalled)
expect(displayPacks.value.map((p) => p.id)).toEqual(['x', 'y'])
})
it('UpdateAvailable tab keeps only installed packs with a newer latest version', () => {
state.installedIds = new Set(['old', 'current', 'nightly'])
state.installedVersions = {
old: '1.0.0',
current: '2.0.0',
nightly: 'not-semver'
}
state.installed = [
pack('old', '1.2.0'),
pack('current', '2.0.0'),
pack('nightly', '9.9.9'),
pack('uninstalled', '5.0.0')
]
const { displayPacks } = display(ManagerTab.UpdateAvailable)
expect(displayPacks.value.map((p) => p.id)).toEqual(['old'])
})
it('Conflicting tab keeps packs flagged by the conflict store', () => {
state.installed = [pack('a'), pack('b')]
state.conflicts = [{ package_id: 'b' }]
const { displayPacks } = display(ManagerTab.Conflicting)
expect(displayPacks.value.map((p) => p.id)).toEqual(['b'])
})
it('Missing tab returns workflow packs that are not installed', () => {
state.workflow = [pack('a'), pack('b')]
state.installedIds = new Set(['a'])
const { displayPacks, missingNodePacks } = display(ManagerTab.Missing)
expect(displayPacks.value.map((p) => p.id)).toEqual(['b'])
expect(missingNodePacks.value.map((p) => p.id)).toEqual(['b'])
})
it('Unresolved tab is always empty', () => {
expect(
display(ManagerTab.Unresolved, [pack('a')]).displayPacks.value
).toEqual([])
})
it('reports loading state scoped to the active tab group', () => {
state.installedLoading = true
state.workflowLoading = false
expect(display(ManagerTab.AllInstalled).isLoading.value).toBe(true)
expect(display(ManagerTab.All).isLoading.value).toBe(false)
state.installedLoading = false
state.workflowLoading = true
expect(display(ManagerTab.Workflow).isLoading.value).toBe(true)
expect(display(ManagerTab.Missing).isLoading.value).toBe(true)
})
it('fetches installed packs when an installed tab is selected and not ready', () => {
state.installedReady = false
display(ManagerTab.AllInstalled)
expect(state.startFetchInstalled).toHaveBeenCalledTimes(1)
expect(state.startFetchWorkflowPacks).not.toHaveBeenCalled()
})
it('fetches workflow and installed packs for missing workflow dependencies', () => {
state.installedReady = false
state.workflowReady = false
display(ManagerTab.Missing)
expect(state.startFetchInstalled).toHaveBeenCalledTimes(1)
expect(state.startFetchWorkflowPacks).toHaveBeenCalledTimes(1)
})
it('filters search results to installed packs on the AllInstalled tab while searching', () => {
state.installedIds = new Set(['a'])
const { displayPacks } = display(
ManagerTab.AllInstalled,
[pack('a'), pack('b')],
'query'
)
expect(displayPacks.value.map((p) => p.id)).toEqual(['a'])
})
it('filters searched update and conflict tabs before applying tab rules', () => {
state.installedIds = new Set(['old', 'conflict'])
state.installedVersions = {
old: '1.0.0',
conflict: '1.0.0'
}
state.conflicts = [{ package_id: 'conflict' }]
const results = [
pack('old', '2.0.0'),
pack('current', '1.0.0'),
pack('conflict', '1.0.0')
]
expect(
display(
ManagerTab.UpdateAvailable,
results,
'query'
).displayPacks.value.map((p) => p.id)
).toEqual(['old'])
expect(
display(ManagerTab.Conflicting, results, 'query').displayPacks.value.map(
(p) => p.id
)
).toEqual(['conflict'])
})
it('filters workflow search results on the Workflow tab while searching', () => {
const { displayPacks } = display(
ManagerTab.Workflow,
[pack('a'), pack('b')],
'query'
)
expect(displayPacks.value.map((p) => p.id)).toEqual(['a', 'b'])
})
it('filters searched missing workflow packs to not-installed packs', () => {
state.installedIds = new Set(['a'])
const { displayPacks } = display(
ManagerTab.Missing,
[pack('a'), pack('b')],
'query'
)
expect(displayPacks.value.map((p) => p.id)).toEqual(['b'])
})
it('falls back to search results for unknown tabs', () => {
const results = [pack('a')]
expect(
display('unknown' as ManagerTab, results).displayPacks.value
).toEqual(results)
})
it('sorts installed packs by the configured field', () => {
state.installed = [pack('b'), pack('a'), pack('c')]
const { displayPacks } = display(ManagerTab.AllInstalled, [], '', 'name')
expect(displayPacks.value.map((p) => p.id)).toEqual(['a', 'b', 'c'])
})
})