mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-04 20:50:06 +00:00
Merge main (as of 10-06-2025) into rh-test (#5965)
## Summary Merges latest changes from `main` as of 10-06-2025. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5965-Merge-main-as-of-10-06-2025-into-rh-test-2856d73d3650812cb95fd8917278a770) by [Unito](https://www.unito.io) --------- Signed-off-by: Marcel Petrick <mail@marcelpetrick.it> Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: snomiao <snomiao@gmail.com> Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com> Co-authored-by: Jake Schroeder <jake.schroeder@isophex.com> Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com> Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Co-authored-by: Marcel Petrick <mail@marcelpetrick.it> Co-authored-by: Alexander Brown <DrJKL0424@gmail.com> Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com> Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe> Co-authored-by: JakeSchroeder <jake@axiom.co> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: DrJKL <DrJKL@users.noreply.github.com> Co-authored-by: ComfyUI Wiki <contact@comfyui-wiki.com>
This commit is contained in:
@@ -1,91 +1,100 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
// Mock the dialog store
|
||||
vi.mock('@/stores/dialogStore')
|
||||
|
||||
// Mock the asset service
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
getAssetsForNodeType: vi.fn().mockResolvedValue([])
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, params?: Record<string, string>) => {
|
||||
if (params) {
|
||||
return `${key}:${JSON.stringify(params)}`
|
||||
}
|
||||
return key
|
||||
}
|
||||
}))
|
||||
|
||||
// Test factory functions
|
||||
interface AssetBrowserProps {
|
||||
nodeType: string
|
||||
inputName: string
|
||||
onAssetSelected?: (filename: string) => void
|
||||
}
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
getAssetsForNodeType: vi.fn().mockResolvedValue([]),
|
||||
getAssetsByTag: vi.fn().mockResolvedValue([])
|
||||
}
|
||||
}))
|
||||
|
||||
function createAssetBrowserProps(
|
||||
overrides: Partial<AssetBrowserProps> = {}
|
||||
): AssetBrowserProps {
|
||||
const { assetService } = await import('@/platform/assets/services/assetService')
|
||||
const mockGetAssetsByTag = vi.mocked(assetService.getAssetsByTag)
|
||||
const mockGetAssetsForNodeType = vi.mocked(assetService.getAssetsForNodeType)
|
||||
|
||||
function createMockAsset(overrides: Partial<AssetItem> = {}): AssetItem {
|
||||
return {
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name',
|
||||
id: 'asset-123',
|
||||
name: 'test-model.safetensors',
|
||||
size: 1024,
|
||||
created_at: '2025-10-01T00:00:00Z',
|
||||
tags: ['models', 'checkpoints'],
|
||||
user_metadata: {
|
||||
filename: 'models/checkpoints/test-model.safetensors'
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function setupDialogMocks() {
|
||||
const mockShowDialog = vi.fn()
|
||||
const mockCloseDialog = vi.fn()
|
||||
vi.mocked(useDialogStore, { partial: true }).mockReturnValue({
|
||||
showDialog: mockShowDialog,
|
||||
closeDialog: mockCloseDialog
|
||||
})
|
||||
|
||||
return { mockShowDialog, mockCloseDialog }
|
||||
}
|
||||
|
||||
describe('useAssetBrowserDialog', () => {
|
||||
describe('Asset Selection Flow', () => {
|
||||
it('auto-closes dialog when asset is selected', async () => {
|
||||
// Create fresh mocks for this test
|
||||
const mockShowDialog = vi.fn()
|
||||
const mockCloseDialog = vi.fn()
|
||||
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
showDialog: mockShowDialog,
|
||||
closeDialog: mockCloseDialog
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
const { mockShowDialog, mockCloseDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const onAssetSelected = vi.fn()
|
||||
const props = createAssetBrowserProps({ onAssetSelected })
|
||||
|
||||
await assetBrowserDialog.show(props)
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name',
|
||||
onAssetSelected
|
||||
})
|
||||
|
||||
// Get the onSelect handler that was passed to the dialog
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
const onSelectHandler = dialogCall.props.onSelect
|
||||
|
||||
// Simulate asset selection
|
||||
onSelectHandler('selected-asset-path')
|
||||
const mockAsset = {
|
||||
id: 'test-asset-id',
|
||||
name: 'test.safetensors',
|
||||
size: 1024,
|
||||
created_at: '2025-10-01T00:00:00Z',
|
||||
tags: ['models', 'checkpoints'],
|
||||
user_metadata: { filename: 'selected-asset-path' }
|
||||
}
|
||||
onSelectHandler(mockAsset)
|
||||
|
||||
// Should call the original callback and trigger hide animation
|
||||
expect(onAssetSelected).toHaveBeenCalledWith('selected-asset-path')
|
||||
expect(onAssetSelected).toHaveBeenCalledWith(mockAsset)
|
||||
expect(mockCloseDialog).toHaveBeenCalledWith({
|
||||
key: 'global-asset-browser'
|
||||
})
|
||||
})
|
||||
|
||||
it('closes dialog when close handler is called', async () => {
|
||||
// Create fresh mocks for this test
|
||||
const mockShowDialog = vi.fn()
|
||||
const mockCloseDialog = vi.fn()
|
||||
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
showDialog: mockShowDialog,
|
||||
closeDialog: mockCloseDialog
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
const { mockShowDialog, mockCloseDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const props = createAssetBrowserProps()
|
||||
|
||||
await assetBrowserDialog.show(props)
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name'
|
||||
})
|
||||
|
||||
// Get the onClose handler that was passed to the dialog
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
const onCloseHandler = dialogCall.props.onClose
|
||||
|
||||
// Simulate dialog close
|
||||
onCloseHandler()
|
||||
|
||||
expect(mockCloseDialog).toHaveBeenCalledWith({
|
||||
@@ -93,4 +102,158 @@ describe('useAssetBrowserDialog', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.browse() method', () => {
|
||||
it('opens asset browser dialog with tag-based filtering', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models',
|
||||
title: 'Model Library'
|
||||
})
|
||||
|
||||
expect(mockShowDialog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: 'global-asset-browser',
|
||||
props: expect.objectContaining({
|
||||
showLeftPanel: true
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('calls onAssetSelected callback when asset is selected', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const mockAsset = createMockAsset()
|
||||
const onAssetSelected = vi.fn()
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models',
|
||||
onAssetSelected
|
||||
})
|
||||
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
const onSelectHandler = dialogCall.props.onSelect
|
||||
|
||||
onSelectHandler(mockAsset)
|
||||
|
||||
expect(onAssetSelected).toHaveBeenCalledWith(mockAsset)
|
||||
})
|
||||
|
||||
it('closes dialog after asset selection', async () => {
|
||||
const { mockShowDialog, mockCloseDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const mockAsset = createMockAsset()
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models'
|
||||
})
|
||||
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
const onSelectHandler = dialogCall.props.onSelect
|
||||
|
||||
onSelectHandler(mockAsset)
|
||||
|
||||
expect(mockCloseDialog).toHaveBeenCalledWith({
|
||||
key: 'global-asset-browser'
|
||||
})
|
||||
})
|
||||
|
||||
it('uses custom title when provided', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models',
|
||||
title: 'Custom Model Browser'
|
||||
})
|
||||
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
expect(dialogCall.props.title).toBe('Custom Model Browser')
|
||||
})
|
||||
|
||||
it('calls getAssetsByTag with correct assetType parameter', async () => {
|
||||
setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models'
|
||||
})
|
||||
|
||||
expect(mockGetAssetsByTag).toHaveBeenCalledWith('models')
|
||||
})
|
||||
|
||||
it('passes fetched assets to dialog props', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const mockAssets = [
|
||||
createMockAsset({ id: 'asset-1', name: 'model1.safetensors' }),
|
||||
createMockAsset({ id: 'asset-2', name: 'model2.safetensors' })
|
||||
]
|
||||
|
||||
mockGetAssetsByTag.mockResolvedValueOnce(mockAssets)
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models'
|
||||
})
|
||||
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
expect(dialogCall.props.assets).toEqual(mockAssets)
|
||||
})
|
||||
|
||||
it('handles asset fetch errors gracefully', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
mockGetAssetsByTag.mockRejectedValueOnce(new Error('Network error'))
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models'
|
||||
})
|
||||
|
||||
expect(mockShowDialog).toHaveBeenCalled()
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
expect(dialogCall.props.assets).toEqual([])
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch assets for tag:',
|
||||
'models',
|
||||
expect.any(Error)
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.show() title formatting', () => {
|
||||
it('formats title with VAE acronym uppercase', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
mockGetAssetsForNodeType.mockResolvedValueOnce([
|
||||
createMockAsset({ tags: ['models', 'vae'] })
|
||||
])
|
||||
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: 'VAELoader',
|
||||
inputName: 'vae_name'
|
||||
})
|
||||
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
expect(dialogCall.props.title).toContain('VAE')
|
||||
})
|
||||
|
||||
it('replaces underscores with spaces in tag names', async () => {
|
||||
const { mockShowDialog } = setupDialogMocks()
|
||||
mockGetAssetsForNodeType.mockResolvedValueOnce([
|
||||
createMockAsset({ tags: ['models', 'style_models'] })
|
||||
])
|
||||
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: 'StyleModelLoader',
|
||||
inputName: 'style_model_name'
|
||||
})
|
||||
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
expect(dialogCall.props.title).toContain('style models')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user