mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-02 20:22:08 +00:00
[feat] add AssetBrowserModal
And all related sub components
This commit is contained in:
315
tests-ui/platform/assets/composables/useAssetBrowser.test.ts
Normal file
315
tests-ui/platform/assets/composables/useAssetBrowser.test.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
describe('useAssetBrowser', () => {
|
||||
// Test fixtures - minimal data focused on functionality being tested
|
||||
const createApiAsset = (overrides: Partial<AssetItem> = {}): AssetItem => ({
|
||||
id: 'test-id',
|
||||
name: 'test-asset.safetensors',
|
||||
asset_hash: 'blake3:abc123',
|
||||
size: 1024,
|
||||
mime_type: 'application/octet-stream',
|
||||
tags: ['models', 'checkpoints'],
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
last_access_time: '2024-01-01T00:00:00Z',
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('Asset Transformation', () => {
|
||||
it('transforms API asset to include display properties', () => {
|
||||
const apiAsset = createApiAsset({
|
||||
size: 2147483648, // 2GB
|
||||
user_metadata: { description: 'Test model' }
|
||||
})
|
||||
|
||||
const { transformAssetForDisplay } = useAssetBrowser([apiAsset])
|
||||
const result = transformAssetForDisplay(apiAsset)
|
||||
|
||||
// Preserves API properties
|
||||
expect(result.id).toBe(apiAsset.id)
|
||||
expect(result.name).toBe(apiAsset.name)
|
||||
|
||||
// Adds display properties
|
||||
expect(result.description).toBe('Test model')
|
||||
expect(result.formattedSize).toBe('2.0 GB')
|
||||
expect(result.badges).toContainEqual({
|
||||
label: 'checkpoints',
|
||||
type: 'type'
|
||||
})
|
||||
expect(result.badges).toContainEqual({ label: '2.0 GB', type: 'size' })
|
||||
})
|
||||
|
||||
it('creates fallback description from tags when metadata missing', () => {
|
||||
const apiAsset = createApiAsset({
|
||||
tags: ['models', 'loras'],
|
||||
user_metadata: undefined
|
||||
})
|
||||
|
||||
const { transformAssetForDisplay } = useAssetBrowser([apiAsset])
|
||||
const result = transformAssetForDisplay(apiAsset)
|
||||
|
||||
expect(result.description).toBe('loras model')
|
||||
})
|
||||
|
||||
it('formats various file sizes correctly', () => {
|
||||
const { transformAssetForDisplay } = useAssetBrowser([])
|
||||
|
||||
const testCases = [
|
||||
{ size: 512, expected: '512.0 B' },
|
||||
{ size: 1536, expected: '1.5 KB' },
|
||||
{ size: 2097152, expected: '2.0 MB' },
|
||||
{ size: 3221225472, expected: '3.0 GB' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ size, expected }) => {
|
||||
const asset = createApiAsset({ size })
|
||||
const result = transformAssetForDisplay(asset)
|
||||
expect(result.formattedSize).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tag-Based Filtering', () => {
|
||||
it('filters assets by category tag', async () => {
|
||||
const assets = [
|
||||
createApiAsset({ id: '1', tags: ['models', 'checkpoints'] }),
|
||||
createApiAsset({ id: '2', tags: ['models', 'loras'] }),
|
||||
createApiAsset({ id: '3', tags: ['models', 'checkpoints'] })
|
||||
]
|
||||
|
||||
const { setCategory, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setCategory('checkpoints')
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(2)
|
||||
expect(
|
||||
filteredAssets.value.every((asset) =>
|
||||
asset.tags.includes('checkpoints')
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns all assets when category is "all"', async () => {
|
||||
const assets = [
|
||||
createApiAsset({ id: '1', tags: ['models', 'checkpoints'] }),
|
||||
createApiAsset({ id: '2', tags: ['models', 'loras'] })
|
||||
]
|
||||
|
||||
const { setCategory, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setCategory('all')
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search Functionality', () => {
|
||||
it('searches across asset name', async () => {
|
||||
const assets = [
|
||||
createApiAsset({ name: 'realistic_vision.safetensors' }),
|
||||
createApiAsset({ name: 'anime_style.ckpt' }),
|
||||
createApiAsset({ name: 'photorealistic_v2.safetensors' })
|
||||
]
|
||||
|
||||
const { setSearchQuery, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setSearchQuery('realistic')
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(2)
|
||||
expect(
|
||||
filteredAssets.value.every((asset) =>
|
||||
asset.name.toLowerCase().includes('realistic')
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('searches in user metadata description', async () => {
|
||||
const assets = [
|
||||
createApiAsset({
|
||||
name: 'model1.safetensors',
|
||||
user_metadata: { description: 'fantasy artwork model' }
|
||||
}),
|
||||
createApiAsset({
|
||||
name: 'model2.safetensors',
|
||||
user_metadata: { description: 'portrait photography' }
|
||||
})
|
||||
]
|
||||
|
||||
const { setSearchQuery, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setSearchQuery('fantasy')
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(1)
|
||||
expect(filteredAssets.value[0].name).toBe('model1.safetensors')
|
||||
})
|
||||
|
||||
it('handles empty search results', async () => {
|
||||
const assets = [createApiAsset({ name: 'test.safetensors' })]
|
||||
|
||||
const { setSearchQuery, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setSearchQuery('nonexistent')
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Combined Search and Filtering', () => {
|
||||
it('applies both search and category filter', async () => {
|
||||
const assets = [
|
||||
createApiAsset({
|
||||
name: 'realistic_checkpoint.safetensors',
|
||||
tags: ['models', 'checkpoints']
|
||||
}),
|
||||
createApiAsset({
|
||||
name: 'realistic_lora.safetensors',
|
||||
tags: ['models', 'loras']
|
||||
}),
|
||||
createApiAsset({
|
||||
name: 'anime_checkpoint.safetensors',
|
||||
tags: ['models', 'checkpoints']
|
||||
})
|
||||
]
|
||||
|
||||
const { setSearchQuery, setCategory, filteredAssets } =
|
||||
useAssetBrowser(assets)
|
||||
|
||||
setSearchQuery('realistic')
|
||||
setCategory('checkpoints')
|
||||
await nextTick()
|
||||
|
||||
expect(filteredAssets.value).toHaveLength(1)
|
||||
expect(filteredAssets.value[0].name).toBe(
|
||||
'realistic_checkpoint.safetensors'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sorting', () => {
|
||||
it('sorts assets by name', async () => {
|
||||
const assets = [
|
||||
createApiAsset({ name: 'zebra.safetensors' }),
|
||||
createApiAsset({ name: 'alpha.safetensors' }),
|
||||
createApiAsset({ name: 'beta.safetensors' })
|
||||
]
|
||||
|
||||
const { setSortBy, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setSortBy('name')
|
||||
await nextTick()
|
||||
|
||||
const names = filteredAssets.value.map((asset) => asset.name)
|
||||
expect(names).toEqual([
|
||||
'alpha.safetensors',
|
||||
'beta.safetensors',
|
||||
'zebra.safetensors'
|
||||
])
|
||||
})
|
||||
|
||||
it('sorts assets by creation date', async () => {
|
||||
const assets = [
|
||||
createApiAsset({ created_at: '2024-03-01T00:00:00Z' }),
|
||||
createApiAsset({ created_at: '2024-01-01T00:00:00Z' }),
|
||||
createApiAsset({ created_at: '2024-02-01T00:00:00Z' })
|
||||
]
|
||||
|
||||
const { setSortBy, filteredAssets } = useAssetBrowser(assets)
|
||||
|
||||
setSortBy('date')
|
||||
await nextTick()
|
||||
|
||||
const dates = filteredAssets.value.map((asset) => asset.created_at)
|
||||
expect(dates).toEqual([
|
||||
'2024-03-01T00:00:00Z',
|
||||
'2024-02-01T00:00:00Z',
|
||||
'2024-01-01T00:00:00Z'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Asset Selection', () => {
|
||||
it('returns selected asset UUID for efficient handling', () => {
|
||||
const asset = createApiAsset({
|
||||
id: 'test-uuid-123',
|
||||
name: 'selected_model.safetensors'
|
||||
})
|
||||
const { selectAsset, transformAssetForDisplay } = useAssetBrowser([asset])
|
||||
|
||||
const displayAsset = transformAssetForDisplay(asset)
|
||||
const result = selectAsset(displayAsset)
|
||||
|
||||
expect(result).toBe('test-uuid-123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dynamic Category Extraction', () => {
|
||||
it('extracts categories from asset tags', () => {
|
||||
const assets = [
|
||||
createApiAsset({ tags: ['models', 'checkpoints'] }),
|
||||
createApiAsset({ tags: ['models', 'loras'] }),
|
||||
createApiAsset({ tags: ['models', 'checkpoints'] }) // duplicate
|
||||
]
|
||||
|
||||
const { availableCategories } = useAssetBrowser(assets)
|
||||
|
||||
expect(availableCategories.value).toEqual([
|
||||
{ id: 'all', label: 'All Models', icon: 'i-lucide:folder' },
|
||||
{ id: 'checkpoints', label: 'Checkpoints', icon: 'i-lucide:package' },
|
||||
{ id: 'loras', label: 'Loras', icon: 'i-lucide:package' }
|
||||
])
|
||||
})
|
||||
|
||||
it('handles assets with no category tag', () => {
|
||||
const assets = [
|
||||
createApiAsset({ tags: ['models'] }), // No second tag
|
||||
createApiAsset({ tags: ['models', 'vae'] })
|
||||
]
|
||||
|
||||
const { availableCategories } = useAssetBrowser(assets)
|
||||
|
||||
expect(availableCategories.value).toEqual([
|
||||
{ id: 'all', label: 'All Models', icon: 'i-lucide:folder' },
|
||||
{ id: 'vae', label: 'Vae', icon: 'i-lucide:package' }
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores non-models root tags', () => {
|
||||
const assets = [
|
||||
createApiAsset({ tags: ['input', 'images'] }),
|
||||
createApiAsset({ tags: ['models', 'checkpoints'] })
|
||||
]
|
||||
|
||||
const { availableCategories } = useAssetBrowser(assets)
|
||||
|
||||
expect(availableCategories.value).toEqual([
|
||||
{ id: 'all', label: 'All Models', icon: 'i-lucide:folder' },
|
||||
{ id: 'checkpoints', label: 'Checkpoints', icon: 'i-lucide:package' }
|
||||
])
|
||||
})
|
||||
|
||||
it('computes content title from selected category', () => {
|
||||
const assets = [createApiAsset({ tags: ['models', 'checkpoints'] })]
|
||||
const { setCategory, contentTitle } = useAssetBrowser(assets)
|
||||
|
||||
// Default
|
||||
expect(contentTitle.value).toBe('All Models')
|
||||
|
||||
// Set specific category
|
||||
setCategory('checkpoints')
|
||||
expect(contentTitle.value).toBe('Checkpoints')
|
||||
|
||||
// Unknown category
|
||||
setCategory('unknown')
|
||||
expect(contentTitle.value).toBe('Assets')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user