import { flushPromises, mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' const mockAssetService = vi.hoisted(() => ({ getAssetsForNodeType: vi.fn(), getAssetsByTag: vi.fn(), getAssetDetails: vi.fn((id: string) => Promise.resolve({ id, name: 'Test Model', user_metadata: { filename: 'Test Model' } }) ) })) vi.mock('@/i18n', () => ({ t: (key: string, params?: Record) => params ? `${key}:${JSON.stringify(params)}` : key, d: (date: Date) => date.toLocaleDateString() })) vi.mock('@/platform/assets/services/assetService', () => ({ assetService: mockAssetService })) vi.mock('@/stores/modelToNodeStore', () => ({ useModelToNodeStore: () => ({ getCategoryForNodeType: () => 'checkpoints' }) })) vi.mock('@/components/input/SearchBox.vue', () => ({ default: { name: 'SearchBox', props: ['modelValue', 'size', 'placeholder', 'class'], emits: ['update:modelValue'], template: ` ` } })) vi.mock('@/components/widget/layout/BaseModalLayout.vue', () => ({ default: { name: 'BaseModalLayout', props: ['contentTitle'], emits: ['close'], template: `
` } })) vi.mock('@/components/widget/panel/LeftSidePanel.vue', () => ({ default: { name: 'LeftSidePanel', props: ['modelValue', 'navItems'], emits: ['update:modelValue'], template: `
` } })) vi.mock('@/platform/assets/components/AssetFilterBar.vue', () => ({ default: { name: 'AssetFilterBar', props: ['assets'], emits: ['filter-change'], template: `
Filter bar with {{ assets?.length ?? 0 }} assets
` } })) vi.mock('@/platform/assets/components/AssetGrid.vue', () => ({ default: { name: 'AssetGrid', props: ['assets', 'loading'], emits: ['asset-select'], template: `
{{ asset.name }}
No assets found
` } })) vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string, params?: Record) => params ? `${key}:${JSON.stringify(params)}` : key }), createI18n: () => ({ global: { t: (key: string, params?: Record) => params ? `${key}:${JSON.stringify(params)}` : key } }) })) describe('AssetBrowserModal', () => { const createTestAsset = ( id: string, name: string, category: string ): AssetItem => ({ id, name, asset_hash: `blake3:${id.padEnd(64, '0')}`, size: 1024000, mime_type: 'application/octet-stream', tags: ['models', category, 'test'], preview_url: `/api/assets/${id}/content`, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', last_access_time: '2024-01-01T00:00:00Z', user_metadata: { description: `Test ${name}`, base_model: 'sd15' } }) const createWrapper = (props: Record) => { const pinia = createPinia() setActivePinia(pinia) return mount(AssetBrowserModal, { props, global: { plugins: [pinia], stubs: { 'i-lucide:folder': { template: '
' } }, mocks: { $t: (key: string) => key } } }) } beforeEach(() => { mockAssetService.getAssetsForNodeType.mockReset() mockAssetService.getAssetsByTag.mockReset() }) describe('Integration with useAssetBrowser', () => { it('passes filtered assets from composable to AssetGrid', async () => { const assets = [ createTestAsset('asset1', 'Model A', 'checkpoints'), createTestAsset('asset2', 'Model B', 'loras') ] mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() const assetGrid = wrapper.findComponent({ name: 'AssetGrid' }) const gridAssets = assetGrid.props('assets') as AssetItem[] expect(gridAssets).toHaveLength(2) expect(gridAssets[0].id).toBe('asset1') }) it('passes category-filtered assets to AssetFilterBar', async () => { const assets = [ createTestAsset('c1', 'model.safetensors', 'checkpoints'), createTestAsset('l1', 'lora.pt', 'loras') ] mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple', showLeftPanel: true }) await flushPromises() const filterBar = wrapper.findComponent({ name: 'AssetFilterBar' }) const filterBarAssets = filterBar.props('assets') as AssetItem[] expect(filterBarAssets).toHaveLength(2) }) }) describe('Data fetching', () => { it('fetches assets for node type', async () => { mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([]) createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() expect(mockAssetService.getAssetsForNodeType).toHaveBeenCalledWith( 'CheckpointLoaderSimple' ) }) it('fetches assets for tag when node type not provided', async () => { mockAssetService.getAssetsByTag.mockResolvedValueOnce([]) createWrapper({ assetType: 'loras' }) await flushPromises() expect(mockAssetService.getAssetsByTag).toHaveBeenCalledWith('loras') }) }) describe('Asset Selection', () => { it('emits asset-select event when asset is selected', async () => { const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')] mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() const assetGrid = wrapper.findComponent({ name: 'AssetGrid' }) await assetGrid.vm.$emit('asset-select', assets[0]) expect(wrapper.emitted('asset-select')).toEqual([[assets[0]]]) }) it('executes onSelect callback when provided', async () => { const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')] mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets) const onSelect = vi.fn() const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple', onSelect }) await flushPromises() const assetGrid = wrapper.findComponent({ name: 'AssetGrid' }) await assetGrid.vm.$emit('asset-select', assets[0]) expect(onSelect).toHaveBeenCalledWith(assets[0]) }) }) describe('Left Panel Conditional Logic', () => { it('hides left panel by default when showLeftPanel is undefined', async () => { mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([]) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() const leftPanel = wrapper.find('[data-testid="left-panel"]') expect(leftPanel.exists()).toBe(false) }) it('shows left panel when showLeftPanel prop is explicitly true', async () => { mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([]) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple', showLeftPanel: true }) await flushPromises() const leftPanel = wrapper.find('[data-testid="left-panel"]') expect(leftPanel.exists()).toBe(true) }) }) describe('Filter Options Reactivity', () => { it('updates filter options when category changes', async () => { const assets = [ createTestAsset('asset1', 'Model A', 'checkpoints'), createTestAsset('asset2', 'Model B', 'loras') ] mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple', showLeftPanel: true }) await flushPromises() const filterBar = wrapper.findComponent({ name: 'AssetFilterBar' }) expect(filterBar.props('assets')).toHaveLength(2) const leftPanel = wrapper.findComponent({ name: 'LeftSidePanel' }) await leftPanel.vm.$emit('update:modelValue', 'loras') await wrapper.vm.$nextTick() expect(filterBar.props('assets')).toHaveLength(1) }) }) describe('Title Management', () => { it('passes custom title to BaseModalLayout when title prop provided', async () => { mockAssetService.getAssetsForNodeType.mockResolvedValueOnce([]) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple', title: 'Custom Title' }) await flushPromises() const layout = wrapper.findComponent({ name: 'BaseModalLayout' }) expect(layout.props('contentTitle')).toBe('Custom Title') }) it('passes computed contentTitle to BaseModalLayout when no title prop', async () => { const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')] mockAssetService.getAssetsForNodeType.mockResolvedValueOnce(assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() const layout = wrapper.findComponent({ name: 'BaseModalLayout' }) expect(layout.props('contentTitle')).toBe( 'assetBrowser.allCategory:{"category":"Checkpoints"}' ) }) }) })