feat: Add more Storybook stories for UI components

This commit is contained in:
snomiao
2025-09-22 21:10:30 +00:00
parent e5d4d07d32
commit ce71c2c529
521 changed files with 8078 additions and 22384 deletions

View File

@@ -1,304 +0,0 @@
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
// Mock external dependencies with minimal functionality needed for business logic tests
vi.mock('@/components/input/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 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">
<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/AssetGrid.vue', () => ({
default: {
name: 'AssetGrid',
props: ['assets'],
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) => 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 = (
assets: AssetItem[] = [],
props: Record<string, unknown> = {}
) => {
const pinia = createPinia()
setActivePinia(pinia)
return mount(AssetBrowserModal, {
props: {
assets: assets,
...props
},
global: {
plugins: [pinia],
stubs: {
'i-lucide:folder': {
template: '<div data-testid="folder-icon"></div>'
}
},
mocks: {
$t: (key: string) => key
}
}
})
}
describe('Search Functionality', () => {
it('filters assets when search query changes', async () => {
const assets = [
createTestAsset('asset1', 'Checkpoint Model A', 'checkpoints'),
createTestAsset('asset2', 'Checkpoint Model B', 'checkpoints'),
createTestAsset('asset3', 'LoRA Model C', 'loras')
]
const wrapper = createWrapper(assets)
const searchBox = wrapper.find('[data-testid="search-box"]')
// Search for "Checkpoint"
await searchBox.setValue('Checkpoint')
await nextTick()
// Should filter to only checkpoint assets
const assetGrid = wrapper.findComponent({ name: 'AssetGrid' })
const filteredAssets = assetGrid.props('assets') as AssetDisplayItem[]
expect(filteredAssets.length).toBe(2)
expect(
filteredAssets.every((asset: AssetDisplayItem) =>
asset.name.includes('Checkpoint')
)
).toBe(true)
})
it('search is case insensitive', async () => {
const assets = [
createTestAsset('asset1', 'LoRA Model C', 'loras'),
createTestAsset('asset2', 'Checkpoint Model', 'checkpoints')
]
const wrapper = createWrapper(assets)
const searchBox = wrapper.find('[data-testid="search-box"]')
// Search with different case
await searchBox.setValue('lora')
await nextTick()
const assetGrid = wrapper.findComponent({ name: 'AssetGrid' })
const filteredAssets = assetGrid.props('assets') as AssetDisplayItem[]
expect(filteredAssets.length).toBe(1)
expect(filteredAssets[0].name).toContain('LoRA')
})
it('shows empty state when search has no results', async () => {
const assets = [
createTestAsset('asset1', 'Checkpoint Model', 'checkpoints')
]
const wrapper = createWrapper(assets)
const searchBox = wrapper.find('[data-testid="search-box"]')
// Search for something that doesn't exist
await searchBox.setValue('nonexistent')
await nextTick()
expect(wrapper.find('[data-testid="empty-state"]').exists()).toBe(true)
})
})
describe('Category Navigation', () => {
it('filters assets by selected category', async () => {
const assets = [
createTestAsset('asset1', 'Checkpoint Model A', 'checkpoints'),
createTestAsset('asset2', 'LoRA Model C', 'loras'),
createTestAsset('asset3', 'VAE Model D', 'vae')
]
const wrapper = createWrapper(assets, { showLeftPanel: true })
// Wait for Vue reactivity and component mounting
await nextTick()
// Check if left panel exists first (since we have multiple categories)
const leftPanel = wrapper.find('[data-testid="left-panel"]')
expect(leftPanel.exists()).toBe(true)
// Check if the nav item exists before clicking
const lorasNavItem = wrapper.find('[data-testid="nav-item-loras"]')
expect(lorasNavItem.exists()).toBe(true)
// Click the loras category
await lorasNavItem.trigger('click')
await nextTick()
// Should filter to only LoRA assets
const assetGrid = wrapper.findComponent({ name: 'AssetGrid' })
const filteredAssets = assetGrid.props('assets') as AssetDisplayItem[]
expect(filteredAssets.length).toBe(1)
expect(filteredAssets[0].name).toContain('LoRA')
})
})
describe('Asset Selection', () => {
it('emits asset-select event when asset is selected', async () => {
const assets = [createTestAsset('asset1', 'Test Model', 'checkpoints')]
const wrapper = createWrapper(assets)
// Click on first asset
await wrapper.find('[data-testid="asset-asset1"]').trigger('click')
const emitted = wrapper.emitted('asset-select')
expect(emitted).toBeDefined()
expect(emitted).toHaveLength(1)
const emittedAsset = emitted![0][0] as AssetDisplayItem
expect(emittedAsset.id).toBe('asset1')
})
it('executes onSelect callback when provided', async () => {
const onSelectSpy = vi.fn()
const assets = [createTestAsset('asset1', 'Test Model', 'checkpoints')]
const wrapper = createWrapper(assets, { onSelect: onSelectSpy })
// Click on first asset
await wrapper.find('[data-testid="asset-asset1"]').trigger('click')
expect(onSelectSpy).toHaveBeenCalledWith('Test Model')
})
})
describe('Left Panel Conditional Logic', () => {
it('hides left panel by default when showLeftPanel prop is undefined', () => {
const singleCategoryAssets = [
createTestAsset('single1', 'Asset 1', 'checkpoints'),
createTestAsset('single2', 'Asset 2', 'checkpoints')
]
const wrapper = createWrapper(singleCategoryAssets)
expect(wrapper.find('[data-testid="left-panel"]').exists()).toBe(false)
})
it('shows left panel when showLeftPanel prop is explicitly true', () => {
const singleCategoryAssets = [
createTestAsset('single1', 'Asset 1', 'checkpoints')
]
// Force show even with single category
const wrapper = createWrapper(singleCategoryAssets, {
showLeftPanel: true
})
expect(wrapper.find('[data-testid="left-panel"]').exists()).toBe(true)
// Force hide even with multiple categories
wrapper.unmount()
const multiCategoryAssets = [
createTestAsset('asset1', 'Checkpoint', 'checkpoints'),
createTestAsset('asset2', 'LoRA', 'loras')
]
const wrapper2 = createWrapper(multiCategoryAssets, {
showLeftPanel: false
})
expect(wrapper2.find('[data-testid="left-panel"]').exists()).toBe(false)
})
})
})

View File

@@ -1,138 +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'
// 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
describe('AssetFilterBar', () => {
describe('Filter State Management', () => {
it('maintains correct initial state', () => {
const wrapper = mount(AssetFilterBar)
// Test initial state through component props
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
const singleSelect = wrapper.findComponent({ name: 'SingleSelect' })
expect(multiSelects[0].props('modelValue')).toEqual([])
expect(multiSelects[1].props('modelValue')).toEqual([])
expect(singleSelect.props('modelValue')).toBe('name-asc')
})
it('handles multiple simultaneous filter changes correctly', async () => {
const wrapper = mount(AssetFilterBar)
// Update file formats
const fileFormatSelect = wrapper.findAllComponents({
name: 'MultiSelect'
})[0]
await fileFormatSelect.vm.$emit('update:modelValue', [
{ name: '.ckpt', value: 'ckpt' },
{ name: '.safetensors', value: 'safetensors' }
])
await nextTick()
// Update base models
const baseModelSelect = wrapper.findAllComponents({
name: 'MultiSelect'
})[1]
await baseModelSelect.vm.$emit('update:modelValue', [
{ name: 'SD XL', value: 'sdxl' }
])
await nextTick()
// Update sort
const sortSelect = wrapper.findComponent({ name: 'SingleSelect' })
await sortSelect.vm.$emit('update:modelValue', 'popular')
await nextTick()
const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(3)
// Check final state
const finalState: FilterState = emitted![2][0] as FilterState
expect(finalState.fileFormats).toEqual(['ckpt', 'safetensors'])
expect(finalState.baseModels).toEqual(['sdxl'])
expect(finalState.sortBy).toBe('popular')
})
it('ensures FilterState interface compliance', async () => {
const wrapper = mount(AssetFilterBar)
const fileFormatSelect = wrapper.findAllComponents({
name: 'MultiSelect'
})[0]
await fileFormatSelect.vm.$emit('update:modelValue', [
{ name: '.ckpt', value: 'ckpt' }
])
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
)
})
})
})

View File

