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' import { useAssetsStore } from '@/stores/assetsStore' vi.mock('@/i18n', () => ({ t: (key: string, params?: Record) => params ? `${key}:${JSON.stringify(params)}` : key, d: (date: Date) => date.toLocaleDateString() })) vi.mock('@/stores/assetsStore', () => { const store = { modelAssetsByNodeType: new Map(), modelLoadingByNodeType: new Map(), updateModelsForNodeType: vi.fn(), updateModelsForTag: vi.fn() } return { useAssetsStore: () => store } }) vi.mock('@/stores/modelToNodeStore', () => ({ useModelToNodeStore: () => ({ getCategoryForNodeType: () => 'checkpoints' }) })) vi.mock('@/components/common/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 } } }) } const mockStore = useAssetsStore() beforeEach(() => { vi.resetAllMocks() mockStore.modelAssetsByNodeType.clear() mockStore.modelLoadingByNodeType.clear() }) 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') ] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', 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') ] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', 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('triggers store refresh for node type on mount', async () => { createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() expect(mockStore.updateModelsForNodeType).toHaveBeenCalledWith( 'CheckpointLoaderSimple' ) }) it('displays cached assets immediately from store', async () => { const assets = [createTestAsset('asset1', 'Cached Model', 'checkpoints')] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' }) const assetGrid = wrapper.findComponent({ name: 'AssetGrid' }) const gridAssets = assetGrid.props('assets') as AssetItem[] expect(gridAssets).toHaveLength(1) expect(gridAssets[0].name).toBe('Cached Model') }) it('triggers store refresh for asset type (tag) on mount', async () => { createWrapper({ assetType: 'models' }) await flushPromises() expect(mockStore.updateModelsForTag).toHaveBeenCalledWith('models') }) it('uses tag: prefix for cache key when assetType is provided', async () => { const assets = [createTestAsset('asset1', 'Tagged Model', 'models')] mockStore.modelAssetsByNodeType.set('tag:models', assets) const wrapper = createWrapper({ assetType: 'models' }) await flushPromises() const assetGrid = wrapper.findComponent({ name: 'AssetGrid' }) const gridAssets = assetGrid.props('assets') as AssetItem[] expect(gridAssets).toHaveLength(1) expect(gridAssets[0].name).toBe('Tagged Model') }) }) describe('Asset Selection', () => { it('emits asset-select event when asset is selected', async () => { const assets = [createTestAsset('asset1', 'Model A', 'checkpoints')] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', 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')] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', 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 () => { 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 () => { 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') ] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', 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 () => { 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')] mockStore.modelAssetsByNodeType.set('CheckpointLoaderSimple', assets) const wrapper = createWrapper({ nodeType: 'CheckpointLoaderSimple' }) await flushPromises() const layout = wrapper.findComponent({ name: 'BaseModalLayout' }) expect(layout.props('contentTitle')).toBe( 'assetBrowser.allCategory:{"category":"Checkpoints"}' ) }) }) })