mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
chore: migrate tests from tests-ui/ to colocate with source files (#7811)
## Summary Migrates all unit tests from `tests-ui/` to colocate with their source files in `src/`, improving discoverability and maintainability. ## Changes - **What**: Relocated all unit tests to be adjacent to the code they test, following the `<source>.test.ts` naming convention - **Config**: Updated `vitest.config.ts` to remove `tests-ui` include pattern and `@tests-ui` alias - **Docs**: Moved testing documentation to `docs/testing/` with updated paths and patterns ## Review Focus - Migration patterns documented in `temp/plans/migrate-tests-ui-to-src.md` - Tests use `@/` path aliases instead of relative imports - Shared fixtures placed in `__fixtures__/` directories ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7811-chore-migrate-tests-from-tests-ui-to-colocate-with-source-files-2da6d73d36508147a4cce85365dee614) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -1,367 +0,0 @@
|
||||
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<string, string>) =>
|
||||
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/common/SearchBox.vue', () => ({
|
||||
default: {
|
||||
name: 'SearchBox',
|
||||
props: ['modelValue', 'size', 'placeholder', 'class'],
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<input
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
data-testid="search-box"
|
||||
/>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/widget/layout/BaseModalLayout.vue', () => ({
|
||||
default: {
|
||||
name: 'BaseModalLayout',
|
||||
props: ['contentTitle'],
|
||||
emits: ['close'],
|
||||
template: `
|
||||
<div data-testid="base-modal-layout">
|
||||
<div v-if="$slots.leftPanel" data-testid="left-panel">
|
||||
<slot name="leftPanel" />
|
||||
</div>
|
||||
<div data-testid="header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div v-if="$slots.contentFilter" data-testid="content-filter">
|
||||
<slot name="contentFilter" />
|
||||
</div>
|
||||
<div data-testid="content">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/widget/panel/LeftSidePanel.vue', () => ({
|
||||
default: {
|
||||
name: 'LeftSidePanel',
|
||||
props: ['modelValue', 'navItems'],
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<div data-testid="left-side-panel">
|
||||
<div v-if="$slots['header-title']" data-testid="header-title">
|
||||
<slot name="header-title" />
|
||||
</div>
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.id"
|
||||
@click="$emit('update:modelValue', item.id)"
|
||||
:data-testid="'nav-item-' + item.id"
|
||||
:class="{ active: modelValue === item.id }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/components/AssetFilterBar.vue', () => ({
|
||||
default: {
|
||||
name: 'AssetFilterBar',
|
||||
props: ['assets'],
|
||||
emits: ['filter-change'],
|
||||
template: `
|
||||
<div data-testid="asset-filter-bar">
|
||||
Filter bar with {{ assets?.length ?? 0 }} assets
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/components/AssetGrid.vue', () => ({
|
||||
default: {
|
||||
name: 'AssetGrid',
|
||||
props: ['assets', 'loading'],
|
||||
emits: ['asset-select'],
|
||||
template: `
|
||||
<div data-testid="asset-grid">
|
||||
<div
|
||||
v-for="asset in assets"
|
||||
:key="asset.id"
|
||||
@click="$emit('asset-select', asset)"
|
||||
:data-testid="'asset-' + asset.id"
|
||||
class="asset-card"
|
||||
>
|
||||
{{ asset.name }}
|
||||
</div>
|
||||
<div v-if="assets.length === 0" data-testid="empty-state">
|
||||
No assets found
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string, params?: Record<string, string>) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: {
|
||||
t: (key: string, params?: Record<string, string>) =>
|
||||
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<string, unknown>) => {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
return mount(AssetBrowserModal, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
stubs: {
|
||||
'i-lucide:folder': {
|
||||
template: '<div data-testid="folder-icon"></div>'
|
||||
}
|
||||
},
|
||||
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"}'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,357 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
|
||||
import type { FilterState } from '@/platform/assets/components/AssetFilterBar.vue'
|
||||
import {
|
||||
createAssetWithSpecificBaseModel,
|
||||
createAssetWithSpecificExtension,
|
||||
createAssetWithoutBaseModel
|
||||
} from '@/platform/assets/fixtures/ui-mock-assets'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
// Mock @/i18n directly since component imports { t } from '@/i18n'
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string) => key
|
||||
}))
|
||||
|
||||
// Mock components with minimal functionality for business logic testing
|
||||
vi.mock('@/components/input/MultiSelect.vue', () => ({
|
||||
default: {
|
||||
name: 'MultiSelect',
|
||||
props: {
|
||||
modelValue: Array,
|
||||
label: String,
|
||||
options: Array,
|
||||
class: String
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<div data-testid="multi-select">
|
||||
<select multiple @change="$emit('update:modelValue', Array.from($event.target.selectedOptions).map(o => ({ name: o.text, value: o.value })))">
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/input/SingleSelect.vue', () => ({
|
||||
default: {
|
||||
name: 'SingleSelect',
|
||||
props: {
|
||||
modelValue: String,
|
||||
label: String,
|
||||
options: Array,
|
||||
class: String
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<div data-testid="single-select">
|
||||
<select @change="$emit('update:modelValue', $event.target.value)">
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
// Test factory functions
|
||||
function mountAssetFilterBar(props = {}) {
|
||||
return mount(AssetFilterBar, {
|
||||
props,
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key: string) => key
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions to find filters by user-facing attributes
|
||||
function findFileFormatsFilter(
|
||||
wrapper: ReturnType<typeof mountAssetFilterBar>
|
||||
) {
|
||||
return wrapper.findComponent(
|
||||
'[data-component-id="asset-filter-file-formats"]'
|
||||
)
|
||||
}
|
||||
|
||||
function findBaseModelsFilter(wrapper: ReturnType<typeof mountAssetFilterBar>) {
|
||||
return wrapper.findComponent('[data-component-id="asset-filter-base-models"]')
|
||||
}
|
||||
|
||||
function findOwnershipFilter(wrapper: ReturnType<typeof mountAssetFilterBar>) {
|
||||
return wrapper.findComponent('[data-component-id="asset-filter-ownership"]')
|
||||
}
|
||||
|
||||
function findSortFilter(wrapper: ReturnType<typeof mountAssetFilterBar>) {
|
||||
return wrapper.findComponent('[data-component-id="asset-filter-sort"]')
|
||||
}
|
||||
|
||||
describe('AssetFilterBar', () => {
|
||||
describe('Filter State Management', () => {
|
||||
it('handles multiple simultaneous filter changes correctly', async () => {
|
||||
// Provide assets with options so filters are visible
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors'),
|
||||
createAssetWithSpecificExtension('ckpt'),
|
||||
createAssetWithSpecificBaseModel('sd15'),
|
||||
createAssetWithSpecificBaseModel('sdxl')
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
// Update file formats
|
||||
const fileFormatSelect = findFileFormatsFilter(wrapper)
|
||||
const fileFormatSelectElement = fileFormatSelect.find('select')
|
||||
const options = fileFormatSelectElement.findAll('option')
|
||||
const ckptOption = options.find((o) => o.element.value === 'ckpt')!
|
||||
const safetensorsOption = options.find(
|
||||
(o) => o.element.value === 'safetensors'
|
||||
)!
|
||||
ckptOption.element.selected = true
|
||||
safetensorsOption.element.selected = true
|
||||
await fileFormatSelectElement.trigger('change')
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Update base models
|
||||
const baseModelSelect = findBaseModelsFilter(wrapper)
|
||||
const baseModelSelectElement = baseModelSelect.find('select')
|
||||
const sdxlOption = baseModelSelectElement
|
||||
.findAll('option')
|
||||
.find((o) => o.element.value === 'sdxl')
|
||||
sdxlOption!.element.selected = true
|
||||
await baseModelSelectElement.trigger('change')
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Update sort
|
||||
const sortSelect = findSortFilter(wrapper)
|
||||
const sortSelectElement = sortSelect.find('select')
|
||||
sortSelectElement.element.value = 'name-desc'
|
||||
await sortSelectElement.trigger('change')
|
||||
|
||||
await nextTick()
|
||||
|
||||
const emitted = wrapper.emitted('filterChange')
|
||||
expect(emitted).toBeTruthy()
|
||||
expect(emitted!.length).toBeGreaterThanOrEqual(3)
|
||||
|
||||
// Check final state
|
||||
const finalState: FilterState = emitted![
|
||||
emitted!.length - 1
|
||||
][0] as FilterState
|
||||
expect(finalState.fileFormats).toEqual(['ckpt', 'safetensors'])
|
||||
expect(finalState.baseModels).toEqual(['sdxl'])
|
||||
expect(finalState.sortBy).toBe('name-desc')
|
||||
})
|
||||
|
||||
it('ensures FilterState interface compliance', async () => {
|
||||
// Provide assets with options so filters are visible
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors'),
|
||||
createAssetWithSpecificBaseModel('sd15')
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const fileFormatSelect = findFileFormatsFilter(wrapper)
|
||||
const fileFormatSelectElement = fileFormatSelect.find('select')
|
||||
const ckptOption = fileFormatSelectElement.findAll('option')[0]
|
||||
ckptOption.element.selected = true
|
||||
await fileFormatSelectElement.trigger('change')
|
||||
|
||||
await nextTick()
|
||||
|
||||
const emitted = wrapper.emitted('filterChange')
|
||||
const filterState = emitted![0][0] as FilterState
|
||||
|
||||
// Type and structure assertions
|
||||
expect(Array.isArray(filterState.fileFormats)).toBe(true)
|
||||
expect(Array.isArray(filterState.baseModels)).toBe(true)
|
||||
expect(typeof filterState.sortBy).toBe('string')
|
||||
|
||||
// Value type assertions
|
||||
expect(filterState.fileFormats.every((f) => typeof f === 'string')).toBe(
|
||||
true
|
||||
)
|
||||
expect(filterState.baseModels.every((m) => typeof m === 'string')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dynamic Filter Options', () => {
|
||||
it('should use dynamic file format options based on actual assets', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors'),
|
||||
createAssetWithSpecificExtension('ckpt'),
|
||||
createAssetWithSpecificExtension('pt')
|
||||
]
|
||||
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const fileFormatSelect = findFileFormatsFilter(wrapper)
|
||||
const options = fileFormatSelect.findAll('option')
|
||||
expect(
|
||||
options.map((o) => ({ name: o.text(), value: o.element.value }))
|
||||
).toEqual([
|
||||
{ name: '.ckpt', value: 'ckpt' },
|
||||
{ name: '.pt', value: 'pt' },
|
||||
{ name: '.safetensors', value: 'safetensors' }
|
||||
])
|
||||
})
|
||||
|
||||
it('should use dynamic base model options based on actual assets', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificBaseModel('sd15'),
|
||||
createAssetWithSpecificBaseModel('sdxl'),
|
||||
createAssetWithSpecificBaseModel('sd35')
|
||||
]
|
||||
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const baseModelSelect = findBaseModelsFilter(wrapper)
|
||||
const options = baseModelSelect.findAll('option')
|
||||
expect(
|
||||
options.map((o) => ({ name: o.text(), value: o.element.value }))
|
||||
).toEqual([
|
||||
{ name: 'sd15', value: 'sd15' },
|
||||
{ name: 'sd35', value: 'sd35' },
|
||||
{ name: 'sdxl', value: 'sdxl' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Filter Visibility', () => {
|
||||
it('hides file format filter when no options available', () => {
|
||||
const assets: AssetItem[] = [] // No assets = no file format options
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const fileFormatSelect = findFileFormatsFilter(wrapper)
|
||||
expect(fileFormatSelect.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides base model filter when no options available', () => {
|
||||
const assets = [createAssetWithoutBaseModel()] // Asset without base model = no base model options
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const baseModelSelect = findBaseModelsFilter(wrapper)
|
||||
expect(baseModelSelect.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows both filters when options are available', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors'),
|
||||
createAssetWithSpecificBaseModel('sd15')
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const fileFormatSelect = findFileFormatsFilter(wrapper)
|
||||
const baseModelSelect = findBaseModelsFilter(wrapper)
|
||||
|
||||
expect(fileFormatSelect.exists()).toBe(true)
|
||||
expect(baseModelSelect.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('hides both filters when no assets provided', () => {
|
||||
const wrapper = mountAssetFilterBar()
|
||||
|
||||
const fileFormatSelect = findFileFormatsFilter(wrapper)
|
||||
const baseModelSelect = findBaseModelsFilter(wrapper)
|
||||
|
||||
expect(fileFormatSelect.exists()).toBe(false)
|
||||
expect(baseModelSelect.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides ownership filter when no mutable assets', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors', true) // immutable
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const ownershipSelect = findOwnershipFilter(wrapper)
|
||||
expect(ownershipSelect.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows ownership filter when mutable assets exist', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors', false) // mutable
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const ownershipSelect = findOwnershipFilter(wrapper)
|
||||
expect(ownershipSelect.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows ownership filter when mixed assets exist', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors', true), // immutable
|
||||
createAssetWithSpecificExtension('ckpt', false) // mutable
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const ownershipSelect = findOwnershipFilter(wrapper)
|
||||
expect(ownershipSelect.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows ownership filter with allAssets when provided', () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors', true) // immutable
|
||||
]
|
||||
const allAssets = [
|
||||
createAssetWithSpecificExtension('safetensors', true), // immutable
|
||||
createAssetWithSpecificExtension('ckpt', false) // mutable
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets, allAssets })
|
||||
|
||||
const ownershipSelect = findOwnershipFilter(wrapper)
|
||||
expect(ownershipSelect.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Ownership Filter', () => {
|
||||
it('emits ownership filter changes', async () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors', false) // mutable
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const ownershipSelect = findOwnershipFilter(wrapper)
|
||||
expect(ownershipSelect.exists()).toBe(true)
|
||||
|
||||
const ownershipSelectElement = ownershipSelect.find('select')
|
||||
ownershipSelectElement.element.value = 'my-models'
|
||||
await ownershipSelectElement.trigger('change')
|
||||
await nextTick()
|
||||
|
||||
const emitted = wrapper.emitted('filterChange')
|
||||
expect(emitted).toBeTruthy()
|
||||
|
||||
const filterState = emitted![emitted!.length - 1][0] as FilterState
|
||||
expect(filterState.ownership).toBe('my-models')
|
||||
})
|
||||
|
||||
it('ownership filter defaults to "all"', async () => {
|
||||
const assets = [
|
||||
createAssetWithSpecificExtension('safetensors', false) // mutable
|
||||
]
|
||||
const wrapper = mountAssetFilterBar({ assets })
|
||||
|
||||
const sortSelect = findSortFilter(wrapper)
|
||||
const sortSelectElement = sortSelect.find('select')
|
||||
sortSelectElement.element.value = 'recent'
|
||||
await sortSelectElement.trigger('change')
|
||||
await nextTick()
|
||||
|
||||
const emitted = wrapper.emitted('filterChange')
|
||||
const filterState = emitted![0][0] as FilterState
|
||||
expect(filterState.ownership).toBe('all')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user