@@ -1,509 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
vi.mock('@/platform/assets/services/assetService', () => ({
assetService: {
getAssetDetails: vi.fn()
}
}))
vi.mock('@/i18n', () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'assetBrowser.allModels': 'All Models',
'assetBrowser.assets': 'Assets',
'assetBrowser.unknown': 'unknown'
}
return translations[key] || key
},
d: (date: Date) => date.toLocaleDateString()
}))
describe('useAssetBrowser', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
// 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 { filteredAssets } = useAssetBrowser([apiAsset])
const result = filteredAssets.value[0] // Get the transformed asset from filteredAssets
// 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 GB')
expect(result.badges).toContainEqual({
label: 'checkpoints',
type: 'type'
})
expect(result.badges).toContainEqual({ label: '2 GB', type: 'size' })
})
it('creates fallback description from tags when metadata missing', () => {
const apiAsset = createApiAsset({
tags: ['models', 'loras'],
user_metadata: undefined
})
const { filteredAssets } = useAssetBrowser([apiAsset])
const result = filteredAssets.value[0]
expect(result.description).toBe('loras model')
})
it('formats various file sizes correctly', () => {
const testCases = [
{ size: 512, expected: '512 B' },
{ size: 1536, expected: '1.5 KB' },
{ size: 2097152, expected: '2 MB' },
{ size: 3221225472, expected: '3 GB' }
]
testCases.forEach(({ size, expected }) => {
const asset = createApiAsset({ size })
const { filteredAssets } = useAssetBrowser([asset])
const result = filteredAssets.value[0]
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 { selectedCategory, filteredAssets } = useAssetBrowser(assets)
selectedCategory.value = '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 { selectedCategory, filteredAssets } = useAssetBrowser(assets)
selectedCategory.value = '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 { searchQuery, filteredAssets } = useAssetBrowser(assets)
searchQuery.value = '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 { searchQuery, filteredAssets } = useAssetBrowser(assets)
searchQuery.value = '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 { searchQuery, filteredAssets } = useAssetBrowser(assets)
searchQuery.value = '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 { searchQuery, selectedCategory, filteredAssets } =
useAssetBrowser(assets)
searchQuery.value = 'realistic'
selectedCategory.value = '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 { sortBy, filteredAssets } = useAssetBrowser(assets)
sortBy.value = '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 { sortBy, filteredAssets } = useAssetBrowser(assets)
sortBy.value = '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('Async Asset Selection with Detail Fetching', () => {
it('should fetch asset details and call onSelect with filename when provided', async () => {
const onSelectSpy = vi.fn()
const asset = createApiAsset({
id: 'asset-123',
name: 'test-model.safetensors'
})
const detailAsset = createApiAsset({
id: 'asset-123',
name: 'test-model.safetensors',
user_metadata: { filename: 'checkpoints/test-model.safetensors' }
})
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
const { selectAssetWithCallback } = useAssetBrowser([asset])
await selectAssetWithCallback(asset.id, onSelectSpy)
expect(assetService.getAssetDetails).toHaveBeenCalledWith('asset-123')
expect(onSelectSpy).toHaveBeenCalledWith(
'checkpoints/test-model.safetensors'
)
})
it('should handle missing user_metadata.filename as error', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {})
const onSelectSpy = vi.fn()
const asset = createApiAsset({ id: 'asset-456' })
const detailAsset = createApiAsset({
id: 'asset-456',
user_metadata: { filename: '' } // Invalid empty filename
})
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
const { selectAssetWithCallback } = useAssetBrowser([asset])
await selectAssetWithCallback(asset.id, onSelectSpy)
expect(assetService.getAssetDetails).toHaveBeenCalledWith('asset-456')
expect(onSelectSpy).not.toHaveBeenCalled()
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Invalid asset filename:',
expect.arrayContaining([
expect.objectContaining({
message: 'Filename cannot be empty'
})
]),
'for asset:',
'asset-456'
)
})
it('should handle API errors gracefully', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {})
const onSelectSpy = vi.fn()
const asset = createApiAsset({ id: 'asset-789' })
const apiError = new Error('API Error')
vi.mocked(assetService.getAssetDetails).mockRejectedValue(apiError)
const { selectAssetWithCallback } = useAssetBrowser([asset])
await selectAssetWithCallback(asset.id, onSelectSpy)
expect(assetService.getAssetDetails).toHaveBeenCalledWith('asset-789')
expect(onSelectSpy).not.toHaveBeenCalled()
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to fetch asset details for asset-789'),
apiError
)
})
it('should not fetch details when no callback provided', async () => {
const asset = createApiAsset({ id: 'asset-no-callback' })
const { selectAssetWithCallback } = useAssetBrowser([asset])
await selectAssetWithCallback(asset.id)
expect(assetService.getAssetDetails).not.toHaveBeenCalled()
})
})
describe('Filename Validation Security', () => {
const createValidationTest = (filename: string) => {
const testAsset = createApiAsset({ id: 'validation-test' })
const detailAsset = createApiAsset({
id: 'validation-test',
user_metadata: { filename }
})
return { testAsset, detailAsset }
}
it('accepts valid file paths with forward slashes', async () => {
const onSelectSpy = vi.fn()
const { testAsset, detailAsset } = createValidationTest(
'models/checkpoints/v1/test-model.safetensors'
)
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
const { selectAssetWithCallback } = useAssetBrowser([testAsset])
await selectAssetWithCallback(testAsset.id, onSelectSpy)
expect(onSelectSpy).toHaveBeenCalledWith(
'models/checkpoints/v1/test-model.safetensors'
)
})
it('rejects directory traversal attacks', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {})
const onSelectSpy = vi.fn()
const maliciousPaths = [
'../malicious-model.safetensors',
'models/../../../etc/passwd',
'/etc/passwd'
]
for (const path of maliciousPaths) {
const { testAsset, detailAsset } = createValidationTest(path)
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
const { selectAssetWithCallback } = useAssetBrowser([testAsset])
await selectAssetWithCallback(testAsset.id, onSelectSpy)
expect(onSelectSpy).not.toHaveBeenCalled()
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Invalid asset filename:',
expect.arrayContaining([
expect.objectContaining({
message: 'Path must not start with / or contain ..'
})
]),
'for asset:',
'validation-test'
)
}
})
it('rejects invalid filename characters', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {})
const onSelectSpy = vi.fn()
const invalidChars = ['\\', ':', '*', '?', '"', '<', '>', '|']
for (const char of invalidChars) {
const { testAsset, detailAsset } = createValidationTest(
`bad${char}filename.safetensors`
)
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
const { selectAssetWithCallback } = useAssetBrowser([testAsset])
await selectAssetWithCallback(testAsset.id, onSelectSpy)
expect(onSelectSpy).not.toHaveBeenCalled()
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Invalid asset filename:',
expect.arrayContaining([
expect.objectContaining({
message: 'Invalid filename characters'
})
]),
'for asset:',
'validation-test'
)
}
})
})
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: 'icon-[lucide--folder]' },
{
id: 'checkpoints',
label: 'Checkpoints',
icon: 'icon-[lucide--package]'
},
{ id: 'loras', label: 'Loras', icon: 'icon-[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: 'icon-[lucide--folder]' },
{ id: 'vae', label: 'Vae', icon: 'icon-[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: 'icon-[lucide--folder]' },
{
id: 'checkpoints',
label: 'Checkpoints',
icon: 'icon-[lucide--package]'
}
])
})
it('computes content title from selected category', () => {
const assets = [createApiAsset({ tags: ['models', 'checkpoints'] })]
const { selectedCategory, contentTitle } = useAssetBrowser(assets)
// Default
expect(contentTitle.value).toBe('All Models')
// Set specific category
selectedCategory.value = 'checkpoints'
expect(contentTitle.value).toBe('Checkpoints')
// Unknown category
selectedCategory.value = 'unknown'
expect(contentTitle.value).toBe('Assets')
})
})
})

View File

@@ -1,96 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
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([])
}
}))
// Test factory functions
interface AssetBrowserProps {
nodeType: string
inputName: string
onAssetSelected?: (filename: string) => void
}
function createAssetBrowserProps(
overrides: Partial<AssetBrowserProps> = {}
): AssetBrowserProps {
return {
nodeType: 'CheckpointLoaderSimple',
inputName: 'ckpt_name',
...overrides
}
}
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 assetBrowserDialog = useAssetBrowserDialog()
const onAssetSelected = vi.fn()
const props = createAssetBrowserProps({ onAssetSelected })
await assetBrowserDialog.show(props)
// 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')
// Should call the original callback and trigger hide animation
expect(onAssetSelected).toHaveBeenCalledWith('selected-asset-path')
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 assetBrowserDialog = useAssetBrowserDialog()
const props = createAssetBrowserProps()
await assetBrowserDialog.show(props)
// 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({
key: 'global-asset-browser'
})
})
})
})

View File

@@ -1,159 +0,0 @@
import { describe, expect, it } from 'vitest'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
// Test factory functions
function createTestAsset(overrides: Partial<AssetItem> = {}): AssetItem {
return {
id: 'test-uuid',
name: 'test-model.safetensors',
asset_hash: 'blake3:test123',
size: 123456,
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',
user_metadata: {
base_model: 'sd15'
},
...overrides
}
}
describe('useAssetFilterOptions', () => {
describe('File Format Extraction', () => {
it('extracts file formats from asset names', () => {
const assets = [
createTestAsset({ name: 'model1.safetensors' }),
createTestAsset({ name: 'model2.ckpt' }),
createTestAsset({ name: 'model3.pt' })
]
const { availableFileFormats } = useAssetFilterOptions(assets)
expect(availableFileFormats.value).toEqual([
{ name: '.ckpt', value: 'ckpt' },
{ name: '.pt', value: 'pt' },
{ name: '.safetensors', value: 'safetensors' }
])
})
it('handles duplicate file formats', () => {
const assets = [
createTestAsset({ name: 'model1.safetensors' }),
createTestAsset({ name: 'model2.safetensors' }),
createTestAsset({ name: 'model3.ckpt' })
]
const { availableFileFormats } = useAssetFilterOptions(assets)
expect(availableFileFormats.value).toEqual([
{ name: '.ckpt', value: 'ckpt' },
{ name: '.safetensors', value: 'safetensors' }
])
})
it('handles assets with no file extension', () => {
const assets = [
createTestAsset({ name: 'model_no_extension' }),
createTestAsset({ name: 'model.safetensors' })
]
const { availableFileFormats } = useAssetFilterOptions(assets)
expect(availableFileFormats.value).toEqual([
{ name: '.safetensors', value: 'safetensors' }
])
})
it('handles empty asset list', () => {
const { availableFileFormats } = useAssetFilterOptions([])
expect(availableFileFormats.value).toEqual([])
})
})
describe('Base Model Extraction', () => {
it('extracts base models from user metadata', () => {
const assets = [
createTestAsset({ user_metadata: { base_model: 'sd15' } }),
createTestAsset({ user_metadata: { base_model: 'sdxl' } }),
createTestAsset({ user_metadata: { base_model: 'sd35' } })
]
const { availableBaseModels } = useAssetFilterOptions(assets)
expect(availableBaseModels.value).toEqual([
{ name: 'sd15', value: 'sd15' },
{ name: 'sd35', value: 'sd35' },
{ name: 'sdxl', value: 'sdxl' }
])
})
it('handles duplicate base models', () => {
const assets = [
createTestAsset({ user_metadata: { base_model: 'sd15' } }),
createTestAsset({ user_metadata: { base_model: 'sd15' } }),
createTestAsset({ user_metadata: { base_model: 'sdxl' } })
]
const { availableBaseModels } = useAssetFilterOptions(assets)
expect(availableBaseModels.value).toEqual([
{ name: 'sd15', value: 'sd15' },
{ name: 'sdxl', value: 'sdxl' }
])
})
it('handles assets with missing user_metadata', () => {
const assets = [
createTestAsset({ user_metadata: undefined }),
createTestAsset({ user_metadata: { base_model: 'sd15' } })
]
const { availableBaseModels } = useAssetFilterOptions(assets)
expect(availableBaseModels.value).toEqual([
{ name: 'sd15', value: 'sd15' }
])
})
it('handles assets with missing base_model field', () => {
const assets = [
createTestAsset({ user_metadata: { description: 'A test model' } }),
createTestAsset({ user_metadata: { base_model: 'sdxl' } })
]
const { availableBaseModels } = useAssetFilterOptions(assets)
expect(availableBaseModels.value).toEqual([
{ name: 'sdxl', value: 'sdxl' }
])
})
it('handles empty asset list', () => {
const { availableBaseModels } = useAssetFilterOptions([])
expect(availableBaseModels.value).toEqual([])
})
})
describe('Reactivity', () => {
it('returns computed properties that can be reactive', () => {
const assets = [createTestAsset({ name: 'model.safetensors' })]
const { availableFileFormats, availableBaseModels } =
useAssetFilterOptions(assets)
// These should be computed refs
expect(availableFileFormats.value).toBeDefined()
expect(availableBaseModels.value).toBeDefined()
expect(typeof availableFileFormats.value).toBe('object')
expect(typeof availableBaseModels.value).toBe('object')
expect(Array.isArray(availableFileFormats.value)).toBe(true)
expect(Array.isArray(availableBaseModels.value)).toBe(true)
})
})
})

View File

@@ -1,6 +1,5 @@
import { createTestingPinia } from '@pinia/testing'
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { VueWrapper, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'

View File

@@ -4,8 +4,8 @@ import Button from 'primevue/button'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, ref } from 'vue'
import NodeConflictDialogContent from '@/components/dialog/content/manager/NodeConflictDialogContent.vue'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
// Mock getConflictMessage utility
vi.mock('@/utils/conflictMessageUtil', () => ({

View File

@@ -4,11 +4,8 @@ import Card from 'primevue/card'
import ProgressSpinner from 'primevue/progressspinner'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
import type {
MergedNodePack,
RegistryPack
} from '@/workbench/extensions/manager/types/comfyManagerTypes'
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import type { MergedNodePack, RegistryPack } from '@/types/comfyManagerTypes'
// Mock dependencies
vi.mock('vue-i18n', () => ({
@@ -24,7 +21,7 @@ vi.mock('vue-i18n', () => ({
}))
}))
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
isPackInstalled: vi.fn(() => false),
isPackEnabled: vi.fn(() => true),

View File

@@ -5,23 +5,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
import { useComfyManagerService } from '@/services/comfyManagerService'
import {
useComfyManagerStore,
useManagerProgressDialogStore
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { TaskLog } from '@/workbench/extensions/manager/types/comfyManagerTypes'
} from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { TaskLog } from '@/types/comfyManagerTypes'
// Mock modules
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore')
vi.mock('@/stores/comfyManagerStore')
vi.mock('@/stores/dialogStore')
vi.mock('@/platform/settings/settingStore')
vi.mock('@/stores/commandStore')
vi.mock('@/workbench/extensions/manager/services/comfyManagerService')
vi.mock('@/services/comfyManagerService')
vi.mock('@/composables/useConflictDetection', () => ({
useConflictDetection: vi.fn(() => ({
conflictedPackages: { value: [] },

View File

@@ -2,10 +2,10 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import {
LGraphEventMode,
type Positionable,
LGraphNode,
Positionable,
Reroute
} from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'

View File

@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
// Mock stores
vi.mock('@/renderer/core/canvas/canvasStore', () => {

View File

@@ -1894,159 +1894,4 @@ describe('useNodePricing', () => {
expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based')
})
})
describe('dynamic pricing - WanTextToVideoApi', () => {
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should return $0.50 for 5s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 5 },
{ name: 'size', value: '720p: 16:9 (1280x720)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.50/Run') // 0.10 * 5
})
it('should return $0.15 for 3s at 480p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '3' },
{ name: 'size', value: '480p: 1:1 (624x624)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.15/Run') // 0.05 * 3
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanTextToVideoApi', [])
const missingSize = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanTextToVideoApi', [
{ name: 'size', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'size', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '2K' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
describe('dynamic pricing - WanImageToVideoApi', () => {
it('should return $0.80 for 8s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 8 },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.80/Run') // 0.10 * 8
})
it('should return $0.60 for 12s at 480P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '12' },
{ name: 'resolution', value: '480P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.60/Run') // 0.05 * 12
})
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should handle "5s" string duration at 1080P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '1080P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.75/Run') // 0.15 * 5
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanImageToVideoApi', [])
const missingRes = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanImageToVideoApi', [
{ name: 'resolution', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: 'weird-res' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
})

View File

@@ -3,8 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual('vue-i18n')

View File

@@ -4,7 +4,7 @@ import { nextTick } from 'vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import type { components } from '@/types/comfyRegistryTypes'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
type InstalledPacksResponse =
ManagerComponents['schemas']['InstalledPacksResponse']
@@ -27,7 +27,7 @@ vi.mock('@/scripts/api', () => ({
}
}))
vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({
vi.mock('@/services/comfyManagerService', () => ({
useComfyManagerService: vi.fn()
}))
@@ -57,7 +57,7 @@ vi.mock('@/composables/nodePack/useInstalledPacks', () => ({
}))
}))
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
isPackInstalled: vi.fn(),
installedPacks: { value: [] }
@@ -140,7 +140,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
// Mock useComfyManagerService
const { useComfyManagerService } = await import(
'@/workbench/extensions/manager/services/comfyManagerService'
'@/services/comfyManagerService'
)
vi.mocked(useComfyManagerService).mockReturnValue(
mockComfyManagerService as any

View File

@@ -4,11 +4,11 @@ import { computed, ref } from 'vue'
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
import * as dialogService from '@/services/dialogService'
import * as comfyManagerStore from '@/stores/comfyManagerStore'
import * as conflictDetectionStore from '@/stores/conflictDetectionStore'
import * as comfyManagerStore from '@/workbench/extensions/manager/stores/comfyManagerStore'
// Mock the stores and services
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore')
vi.mock('@/stores/comfyManagerStore')
vi.mock('@/stores/conflictDetectionStore')
vi.mock('@/services/dialogService')
vi.mock('vue-i18n', async (importOriginal) => {

View File

@@ -1,8 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import { useManagerQueue } from '@/composables/useManagerQueue'
import { components } from '@/types/generatedManagerTypes'
// Mock dialog service
vi.mock('@/services/dialogService', () => ({

View File

@@ -2,13 +2,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
import { api } from '@/scripts/api'
import { useExtensionStore } from '@/stores/extensionStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import {
ManagerUIState,
useManagerState
} from '@/workbench/extensions/manager/composables/useManagerState'
// Mock dependencies
vi.mock('@/scripts/api', () => ({
@@ -59,10 +56,10 @@ describe('useManagerState', () => {
})
describe('managerUIState property', () => {
it('should return DISABLED state when --enable-manager is NOT present', () => {
it('should return DISABLED state when --disable-manager is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py'] } // No --enable-manager flag
system: { argv: ['python', 'main.py', '--disable-manager'] }
}),
isInitialized: ref(true)
} as any)
@@ -79,14 +76,7 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: {
argv: [
'python',
'main.py',
'--enable-manager',
'--enable-manager-legacy-ui'
]
} // Both flags needed
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
}),
isInitialized: ref(true)
} as any)
@@ -102,9 +92,7 @@ describe('useManagerState', () => {
it('should return NEW_UI state when client and server both support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -126,9 +114,7 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when server supports v4 but client does not', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -150,9 +136,7 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when legacy manager extension exists', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -171,9 +155,7 @@ describe('useManagerState', () => {
it('should return NEW_UI state when server feature flags are undefined', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -193,9 +175,7 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when server does not support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -232,17 +212,14 @@ describe('useManagerState', () => {
const managerState = useManagerState()
// When systemStats is null, we can't check for --enable-manager flag, so manager is disabled
expect(managerState.managerUIState.value).toBe(ManagerUIState.DISABLED)
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
})
})
describe('helper properties', () => {
it('isManagerEnabled should return true when state is not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -260,7 +237,7 @@ describe('useManagerState', () => {
it('isManagerEnabled should return false when state is DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py'] } // No --enable-manager flag means disabled
system: { argv: ['python', 'main.py', '--disable-manager'] }
}),
isInitialized: ref(true)
} as any)
@@ -275,9 +252,7 @@ describe('useManagerState', () => {
it('isNewManagerUI should return true when state is NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -295,14 +270,7 @@ describe('useManagerState', () => {
it('isLegacyManagerUI should return true when state is LEGACY_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: {
argv: [
'python',
'main.py',
'--enable-manager',
'--enable-manager-legacy-ui'
]
} // Both flags needed
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
}),
isInitialized: ref(true)
} as any)
@@ -317,9 +285,7 @@ describe('useManagerState', () => {
it('shouldShowInstallButton should return true only for NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -336,9 +302,7 @@ describe('useManagerState', () => {
it('shouldShowManagerButtons should return true when not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({

View File

@@ -5,9 +5,9 @@ import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
// Mock Vue's onMounted to execute immediately for testing
vi.mock('vue', async () => {
@@ -23,7 +23,7 @@ vi.mock('@/composables/nodePack/useWorkflowPacks', () => ({
useWorkflowPacks: vi.fn()
}))
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn()
}))

View File

@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useNodeChatHistory } from '@/composables/node/useNodeChatHistory'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
vi.mock(
'@/renderer/extensions/vueNodes/widgets/composables/useChatHistoryWidget',

View File

@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useServerLogs } from '@/composables/useServerLogs'
import type { LogsWsMessage } from '@/schemas/apiSchema'
import { LogsWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
vi.mock('@/scripts/api', () => ({

View File

@@ -1,10 +1,11 @@
import { compare, valid } from 'semver'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
// Import mocked utils
import { compareVersions, isSemVer } from '@/utils/formatUtil'
// Mock Vue's onMounted to execute immediately for testing
vi.mock('vue', async () => {
@@ -20,20 +21,20 @@ vi.mock('@/composables/nodePack/useInstalledPacks', () => ({
useInstalledPacks: vi.fn()
}))
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn()
}))
vi.mock('semver', () => ({
compare: vi.fn(),
valid: vi.fn()
vi.mock('@/utils/formatUtil', () => ({
compareVersions: vi.fn(),
isSemVer: vi.fn()
}))
const mockUseInstalledPacks = vi.mocked(useInstalledPacks)
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
const mockSemverCompare = vi.mocked(compare)
const mockSemverValid = vi.mocked(valid)
const mockCompareVersions = vi.mocked(compareVersions)
const mockIsSemVer = vi.mocked(isSemVer)
describe('useUpdateAvailableNodes', () => {
const mockInstalledPacks = [
@@ -85,19 +86,19 @@ describe('useUpdateAvailableNodes', () => {
}
})
mockSemverValid.mockImplementation((version) => {
return version &&
typeof version === 'string' &&
!version.includes('nightly')
? version
: null
})
mockIsSemVer.mockImplementation(
(version: string): version is `${number}.${number}.${number}` => {
return !version.includes('nightly')
}
)
mockSemverCompare.mockImplementation((latest, installed) => {
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
return 0
})
mockCompareVersions.mockImplementation(
(latest: string | undefined, installed: string | undefined) => {
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
return 0
}
)
mockUseComfyManagerStore.mockReturnValue({
isPackInstalled: mockIsPackInstalled,
@@ -321,10 +322,10 @@ describe('useUpdateAvailableNodes', () => {
// Access the computed to trigger the logic
expect(updateAvailableNodePacks.value).toBeDefined()
expect(mockSemverCompare).toHaveBeenCalledWith('2.0.0', '1.0.0')
expect(mockCompareVersions).toHaveBeenCalledWith('2.0.0', '1.0.0')
})
it('calls semver.valid to check nightly versions', () => {
it('calls isSemVer to check nightly versions', () => {
mockUseInstalledPacks.mockReturnValue({
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
isLoading: ref(false),
@@ -337,7 +338,7 @@ describe('useUpdateAvailableNodes', () => {
// Access the computed to trigger the logic
expect(updateAvailableNodePacks.value).toBeDefined()
expect(mockSemverValid).toHaveBeenCalledWith('nightly-abc123')
expect(mockIsSemVer).toHaveBeenCalledWith('nightly-abc123')
})
it('calls isPackInstalled for each pack', () => {

View File

@@ -5,7 +5,7 @@ import { LinkConnector } from '@/lib/litegraph/src/litegraph'
import { MovingOutputLink } from '@/lib/litegraph/src/litegraph'
import { ToOutputRenderLink } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
import type { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import { createTestSubgraph } from '../subgraph/fixtures/subgraphHelpers'

View File

@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
describe('LGraphNode Title Buttons', () => {

View File

@@ -1,12 +1,13 @@
// TODO: Fix these tests after migration
import { afterEach, describe, expect, vi } from 'vitest'
import type { LGraph, Reroute } from '@/lib/litegraph/src/litegraph'
import {
type CanvasPointerEvent,
LGraph,
LGraphNode,
LLink,
LinkConnector,
Reroute,
type RerouteId
} from '@/lib/litegraph/src/litegraph'

View File

@@ -1,9 +1,6 @@
import { describe, expect, it } from 'vitest'
import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/litegraph'
import { INodeInputSlot, INodeOutputSlot } from '@/lib/litegraph/src/litegraph'
import {
inputAsSerialisable,
outputAsSerialisable

View File

@@ -155,11 +155,9 @@ LiteGraphGlobal {
"do_add_triggers_slots": false,
"highlight_selected_group": true,
"isInsideRectangle": [Function],
"leftMouseClickBehavior": "panning",
"macGesturesRequireMac": true,
"macTrackpadGestures": false,
"middle_click_slot_add_default_node": false,
"mouseWheelScroll": "panning",
"node_box_coloured_by_mode": false,
"node_box_coloured_when_on": false,
"node_images_path": "",

View File

@@ -1,9 +1,9 @@
// TODO: Fix these tests after migration
import { assert, describe, expect, it } from 'vitest'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import {
type ISlotType,
ISlotType,
LGraph,
LGraphGroup,
LGraphNode,
LiteGraph

View File

@@ -2,7 +2,7 @@
import { describe, expect, it, vi } from 'vitest'
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import {
createTestSubgraph,

View File

@@ -5,8 +5,8 @@ import { LinkConnector } from '@/lib/litegraph/src/litegraph'
import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/litegraph'
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, type LinkNetwork } from '@/lib/litegraph/src/litegraph'
import type { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import type { NodeOutputSlot } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import { NodeOutputSlot } from '@/lib/litegraph/src/litegraph'
import {
isSubgraphInput,
isSubgraphOutput

View File

@@ -1,8 +1,8 @@
// TODO: Fix these tests after migration
import { describe, expect, it } from 'vitest'
import type { ISlotType, Subgraph } from '@/lib/litegraph/src/litegraph'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { ISlotType } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
import type { TWidgetType } from '@/lib/litegraph/src/litegraph'
import { BaseWidget } from '@/lib/litegraph/src/litegraph'

View File

@@ -5,9 +5,8 @@
* in their test files. Each fixture provides a clean, pre-configured subgraph
* setup for different testing scenarios.
*/
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { test } from '../../core/fixtures/testExtensions'
import {

View File

@@ -1,174 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { type ComputedRef, computed } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { NodeLayout } from '@/renderer/core/layout/types'
import { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'
// Mock layoutStore
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
layoutStore: {
getAllNodes: vi.fn()
}
}))
// Helper to create mock links that satisfy LGraph['links'] type
function createMockLinks(): LGraph['links'] {
const map = new Map<number, LLink>()
return Object.assign(map, {}) as LGraph['links']
}
describe('MinimapDataSource', () => {
describe('MinimapDataSourceFactory', () => {
it('should create LayoutStoreDataSource when LayoutStore has data', () => {
// Arrange
const mockNodes = new Map<string, NodeLayout>([
[
'node1',
{
id: 'node1',
position: { x: 0, y: 0 },
size: { width: 100, height: 50 },
zIndex: 0,
visible: true,
bounds: { x: 0, y: 0, width: 100, height: 50 }
}
]
])
// Create a computed ref that returns the map
const computedNodes: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => mockNodes)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedNodes)
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
// Assert
expect(dataSource).toBeDefined()
expect(dataSource.hasData()).toBe(true)
expect(dataSource.getNodeCount()).toBe(1)
})
it('should create LiteGraphDataSource when LayoutStore is empty', () => {
// Arrange
const emptyMap = new Map<string, NodeLayout>()
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => emptyMap)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
const mockNode: Pick<
LGraphNode,
'id' | 'pos' | 'size' | 'bgcolor' | 'mode' | 'has_errors' | 'outputs'
> = {
id: 'node1' as NodeId,
pos: [0, 0],
size: [100, 50],
bgcolor: '#fff',
mode: 0,
has_errors: false,
outputs: []
}
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [mockNode as LGraphNode],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
// Assert
expect(dataSource).toBeDefined()
expect(dataSource.hasData()).toBe(true)
expect(dataSource.getNodeCount()).toBe(1)
const nodes = dataSource.getNodes()
expect(nodes).toHaveLength(1)
expect(nodes[0]).toMatchObject({
id: 'node1',
x: 0,
y: 0,
width: 100,
height: 50
})
})
it('should handle empty graph correctly', () => {
// Arrange
const emptyMap = new Map<string, NodeLayout>()
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => emptyMap)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
// Assert
expect(dataSource.hasData()).toBe(false)
expect(dataSource.getNodeCount()).toBe(0)
expect(dataSource.getNodes()).toEqual([])
expect(dataSource.getLinks()).toEqual([])
expect(dataSource.getGroups()).toEqual([])
})
})
describe('Bounds calculation', () => {
it('should calculate correct bounds from nodes', () => {
// Arrange
const emptyMap = new Map<string, NodeLayout>()
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => emptyMap)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
const mockNode1: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
id: 'node1' as NodeId,
pos: [0, 0],
size: [100, 50],
outputs: []
}
const mockNode2: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
id: 'node2' as NodeId,
pos: [200, 100],
size: [150, 75],
outputs: []
}
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [mockNode1 as LGraphNode, mockNode2 as LGraphNode],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
const bounds = dataSource.getBounds()
// Assert
expect(bounds).toEqual({
minX: 0,
minY: 0,
maxX: 350,
maxY: 175,
width: 350,
height: 175
})
})
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { NodeSearchService } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'

View File

@@ -14,7 +14,7 @@ const createMockCanvasContext = () => ({
const isCI = Boolean(process.env.CI)
const describeIfNotCI = isCI ? describe.skip : describe
describeIfNotCI.skip('Transform Performance', () => {
describeIfNotCI('Transform Performance', () => {
let transformState: ReturnType<typeof useTransformState>
let mockCanvas: any

View File

@@ -1,65 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import {
getAssetBaseModel,
getAssetDescription
} from '@/platform/assets/utils/assetMetadataUtils'
describe('assetMetadataUtils', () => {
const mockAsset: AssetItem = {
id: 'test-id',
name: 'test-model',
asset_hash: 'hash123',
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'
}
describe('getAssetDescription', () => {
it('should return string description when present', () => {
const asset = {
...mockAsset,
user_metadata: { description: 'A test model' }
}
expect(getAssetDescription(asset)).toBe('A test model')
})
it('should return null when description is not a string', () => {
const asset = {
...mockAsset,
user_metadata: { description: 123 }
}
expect(getAssetDescription(asset)).toBeNull()
})
it('should return null when no metadata', () => {
expect(getAssetDescription(mockAsset)).toBeNull()
})
})
describe('getAssetBaseModel', () => {
it('should return string base_model when present', () => {
const asset = {
...mockAsset,
user_metadata: { base_model: 'SDXL' }
}
expect(getAssetBaseModel(asset)).toBe('SDXL')
})
it('should return null when base_model is not a string', () => {
const asset = {
...mockAsset,
user_metadata: { base_model: 123 }
}
expect(getAssetBaseModel(asset)).toBeNull()
})
it('should return null when no metadata', () => {
expect(getAssetBaseModel(mockAsset)).toBeNull()
})
})
})

View File

@@ -1,38 +1,14 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, toValue } from 'vue'
import type { ComponentProps } from 'vue-component-type-helpers'
import { computed, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
const mockData = vi.hoisted(() => ({
mockNodeIds: new Set<string>(),
mockExecuting: false
}))
vi.mock('@/renderer/core/canvas/canvasStore', () => {
const getCanvas = vi.fn()
const useCanvasStore = () => ({
getCanvas,
selectedNodeIds: computed(() => mockData.mockNodeIds)
})
return {
useCanvasStore
}
})
vi.mock(
'@/renderer/extensions/vueNodes/composables/useNodeEventHandlers',
() => {
const handleNodeSelect = vi.fn()
return { useNodeEventHandlers: () => ({ handleNodeSelect }) }
}
)
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
vi.mock(
'@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking',
@@ -50,7 +26,6 @@ vi.mock('@/composables/useErrorHandling', () => ({
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
useNodeLayout: () => ({
position: { x: 100, y: 50 },
size: { width: 200, height: 100 },
startDrag: vi.fn(),
handleDrag: vi.fn(),
endDrag: vi.fn()
@@ -72,7 +47,7 @@ vi.mock(
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
() => ({
useNodeExecutionState: vi.fn(() => ({
executing: computed(() => mockData.mockExecuting),
executing: computed(() => false),
progress: computed(() => undefined),
progressPercentage: computed(() => undefined),
progressState: computed(() => undefined as any),
@@ -81,13 +56,6 @@ vi.mock(
})
)
vi.mock('@/renderer/extensions/vueNodes/preview/useNodePreviewState', () => ({
useNodePreviewState: vi.fn(() => ({
latestPreviewUrl: computed(() => ''),
shouldShowPreviewImg: computed(() => false)
}))
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
@@ -97,56 +65,61 @@ const i18n = createI18n({
}
}
})
function mountLGraphNode(props: ComponentProps<typeof LGraphNode>) {
return mount(LGraphNode, {
props,
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn
}),
i18n
],
stubs: {
NodeHeader: true,
NodeSlots: true,
NodeWidgets: true,
NodeContent: true,
SlotConnectionDot: true
}
}
})
}
const mockNodeData: VueNodeData = {
id: 'test-node-123',
title: 'Test Node',
type: 'TestNode',
mode: 0,
flags: {},
inputs: [],
outputs: [],
widgets: [],
selected: false,
executing: false
}
describe('LGraphNode', () => {
const mockNodeData: VueNodeData = {
id: 'test-node-123',
title: 'Test Node',
type: 'TestNode',
mode: 0,
flags: {},
inputs: [],
outputs: [],
widgets: [],
selected: false,
executing: false
}
const mountLGraphNode = (props: any, selectedNodeIds = new Set()) => {
return mount(LGraphNode, {
props,
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn
}),
i18n
],
provide: {
[SelectedNodeIdsKey as symbol]: ref(selectedNodeIds)
},
stubs: {
NodeHeader: true,
NodeSlots: true,
NodeWidgets: true,
NodeContent: true,
SlotConnectionDot: true
}
}
})
}
beforeEach(() => {
vi.resetAllMocks()
mockData.mockNodeIds = new Set()
mockData.mockExecuting = false
vi.clearAllMocks()
// Reset to default mock
vi.mocked(useNodeExecutionState).mockReturnValue({
executing: computed(() => false),
progress: computed(() => undefined),
progressPercentage: computed(() => undefined),
progressState: computed(() => undefined as any),
executionState: computed(() => 'idle' as const)
})
})
it('should call resize tracking composable with node ID', () => {
mountLGraphNode({ nodeData: mockNodeData })
expect(useVueElementTracking).toHaveBeenCalledWith(
expect.any(Function),
'node'
)
const idArg = vi.mocked(useVueElementTracking).mock.calls[0]?.[0]
const id = toValue(idArg)
expect(id).toEqual('test-node-123')
expect(useVueElementTracking).toHaveBeenCalledWith('test-node-123', 'node')
})
it('should render with data-node-id attribute', () => {
@@ -166,6 +139,9 @@ describe('LGraphNode', () => {
}),
i18n
],
provide: {
[SelectedNodeIdsKey as symbol]: ref(new Set())
},
stubs: {
NodeSlots: true,
NodeWidgets: true,
@@ -179,15 +155,24 @@ describe('LGraphNode', () => {
})
it('should apply selected styling when selected prop is true', () => {
mockData.mockNodeIds = new Set(['test-node-123'])
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
const wrapper = mountLGraphNode(
{ nodeData: mockNodeData, selected: true },
new Set(['test-node-123'])
)
expect(wrapper.classes()).toContain('outline-2')
expect(wrapper.classes()).toContain('outline-black')
expect(wrapper.classes()).toContain('dark-theme:outline-white')
})
it('should apply executing animation when executing prop is true', () => {
mockData.mockExecuting = true
// Mock the execution state to return executing: true
vi.mocked(useNodeExecutionState).mockReturnValue({
executing: computed(() => true),
progress: computed(() => undefined),
progressPercentage: computed(() => undefined),
progressState: computed(() => undefined as any),
executionState: computed(() => 'running' as const)
})
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
@@ -195,16 +180,12 @@ describe('LGraphNode', () => {
})
it('should emit node-click event on pointer up', async () => {
const { handleNodeSelect } = useNodeEventHandlers()
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
await wrapper.trigger('pointerup')
expect(handleNodeSelect).toHaveBeenCalledOnce()
expect(handleNodeSelect).toHaveBeenCalledWith(
expect.any(PointerEvent),
mockNodeData,
expect.any(Boolean)
)
expect(wrapper.emitted('node-click')).toHaveLength(1)
expect(wrapper.emitted('node-click')?.[0]).toHaveLength(3)
expect(wrapper.emitted('node-click')?.[0][1]).toEqual(mockNodeData)
})
})

View File

@@ -1,223 +0,0 @@
/**
* Tests for NodeHeader subgraph functionality
*/
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
// Mock dependencies
vi.mock('@/scripts/app', () => ({
app: {
graph: null as any
}
}))
vi.mock('@/utils/graphTraversalUtil', () => ({
getNodeByLocatorId: vi.fn(),
getLocatorIdFromNodeData: vi.fn((nodeData) =>
nodeData.subgraphId
? `${nodeData.subgraphId}:${String(nodeData.id)}`
: String(nodeData.id)
)
}))
vi.mock('@/composables/useErrorHandling', () => ({
useErrorHandling: () => ({
toastErrorHandler: vi.fn()
})
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: vi.fn((key) => key)
}),
createI18n: vi.fn(() => ({
global: {
t: vi.fn((key) => key)
}
}))
}))
vi.mock('@/i18n', () => ({
st: vi.fn((key) => key),
t: vi.fn((key) => key),
i18n: {
global: {
t: vi.fn((key) => key)
}
}
}))
describe('NodeHeader - Subgraph Functionality', () => {
// Helper to setup common mocks
const setupMocks = async (isSubgraph = true, hasGraph = true) => {
const { app } = await import('@/scripts/app')
if (hasGraph) {
;(app as any).graph = { rootGraph: {} }
} else {
;(app as any).graph = null
}
vi.mocked(getNodeByLocatorId).mockReturnValue({
isSubgraphNode: () => isSubgraph
} as any)
}
beforeEach(() => {
vi.clearAllMocks()
})
const createMockNodeData = (
id: string,
subgraphId?: string
): VueNodeData => ({
id,
title: 'Test Node',
type: 'TestNode',
mode: 0,
selected: false,
executing: false,
subgraphId,
widgets: [],
inputs: [],
outputs: [],
hasErrors: false,
flags: {}
})
const createWrapper = (props = {}) => {
return mount(NodeHeader, {
props,
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })],
mocks: {
$t: vi.fn((key: string) => key),
$primevue: { config: {} }
}
}
})
}
it('should show subgraph button for subgraph nodes', async () => {
await setupMocks(true) // isSubgraph = true
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})
await wrapper.vm.$nextTick()
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
expect(subgraphButton.exists()).toBe(true)
})
it('should not show subgraph button for regular nodes', async () => {
await setupMocks(false) // isSubgraph = false
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})
await wrapper.vm.$nextTick()
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
expect(subgraphButton.exists()).toBe(false)
})
it('should not show subgraph button in readonly mode', async () => {
await setupMocks(true) // isSubgraph = true
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: true
})
await wrapper.vm.$nextTick()
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
expect(subgraphButton.exists()).toBe(false)
})
it('should emit enter-subgraph event when button is clicked', async () => {
await setupMocks(true) // isSubgraph = true
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})
await wrapper.vm.$nextTick()
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
await subgraphButton.trigger('click')
expect(wrapper.emitted('enter-subgraph')).toBeTruthy()
expect(wrapper.emitted('enter-subgraph')).toHaveLength(1)
})
it('should handle subgraph context correctly', async () => {
await setupMocks(true) // isSubgraph = true
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1', 'subgraph-id'),
readonly: false
})
await wrapper.vm.$nextTick()
// Should call getNodeByLocatorId with correct locator ID
expect(vi.mocked(getNodeByLocatorId)).toHaveBeenCalledWith(
expect.anything(),
'subgraph-id:test-node-1'
)
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
expect(subgraphButton.exists()).toBe(true)
})
it('should handle missing graph gracefully', async () => {
await setupMocks(true, false) // isSubgraph = true, hasGraph = false
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})
await wrapper.vm.$nextTick()
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
expect(subgraphButton.exists()).toBe(false)
})
it('should prevent event propagation on double click', async () => {
await setupMocks(true) // isSubgraph = true
const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})
await wrapper.vm.$nextTick()
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
// Mock event object
const mockEvent = {
stopPropagation: vi.fn()
}
// Trigger dblclick event
await subgraphButton.trigger('dblclick', mockEvent)
// Should prevent propagation (handled by @dblclick.stop directive)
// This is tested by ensuring the component doesn't error and renders correctly
expect(subgraphButton.exists()).toBe(true)
})
})

View File

@@ -1,82 +1,79 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, shallowRef } from 'vue'
import { ref } from 'vue'
import {
type GraphNodeManager,
type VueNodeData,
useGraphNodeManager
} from '@/composables/graph/useGraphNodeManager'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import type {
LGraph,
LGraphCanvas,
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
vi.mock('@/renderer/core/canvas/canvasStore', () => {
const canvas: Partial<LGraphCanvas> = {
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: vi.fn()
}))
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({
useLayoutMutations: vi.fn()
}))
function createMockCanvas(): Pick<
LGraphCanvas,
'select' | 'deselect' | 'deselectAll'
> {
return {
select: vi.fn(),
deselect: vi.fn(),
deselectAll: vi.fn()
}
const updateSelectedItems = vi.fn()
}
function createMockNode(): Pick<LGraphNode, 'id' | 'selected' | 'flags'> {
return {
useCanvasStore: vi.fn(() => ({
canvas: canvas as LGraphCanvas,
updateSelectedItems,
selectedItems: []
}))
}
})
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
useCanvasInteractions: vi.fn(() => ({
shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events
}))
}))
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => {
const setSource = vi.fn()
const bringNodeToFront = vi.fn()
return {
useLayoutMutations: vi.fn(() => ({
setSource,
bringNodeToFront
}))
}
})
vi.mock('@/composables/graph/useGraphNodeManager', () => {
const mockNode = {
id: 'node-1',
selected: false,
flags: { pinned: false }
}
const nodeManager = shallowRef({
getNode: vi.fn(() => mockNode as Partial<LGraphNode> as LGraphNode)
} as Partial<GraphNodeManager> as GraphNodeManager)
return {
useGraphNodeManager: vi.fn(() => nodeManager)
}
})
}
vi.mock('@/composables/graph/useVueNodeLifecycle', () => {
const nodeManager = useGraphNodeManager(undefined as unknown as LGraph)
function createMockNodeManager(
node: Pick<LGraphNode, 'id' | 'selected' | 'flags'>
) {
return {
useVueNodeLifecycle: vi.fn(() => ({
nodeManager
}))
getNode: vi.fn().mockReturnValue(node) as ReturnType<
typeof useGraphNodeManager
>['getNode']
}
})
}
function createMockCanvasStore(
canvas: Pick<LGraphCanvas, 'select' | 'deselect' | 'deselectAll'>
): Pick<
ReturnType<typeof useCanvasStore>,
'canvas' | 'selectedItems' | 'updateSelectedItems'
> {
return {
canvas: canvas as LGraphCanvas,
selectedItems: [],
updateSelectedItems: vi.fn()
}
}
function createMockLayoutMutations(): Pick<
ReturnType<typeof useLayoutMutations>,
'setSource' | 'bringNodeToFront'
> {
return {
setSource: vi.fn(),
bringNodeToFront: vi.fn()
}
}
describe('useNodeEventHandlers', () => {
const { nodeManager: mockNodeManager } = useVueNodeLifecycle()
const mockNode = mockNodeManager.value!.getNode('fake_id')
const mockLayoutMutations = useLayoutMutations()
let mockCanvas: ReturnType<typeof createMockCanvas>
let mockNode: ReturnType<typeof createMockNode>
let mockNodeManager: ReturnType<typeof createMockNodeManager>
let mockCanvasStore: ReturnType<typeof createMockCanvasStore>
let mockLayoutMutations: ReturnType<typeof createMockLayoutMutations>
const testNodeData: VueNodeData = {
id: 'node-1',
@@ -88,13 +85,24 @@ describe('useNodeEventHandlers', () => {
}
beforeEach(async () => {
vi.restoreAllMocks()
mockNode = createMockNode()
mockCanvas = createMockCanvas()
mockNodeManager = createMockNodeManager(mockNode)
mockCanvasStore = createMockCanvasStore(mockCanvas)
mockLayoutMutations = createMockLayoutMutations()
vi.mocked(useCanvasStore).mockReturnValue(
mockCanvasStore as ReturnType<typeof useCanvasStore>
)
vi.mocked(useLayoutMutations).mockReturnValue(
mockLayoutMutations as ReturnType<typeof useLayoutMutations>
)
})
describe('handleNodeSelect', () => {
it('should select single node on regular click', () => {
const { handleNodeSelect } = useNodeEventHandlers()
const { canvas, updateSelectedItems } = useCanvasStore()
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
const event = new PointerEvent('pointerdown', {
bubbles: true,
@@ -104,17 +112,17 @@ describe('useNodeEventHandlers', () => {
handleNodeSelect(event, testNodeData, false)
expect(canvas?.deselectAll).toHaveBeenCalledOnce()
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
expect(updateSelectedItems).toHaveBeenCalledOnce()
expect(mockCanvas.deselectAll).toHaveBeenCalledOnce()
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
expect(mockCanvasStore.updateSelectedItems).toHaveBeenCalledOnce()
})
it('should toggle selection on ctrl+click', () => {
const { handleNodeSelect } = useNodeEventHandlers()
const { canvas } = useCanvasStore()
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
// Test selecting unselected node with ctrl
mockNode!.selected = false
mockNode.selected = false
const ctrlClickEvent = new PointerEvent('pointerdown', {
bubbles: true,
@@ -124,16 +132,16 @@ describe('useNodeEventHandlers', () => {
handleNodeSelect(ctrlClickEvent, testNodeData, false)
expect(canvas?.deselectAll).not.toHaveBeenCalled()
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
})
it('should deselect on ctrl+click of selected node', () => {
const { handleNodeSelect } = useNodeEventHandlers()
const { canvas } = useCanvasStore()
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
// Test deselecting selected node with ctrl
mockNode!.selected = true
mockNode.selected = true
const ctrlClickEvent = new PointerEvent('pointerdown', {
bubbles: true,
@@ -143,15 +151,15 @@ describe('useNodeEventHandlers', () => {
handleNodeSelect(ctrlClickEvent, testNodeData, false)
expect(canvas?.deselect).toHaveBeenCalledWith(mockNode)
expect(canvas?.select).not.toHaveBeenCalled()
expect(mockCanvas.deselect).toHaveBeenCalledWith(mockNode)
expect(mockCanvas.select).not.toHaveBeenCalled()
})
it('should handle meta key (Cmd) on Mac', () => {
const { handleNodeSelect } = useNodeEventHandlers()
const { canvas } = useCanvasStore()
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
mockNode!.selected = false
mockNode.selected = false
const metaClickEvent = new PointerEvent('pointerdown', {
bubbles: true,
@@ -161,14 +169,15 @@ describe('useNodeEventHandlers', () => {
handleNodeSelect(metaClickEvent, testNodeData, false)
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
expect(canvas?.deselectAll).not.toHaveBeenCalled()
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
})
it('should bring node to front when not pinned', () => {
const { handleNodeSelect } = useNodeEventHandlers()
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
mockNode!.flags.pinned = false
mockNode.flags.pinned = false
const event = new PointerEvent('pointerdown')
handleNodeSelect(event, testNodeData, false)
@@ -179,14 +188,49 @@ describe('useNodeEventHandlers', () => {
})
it('should not bring pinned node to front', () => {
const { handleNodeSelect } = useNodeEventHandlers()
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
mockNode!.flags.pinned = true
mockNode.flags.pinned = true
const event = new PointerEvent('pointerdown')
handleNodeSelect(event, testNodeData, false)
expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled()
})
it('should handle missing canvas gracefully', () => {
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
mockCanvasStore.canvas = null
const event = new PointerEvent('pointerdown')
expect(() => {
handleNodeSelect(event, testNodeData, false)
}).not.toThrow()
expect(mockCanvas.select).not.toHaveBeenCalled()
})
it('should handle missing node gracefully', () => {
const nodeManager = ref(mockNodeManager)
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
vi.mocked(mockNodeManager.getNode).mockReturnValue(undefined)
const event = new PointerEvent('pointerdown')
const nodeData = {
id: 'missing-node',
title: 'Missing Node',
type: 'test'
} as any
expect(() => {
handleNodeSelect(event, nodeData, false)
}).not.toThrow()
expect(mockCanvas.select).not.toHaveBeenCalled()
})
})
})

View File

@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import { assetService } from '@/platform/assets/services/assetService'
import { useComboWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useComboWidget'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -30,25 +29,13 @@ vi.mock('@/platform/assets/services/assetService', () => ({
}
}))
vi.mock('@/platform/assets/composables/useAssetBrowserDialog', () => {
const mockAssetBrowserDialogShow = vi.fn()
return {
useAssetBrowserDialog: vi.fn(() => ({
show: mockAssetBrowserDialogShow
}))
}
})
// Test factory functions
function createMockWidget(overrides: Partial<IBaseWidget> = {}): IBaseWidget {
const mockCallback = vi.fn()
return {
type: 'combo',
options: {},
name: 'testWidget',
value: undefined,
callback: mockCallback,
y: 0,
...overrides
} as IBaseWidget
}
@@ -58,16 +45,7 @@ function createMockNode(comfyClass = 'TestNode'): LGraphNode {
node.comfyClass = comfyClass
// Spy on the addWidget method
vi.spyOn(node, 'addWidget').mockImplementation(
(type, name, value, callback) => {
const widget = createMockWidget({ type, name, value })
// Store the callback function on the widget for testing
if (typeof callback === 'function') {
widget.callback = callback
}
return widget
}
)
vi.spyOn(node, 'addWidget').mockReturnValue(createMockWidget())
return node
}
@@ -83,9 +61,9 @@ function createMockInputSpec(overrides: Partial<InputSpec> = {}): InputSpec {
describe('useComboWidget', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset to defaults
mockSettingStoreGet.mockReturnValue(false)
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(false)
vi.mocked(useAssetBrowserDialog).mockClear()
})
it('should handle undefined spec', () => {

View File

@@ -2,7 +2,7 @@ import axios from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useRemoteWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget'
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
vi.mock('axios', () => {
return {

View File

@@ -1,8 +1,10 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import {
ComfyWorkflow,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
vi.mock('@/renderer/core/thumbnail/graphThumbnailRenderer', () => ({
createGraphThumbnail: vi.fn()

View File

@@ -2,7 +2,7 @@ import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/brow
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
// Mock global Algolia constants

View File

@@ -1,11 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { api } from '@/scripts/api'
const mockGetCategoryForNodeType = vi.fn()
vi.mock('@/stores/modelToNodeStore', () => ({
useModelToNodeStore: vi.fn(() => ({
getRegisteredNodeTypes: vi.fn(
@@ -16,49 +13,30 @@ vi.mock('@/stores/modelToNodeStore', () => ({
'VAELoader',
'TestNode'
])
),
getCategoryForNodeType: mockGetCategoryForNodeType,
modelToNodeMap: {
checkpoints: [{ nodeDef: { name: 'CheckpointLoaderSimple' } }],
loras: [{ nodeDef: { name: 'LoraLoader' } }],
vae: [{ nodeDef: { name: 'VAELoader' } }]
}
)
}))
}))
// Helper to create API-compliant test assets
function createTestAsset(overrides: Partial<AssetItem> = {}) {
return {
id: 'test-uuid',
name: 'test-model.safetensors',
asset_hash: 'blake3:test123',
size: 123456,
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
}
}
// Test data constants
const MOCK_ASSETS = {
checkpoints: createTestAsset({
checkpoints: {
id: 'uuid-1',
name: 'model1.safetensors',
tags: ['models', 'checkpoints']
}),
loras: createTestAsset({
tags: ['models', 'checkpoints'],
size: 123456
},
loras: {
id: 'uuid-2',
name: 'model2.safetensors',
tags: ['models', 'loras']
}),
vae: createTestAsset({
tags: ['models', 'loras'],
size: 654321
},
vae: {
id: 'uuid-3',
name: 'vae1.safetensors',
tags: ['models', 'vae']
})
tags: ['models', 'vae'],
size: 789012
}
} as const
// Helper functions
@@ -88,21 +66,24 @@ describe('assetService', () => {
describe('getAssetModelFolders', () => {
it('should extract directory names from asset tags and filter blacklisted ones', async () => {
const assets = [
createTestAsset({
{
id: 'uuid-1',
name: 'checkpoint1.safetensors',
tags: ['models', 'checkpoints']
}),
createTestAsset({
tags: ['models', 'checkpoints'],
size: 123456
},
{
id: 'uuid-2',
name: 'config.yaml',
tags: ['models', 'configs'] // Blacklisted
}),
createTestAsset({
tags: ['models', 'configs'], // Blacklisted
size: 654321
},
{
id: 'uuid-3',
name: 'vae1.safetensors',
tags: ['models', 'vae']
})
tags: ['models', 'vae'],
size: 789012
}
]
mockApiResponse(assets)
@@ -142,11 +123,12 @@ describe('assetService', () => {
const assets = [
{ ...MOCK_ASSETS.checkpoints, name: 'valid.safetensors' },
{ ...MOCK_ASSETS.loras, name: 'lora.safetensors' }, // Wrong tag
createTestAsset({
{
id: 'uuid-4',
name: 'missing-model.safetensors',
tags: ['models', 'checkpoints', 'missing'] // Has missing tag
})
tags: ['models', 'checkpoints', 'missing'], // Has missing tag
size: 654321
}
]
mockApiResponse(assets)
@@ -218,87 +200,4 @@ describe('assetService', () => {
).toBe(false)
})
})
describe('getAssetsForNodeType', () => {
beforeEach(() => {
mockGetCategoryForNodeType.mockClear()
})
it('should return empty array for unregistered node types', async () => {
mockGetCategoryForNodeType.mockReturnValue(undefined)
const result = await assetService.getAssetsForNodeType('UnknownNode')
expect(mockGetCategoryForNodeType).toHaveBeenCalledWith('UnknownNode')
expect(result).toEqual([])
})
it('should use getCategoryForNodeType for efficient category lookup', async () => {
mockGetCategoryForNodeType.mockReturnValue('checkpoints')
const testAssets = [MOCK_ASSETS.checkpoints]
mockApiResponse(testAssets)
const result = await assetService.getAssetsForNodeType(
'CheckpointLoaderSimple'
)
expect(mockGetCategoryForNodeType).toHaveBeenCalledWith(
'CheckpointLoaderSimple'
)
expect(result).toEqual(testAssets)
// Verify API call includes correct category
expect(api.fetchApi).toHaveBeenCalledWith(
'/assets?include_tags=models,checkpoints'
)
})
it('should return empty array when no category found', async () => {
mockGetCategoryForNodeType.mockReturnValue(undefined)
const result = await assetService.getAssetsForNodeType('TestNode')
expect(result).toEqual([])
expect(api.fetchApi).not.toHaveBeenCalled()
})
it('should handle API errors gracefully', async () => {
mockGetCategoryForNodeType.mockReturnValue('loras')
mockApiError(500, 'Internal Server Error')
await expect(
assetService.getAssetsForNodeType('LoraLoader')
).rejects.toThrow(
'Unable to load assets for LoraLoader: Server returned 500. Please try again.'
)
})
it('should return all assets without filtering for different categories', async () => {
// Test checkpoints
mockGetCategoryForNodeType.mockReturnValue('checkpoints')
const checkpointAssets = [MOCK_ASSETS.checkpoints]
mockApiResponse(checkpointAssets)
let result = await assetService.getAssetsForNodeType(
'CheckpointLoaderSimple'
)
expect(result).toEqual(checkpointAssets)
// Test loras
mockGetCategoryForNodeType.mockReturnValue('loras')
const loraAssets = [MOCK_ASSETS.loras]
mockApiResponse(loraAssets)
result = await assetService.getAssetsForNodeType('LoraLoader')
expect(result).toEqual(loraAssets)
// Test vae
mockGetCategoryForNodeType.mockReturnValue('vae')
const vaeAssets = [MOCK_ASSETS.vae]
mockApiResponse(vaeAssets)
result = await assetService.getAssetsForNodeType('VAELoader')
expect(result).toEqual(vaeAssets)
})
})
})

View File

@@ -2,9 +2,9 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type InstalledPacksResponse =
ManagerComponents['schemas']['InstalledPacksResponse']
@@ -13,7 +13,7 @@ type ManagerDatabaseSource =
ManagerComponents['schemas']['ManagerDatabaseSource']
type ManagerPackInstalled = ManagerComponents['schemas']['ManagerPackInstalled']
vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({
vi.mock('@/services/comfyManagerService', () => ({
useComfyManagerService: vi.fn()
}))
@@ -23,7 +23,7 @@ vi.mock('@/services/dialogService', () => ({
})
}))
vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => {
vi.mock('@/composables/useManagerQueue', () => {
const enqueueTaskMock = vi.fn()
return {

View File

@@ -58,11 +58,9 @@ vi.mock('firebase/auth', async (importOriginal) => {
onAuthStateChanged: vi.fn(),
signInWithPopup: vi.fn(),
GoogleAuthProvider: class {
addScope = vi.fn()
setCustomParameters = vi.fn()
},
GithubAuthProvider: class {
addScope = vi.fn()
setCustomParameters = vi.fn()
},
setPersistence: vi.fn().mockResolvedValue(undefined)
@@ -150,6 +148,13 @@ describe('useFirebaseAuthStore', () => {
expect(store.loading).toBe(false)
})
it('should set persistence to local storage on initialization', () => {
expect(firebaseAuth.setPersistence).toHaveBeenCalledWith(
mockAuth,
firebaseAuth.browserLocalPersistence
)
})
it('should properly clean up error state between operations', async () => {
// First, cause an error
const mockError = new Error('Invalid password')

View File

@@ -1,8 +1,8 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { ExecutedWsMessage } from '@/schemas/apiSchema'
import { app } from '@/scripts/app'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import * as litegraphUtil from '@/utils/litegraphUtil'

View File

@@ -19,7 +19,7 @@ const EXPECTED_DEFAULT_TYPES = [
'gligen'
] as const
type NodeDefStoreType = ReturnType<typeof useNodeDefStore>
type NodeDefStoreType = typeof import('@/stores/nodeDefStore')
// Create minimal but valid ComfyNodeDefImpl for testing
function createMockNodeDef(name: string): ComfyNodeDefImpl {
@@ -343,107 +343,6 @@ describe('useModelToNodeStore', () => {
})
})
describe('getCategoryForNodeType', () => {
it('should return category for known node type', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
expect(
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
).toBe('checkpoints')
expect(modelToNodeStore.getCategoryForNodeType('LoraLoader')).toBe(
'loras'
)
expect(modelToNodeStore.getCategoryForNodeType('VAELoader')).toBe('vae')
})
it('should return undefined for unknown node type', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
expect(
modelToNodeStore.getCategoryForNodeType('NonExistentNode')
).toBeUndefined()
expect(modelToNodeStore.getCategoryForNodeType('')).toBeUndefined()
})
it('should return first category when node type exists in multiple categories', () => {
const modelToNodeStore = useModelToNodeStore()
// Test with a node that exists in the defaults but add our own first
// Since defaults register 'StyleModelLoader' in 'style_models',
// we verify our custom registrations come after defaults in Object.entries iteration
const result = modelToNodeStore.getCategoryForNodeType('StyleModelLoader')
expect(result).toBe('style_models') // This proves the method works correctly
// Now test that custom registrations after defaults also work
modelToNodeStore.quickRegister(
'unicorn_styles',
'StyleModelLoader',
'param1'
)
const result2 =
modelToNodeStore.getCategoryForNodeType('StyleModelLoader')
// Should still be style_models since it was registered first by defaults
expect(result2).toBe('style_models')
})
it('should trigger lazy registration when called before registerDefaults', () => {
const modelToNodeStore = useModelToNodeStore()
const result = modelToNodeStore.getCategoryForNodeType(
'CheckpointLoaderSimple'
)
expect(result).toBe('checkpoints')
})
it('should be performant for repeated lookups', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
// Measure performance without assuming implementation
const start = performance.now()
for (let i = 0; i < 1000; i++) {
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
}
const end = performance.now()
// Should be fast enough for UI responsiveness
expect(end - start).toBeLessThan(10)
})
it('should handle invalid input types gracefully', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
// These should not throw but return undefined
expect(
modelToNodeStore.getCategoryForNodeType(null as any)
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(undefined as any)
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(123 as any)
).toBeUndefined()
})
it('should be case-sensitive for node type matching', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
expect(
modelToNodeStore.getCategoryForNodeType('checkpointloadersimple')
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType('CHECKPOINTLOADERSIMPLE')
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
).toBe('checkpoints')
})
})
describe('edge cases', () => {
it('should handle empty string model type', () => {
const modelToNodeStore = useModelToNodeStore()

View File

@@ -1,11 +1,10 @@
import { createPinia, setActivePinia } from 'pinia'
import { compare as semverCompare } from 'semver'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
// Mock the dependencies
vi.mock('semver')
vi.mock('@/utils/formatUtil')
vi.mock('@/utils/envUtil')
vi.mock('@/platform/updates/common/releaseService')
vi.mock('@/platform/settings/settingStore')
@@ -114,15 +113,17 @@ describe('useReleaseStore', () => {
expect(store.recentReleases).toEqual(releases.slice(0, 3))
})
it('should show update button (shouldShowUpdateButton)', () => {
vi.mocked(semverCompare).mockReturnValue(1) // newer version available
it('should show update button (shouldShowUpdateButton)', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1) // newer version available
store.releases = [mockRelease]
expect(store.shouldShowUpdateButton).toBe(true)
})
it('should not show update button when no new version', () => {
vi.mocked(semverCompare).mockReturnValue(-1) // current version is newer
it('should not show update button when no new version', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(-1) // current version is newer
store.releases = [mockRelease]
expect(store.shouldShowUpdateButton).toBe(false)
@@ -130,20 +131,21 @@ describe('useReleaseStore', () => {
})
describe('showVersionUpdates setting', () => {
beforeEach(async () => {
beforeEach(() => {
store.releases = [mockRelease]
})
describe('when notifications are enabled', () => {
beforeEach(async () => {
beforeEach(() => {
mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null
})
})
it('should show toast for medium/high attention releases', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should show toast for medium/high attention releases', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
// Need multiple releases for hasMediumOrHighAttention to work
const mediumRelease = {
@@ -156,16 +158,17 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(true)
})
it('should show red dot for new versions', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should show red dot for new versions', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(true)
})
it('should show popup for latest version', () => {
it('should show popup for latest version', async () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
vi.mocked(semverCompare).mockReturnValue(0)
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(true)
})
@@ -178,36 +181,37 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'git-windows',
locale: 'en'
form_factor: 'git-windows'
})
})
})
describe('when notifications are disabled', () => {
beforeEach(async () => {
beforeEach(() => {
mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
return null
})
})
it('should not show toast even with new version available', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should not show toast even with new version available', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowToast).toBe(false)
})
it('should not show red dot even with new version available', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should not show red dot even with new version available', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(false)
})
it('should not show popup even for latest version', () => {
it('should not show popup even for latest version', async () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
vi.mocked(semverCompare).mockReturnValue(0)
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(false)
})
@@ -236,8 +240,7 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'git-windows',
locale: 'en'
form_factor: 'git-windows'
})
expect(store.releases).toEqual([mockRelease])
})
@@ -251,8 +254,7 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'desktop-mac',
locale: 'en'
form_factor: 'desktop-mac'
})
})
@@ -422,7 +424,7 @@ describe('useReleaseStore', () => {
})
describe('action handlers', () => {
beforeEach(async () => {
beforeEach(() => {
store.releases = [mockRelease]
})
@@ -479,7 +481,7 @@ describe('useReleaseStore', () => {
})
describe('popup visibility', () => {
it('should show toast for medium/high attention releases', () => {
it('should show toast for medium/high attention releases', async () => {
mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Release.Version') return null
if (key === 'Comfy.Release.Status') return null
@@ -487,7 +489,8 @@ describe('useReleaseStore', () => {
return null
})
vi.mocked(semverCompare).mockReturnValue(1)
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
const mediumRelease = { ...mockRelease, attention: 'medium' as const }
store.releases = [
@@ -499,8 +502,9 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(true)
})
it('should show red dot for new versions', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should show red dot for new versions', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null
@@ -511,14 +515,15 @@ describe('useReleaseStore', () => {
expect(store.shouldShowRedDot).toBe(true)
})
it('should show popup for latest version', () => {
it('should show popup for latest version', async () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release
mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null
})
vi.mocked(semverCompare).mockReturnValue(0) // versions are equal (latest version)
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0) // versions are equal (latest version)
store.releases = [mockRelease]
@@ -560,7 +565,7 @@ describe('useReleaseStore', () => {
})
describe('isElectron environment checks', () => {
beforeEach(async () => {
beforeEach(() => {
// Set up a new version available
store.releases = [mockRelease]
mockSettingStore.get.mockImplementation((key: string) => {
@@ -575,8 +580,9 @@ describe('useReleaseStore', () => {
vi.mocked(isElectron).mockReturnValue(true)
})
it('should show toast when conditions are met', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should show toast when conditions are met', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
// Need multiple releases for hasMediumOrHighAttention
const mediumRelease = {
@@ -589,16 +595,17 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(true)
})
it('should show red dot when new version available', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should show red dot when new version available', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(true)
})
it('should show popup for latest version', () => {
it('should show popup for latest version', async () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
vi.mocked(semverCompare).mockReturnValue(0)
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(true)
})
@@ -610,8 +617,9 @@ describe('useReleaseStore', () => {
vi.mocked(isElectron).mockReturnValue(false)
})
it('should NOT show toast even when all other conditions are met', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should NOT show toast even when all other conditions are met', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
// Set up all conditions that would normally show toast
const mediumRelease = {
@@ -624,14 +632,16 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(false)
})
it('should NOT show red dot even when new version available', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should NOT show red dot even when new version available', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(false)
})
it('should NOT show toast regardless of attention level', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should NOT show toast regardless of attention level', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
// Test with high attention releases
const highRelease = {
@@ -649,18 +659,19 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(false)
})
it('should NOT show red dot even with high attention release', () => {
vi.mocked(semverCompare).mockReturnValue(1)
it('should NOT show red dot even with high attention release', async () => {
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(1)
store.releases = [{ ...mockRelease, attention: 'high' as const }]
expect(store.shouldShowRedDot).toBe(false)
})
it('should NOT show popup even for latest version', () => {
it('should NOT show popup even for latest version', async () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
vi.mocked(semverCompare).mockReturnValue(0)
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(false)
})

View File

@@ -1,7 +1,7 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { ServerConfig } from '@/constants/serverConfig'
import { ServerConfig } from '@/constants/serverConfig'
import type { FormItem } from '@/platform/settings/types'
import { useServerConfigStore } from '@/stores/serverConfigStore'

View File

@@ -3,9 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import {
type LoadedComfyWorkflow,
ComfyWorkflow,
LoadedComfyWorkflow,
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
import { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import {

View File

@@ -19,7 +19,7 @@ describe('migrateReroute', () => {
'single_connected.json',
'floating.json',
'floating_branch.json'
])('should correctly migrate %s', async (fileName) => {
])('should correctly migrate %s', (fileName) => {
// Load the legacy workflow
const legacyWorkflow = loadWorkflow(
`workflows/reroute/legacy/${fileName}`
@@ -29,9 +29,9 @@ describe('migrateReroute', () => {
const migratedWorkflow = migrateLegacyRerouteNodes(legacyWorkflow)
// Compare with snapshot
await expect(
JSON.stringify(migratedWorkflow, null, 2)
).toMatchFileSnapshot(`workflows/reroute/native/${fileName}`)
expect(JSON.stringify(migratedWorkflow, null, 2)).toMatchFileSnapshot(
`workflows/reroute/native/${fileName}`
)
})
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { TreeNode } from '@/types/treeExplorerTypes'
import { TreeNode } from '@/types/treeExplorerTypes'
import { buildTree, sortedTree } from '@/utils/treeUtil'
describe('buildTree', () => {

View File

@@ -1,14 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* Test files should not be compiled */
"noEmit": true,
// "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true
},
"include": [
"**/*.ts",
]
}