mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Merge remote-tracking branch 'upstream/main' into js/async_nodes
This commit is contained in:
@@ -227,7 +227,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.02/Run')
|
||||
expect(price).toBe('$0.020/Run')
|
||||
})
|
||||
|
||||
it('should return $0.018 for 512x512 size', () => {
|
||||
@@ -255,7 +255,7 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('OpenAIDalle2', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.016-0.02/Run (varies with size)')
|
||||
expect(price).toBe('$0.016-0.02 x n/Run (varies with size & n)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -295,19 +295,19 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('OpenAIGPTImage1', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.011-0.30/Run (varies with quality)')
|
||||
expect(price).toBe('$0.011-0.30 x n/Run (varies with quality & n)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - IdeogramV3', () => {
|
||||
it('should return $0.08 for Quality rendering speed', () => {
|
||||
it('should return $0.09 for Quality rendering speed', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV3', [
|
||||
{ name: 'rendering_speed', value: 'Quality' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.08/Run')
|
||||
expect(price).toBe('$0.09/Run')
|
||||
})
|
||||
|
||||
it('should return $0.06 for Balanced rendering speed', () => {
|
||||
@@ -335,7 +335,31 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('IdeogramV3', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.03-0.08/Run (varies with rendering speed)')
|
||||
expect(price).toBe(
|
||||
'$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)'
|
||||
)
|
||||
})
|
||||
|
||||
it('should multiply price by num_images for Quality rendering speed', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV3', [
|
||||
{ name: 'rendering_speed', value: 'Quality' },
|
||||
{ name: 'num_images', value: 3 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.27/Run') // 0.09 * 3
|
||||
})
|
||||
|
||||
it('should multiply price by num_images for Turbo rendering speed', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV3', [
|
||||
{ name: 'rendering_speed', value: 'Turbo' },
|
||||
{ name: 'num_images', value: 5 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.15/Run') // 0.03 * 5
|
||||
})
|
||||
})
|
||||
|
||||
@@ -742,6 +766,29 @@ describe('useNodePricing', () => {
|
||||
expect(widgetNames).toEqual([])
|
||||
})
|
||||
|
||||
describe('Ideogram nodes with num_images parameter', () => {
|
||||
it('should return correct widget names for IdeogramV1', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
|
||||
const widgetNames = getRelevantWidgetNames('IdeogramV1')
|
||||
expect(widgetNames).toEqual(['num_images'])
|
||||
})
|
||||
|
||||
it('should return correct widget names for IdeogramV2', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
|
||||
const widgetNames = getRelevantWidgetNames('IdeogramV2')
|
||||
expect(widgetNames).toEqual(['num_images'])
|
||||
})
|
||||
|
||||
it('should return correct widget names for IdeogramV3', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
|
||||
const widgetNames = getRelevantWidgetNames('IdeogramV3')
|
||||
expect(widgetNames).toEqual(['rendering_speed', 'num_images'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Recraft nodes with n parameter', () => {
|
||||
it('should return correct widget names for RecraftTextToImageNode', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
@@ -759,6 +806,54 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Ideogram nodes dynamic pricing', () => {
|
||||
it('should calculate dynamic pricing for IdeogramV1 based on num_images value', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV1', [
|
||||
{ name: 'num_images', value: 3 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.18/Run') // 0.06 * 3
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for IdeogramV2 based on num_images value', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV2', [
|
||||
{ name: 'num_images', value: 4 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.32/Run') // 0.08 * 4
|
||||
})
|
||||
|
||||
it('should fall back to static display when num_images widget is missing for IdeogramV1', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV1', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.06 x num_images/Run')
|
||||
})
|
||||
|
||||
it('should fall back to static display when num_images widget is missing for IdeogramV2', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV2', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.08 x num_images/Run')
|
||||
})
|
||||
|
||||
it('should handle edge case when num_images value is 1 for IdeogramV1', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV1', [
|
||||
{ name: 'num_images', value: 1 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.06/Run') // 0.06 * 1
|
||||
})
|
||||
})
|
||||
|
||||
describe('Recraft nodes dynamic pricing', () => {
|
||||
it('should calculate dynamic pricing for RecraftTextToImageNode based on n value', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
@@ -799,4 +894,133 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('OpenAI nodes dynamic pricing with n parameter', () => {
|
||||
it('should calculate dynamic pricing for OpenAIDalle2 based on size and n', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIDalle2', [
|
||||
{ name: 'size', value: '1024x1024' },
|
||||
{ name: 'n', value: 3 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.060/Run') // 0.02 * 3
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for OpenAIGPTImage1 based on quality and n', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIGPTImage1', [
|
||||
{ name: 'quality', value: 'low' },
|
||||
{ name: 'n', value: 2 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.011-0.02 x 2/Run')
|
||||
})
|
||||
|
||||
it('should fall back to static display when n widget is missing for OpenAIDalle2', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIDalle2', [
|
||||
{ name: 'size', value: '512x512' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.018/Run') // n defaults to 1
|
||||
})
|
||||
})
|
||||
|
||||
describe('KlingImageGenerationNode dynamic pricing with n parameter', () => {
|
||||
it('should calculate dynamic pricing for text-to-image with kling-v1', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('KlingImageGenerationNode', [
|
||||
{ name: 'model_name', value: 'kling-v1' },
|
||||
{ name: 'n', value: 4 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.0140/Run') // 0.0035 * 4
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for text-to-image with kling-v1-5', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
// Mock node without image input (text-to-image mode)
|
||||
const node = createMockNode('KlingImageGenerationNode', [
|
||||
{ name: 'model_name', value: 'kling-v1-5' },
|
||||
{ name: 'n', value: 2 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.0280/Run') // For kling-v1-5 text-to-image: 0.014 * 2
|
||||
})
|
||||
|
||||
it('should fall back to static display when model widget is missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('KlingImageGenerationNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.0035-0.028 x n/Run (varies with modality & model)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('New Recraft nodes dynamic pricing', () => {
|
||||
it('should calculate dynamic pricing for RecraftGenerateImageNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RecraftGenerateImageNode', [
|
||||
{ name: 'n', value: 3 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.12/Run') // 0.04 * 3
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for RecraftVectorizeImageNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RecraftVectorizeImageNode', [
|
||||
{ name: 'n', value: 5 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05/Run') // 0.01 * 5
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for RecraftGenerateVectorImageNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RecraftGenerateVectorImageNode', [
|
||||
{ name: 'n', value: 2 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.16/Run') // 0.08 * 2
|
||||
})
|
||||
})
|
||||
|
||||
describe('Widget names for reactive updates', () => {
|
||||
it('should include n parameter for OpenAI nodes', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
|
||||
expect(getRelevantWidgetNames('OpenAIDalle2')).toEqual(['size', 'n'])
|
||||
expect(getRelevantWidgetNames('OpenAIGPTImage1')).toEqual([
|
||||
'quality',
|
||||
'n'
|
||||
])
|
||||
})
|
||||
|
||||
it('should include n parameter for Kling and new Recraft nodes', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
|
||||
expect(getRelevantWidgetNames('KlingImageGenerationNode')).toEqual([
|
||||
'modality',
|
||||
'model_name',
|
||||
'n'
|
||||
])
|
||||
expect(getRelevantWidgetNames('RecraftVectorizeImageNode')).toEqual(['n'])
|
||||
expect(getRelevantWidgetNames('RecraftGenerateImageNode')).toEqual(['n'])
|
||||
expect(getRelevantWidgetNames('RecraftGenerateVectorImageNode')).toEqual([
|
||||
'n'
|
||||
])
|
||||
expect(
|
||||
getRelevantWidgetNames('RecraftGenerateColorFromImageNode')
|
||||
).toEqual(['n'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
425
tests-ui/tests/composables/useSettingSearch.test.ts
Normal file
425
tests-ui/tests/composables/useSettingSearch.test.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { st } from '@/i18n'
|
||||
import { getSettingInfo, useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: vi.fn((_: string, fallback: string) => fallback)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: vi.fn(),
|
||||
getSettingInfo: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useSettingSearch', () => {
|
||||
let mockSettingStore: any
|
||||
let mockSettings: any
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock settings data
|
||||
mockSettings = {
|
||||
'Category.Setting1': {
|
||||
id: 'Category.Setting1',
|
||||
name: 'Setting One',
|
||||
type: 'text',
|
||||
defaultValue: 'default',
|
||||
category: ['Category', 'Basic']
|
||||
},
|
||||
'Category.Setting2': {
|
||||
id: 'Category.Setting2',
|
||||
name: 'Setting Two',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
category: ['Category', 'Advanced']
|
||||
},
|
||||
'Category.HiddenSetting': {
|
||||
id: 'Category.HiddenSetting',
|
||||
name: 'Hidden Setting',
|
||||
type: 'hidden',
|
||||
defaultValue: 'hidden',
|
||||
category: ['Category', 'Basic']
|
||||
},
|
||||
'Category.DeprecatedSetting': {
|
||||
id: 'Category.DeprecatedSetting',
|
||||
name: 'Deprecated Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'deprecated',
|
||||
deprecated: true,
|
||||
category: ['Category', 'Advanced']
|
||||
},
|
||||
'Other.Setting3': {
|
||||
id: 'Other.Setting3',
|
||||
name: 'Other Setting',
|
||||
type: 'select',
|
||||
defaultValue: 'option1',
|
||||
category: ['Other', 'SubCategory']
|
||||
}
|
||||
}
|
||||
|
||||
// Mock setting store
|
||||
mockSettingStore = {
|
||||
settingsById: mockSettings
|
||||
}
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
|
||||
|
||||
// Mock getSettingInfo function
|
||||
vi.mocked(getSettingInfo).mockImplementation((setting: any) => {
|
||||
const parts = setting.category || setting.id.split('.')
|
||||
return {
|
||||
category: parts[0] ?? 'Other',
|
||||
subCategory: parts[1] ?? 'Other'
|
||||
}
|
||||
})
|
||||
|
||||
// Mock st function to return fallback value
|
||||
vi.mocked(st).mockImplementation((_: string, fallback: string) => fallback)
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('initializes with default state', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
expect(search.searchQuery.value).toBe('')
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
expect(search.searchInProgress.value).toBe(false)
|
||||
expect(search.queryIsEmpty.value).toBe(true)
|
||||
expect(search.inSearch.value).toBe(false)
|
||||
expect(search.searchResultsCategories.value).toEqual(new Set())
|
||||
})
|
||||
})
|
||||
|
||||
describe('reactive properties', () => {
|
||||
it('queryIsEmpty computed property works correctly', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
expect(search.queryIsEmpty.value).toBe(true)
|
||||
|
||||
search.searchQuery.value = 'test'
|
||||
expect(search.queryIsEmpty.value).toBe(false)
|
||||
|
||||
search.searchQuery.value = ''
|
||||
expect(search.queryIsEmpty.value).toBe(true)
|
||||
})
|
||||
|
||||
it('inSearch computed property works correctly', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Empty query, not in search
|
||||
expect(search.inSearch.value).toBe(false)
|
||||
|
||||
// Has query but search in progress
|
||||
search.searchQuery.value = 'test'
|
||||
search.searchInProgress.value = true
|
||||
expect(search.inSearch.value).toBe(false)
|
||||
|
||||
// Has query and search complete
|
||||
search.searchInProgress.value = false
|
||||
expect(search.inSearch.value).toBe(true)
|
||||
})
|
||||
|
||||
it('searchResultsCategories computed property works correctly', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// No results
|
||||
expect(search.searchResultsCategories.value).toEqual(new Set())
|
||||
|
||||
// Add some filtered results
|
||||
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
|
||||
expect(search.searchResultsCategories.value).toEqual(
|
||||
new Set(['Category', 'Other'])
|
||||
)
|
||||
})
|
||||
|
||||
it('watches searchQuery and sets searchInProgress to true', async () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
expect(search.searchInProgress.value).toBe(false)
|
||||
|
||||
search.searchQuery.value = 'test'
|
||||
await nextTick()
|
||||
|
||||
expect(search.searchInProgress.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleSearch', () => {
|
||||
it('clears results when query is empty', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = ['Category.Setting1']
|
||||
|
||||
search.handleSearch('')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('filters settings by ID (case insensitive)', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('category.setting1')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Category.Setting1')
|
||||
expect(search.filteredSettingIds.value).not.toContain('Other.Setting3')
|
||||
})
|
||||
|
||||
it('filters settings by name (case insensitive)', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('setting one')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Category.Setting1')
|
||||
expect(search.filteredSettingIds.value).not.toContain('Category.Setting2')
|
||||
})
|
||||
|
||||
it('filters settings by category', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('other')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Other.Setting3')
|
||||
expect(search.filteredSettingIds.value).not.toContain('Category.Setting1')
|
||||
})
|
||||
|
||||
it('excludes hidden settings from results', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('hidden')
|
||||
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.HiddenSetting'
|
||||
)
|
||||
})
|
||||
|
||||
it('excludes deprecated settings from results', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('deprecated')
|
||||
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.DeprecatedSetting'
|
||||
)
|
||||
})
|
||||
|
||||
it('sets searchInProgress to false after search', () => {
|
||||
const search = useSettingSearch()
|
||||
search.searchInProgress.value = true
|
||||
|
||||
search.handleSearch('test')
|
||||
|
||||
expect(search.searchInProgress.value).toBe(false)
|
||||
})
|
||||
|
||||
it('includes visible settings in results', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('setting')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Category.Setting1',
|
||||
'Category.Setting2',
|
||||
'Other.Setting3'
|
||||
])
|
||||
)
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.HiddenSetting'
|
||||
)
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.DeprecatedSetting'
|
||||
)
|
||||
})
|
||||
|
||||
it('includes all visible settings in comprehensive search', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Search for a partial match that should include multiple settings
|
||||
search.handleSearch('setting')
|
||||
|
||||
// Should find all visible settings (not hidden/deprecated)
|
||||
expect(search.filteredSettingIds.value.length).toBeGreaterThan(0)
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Category.Setting1',
|
||||
'Category.Setting2',
|
||||
'Other.Setting3'
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('uses translated categories for search', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Mock st to return translated category names
|
||||
vi.mocked(st).mockImplementation((key: string, fallback: string) => {
|
||||
if (key === 'settingsCategories.Category') {
|
||||
return 'Translated Category'
|
||||
}
|
||||
return fallback
|
||||
})
|
||||
|
||||
search.handleSearch('translated category')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining(['Category.Setting1', 'Category.Setting2'])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSearchResults', () => {
|
||||
it('groups results by subcategory', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = [
|
||||
'Category.Setting1',
|
||||
'Category.Setting2'
|
||||
]
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
settings: [mockSettings['Category.Setting2']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('filters results by active category', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
|
||||
|
||||
const activeCategory = { label: 'Category' } as any
|
||||
const results = search.getSearchResults(activeCategory)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns all results when no active category', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
},
|
||||
{
|
||||
label: 'SubCategory',
|
||||
settings: [mockSettings['Other.Setting3']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns empty array when no filtered results', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = []
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
it('handles multiple settings in same subcategory', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Add another setting to Basic subcategory
|
||||
mockSettings['Category.Setting4'] = {
|
||||
id: 'Category.Setting4',
|
||||
name: 'Setting Four',
|
||||
type: 'text',
|
||||
defaultValue: 'default',
|
||||
category: ['Category', 'Basic']
|
||||
}
|
||||
|
||||
search.filteredSettingIds.value = [
|
||||
'Category.Setting1',
|
||||
'Category.Setting4'
|
||||
]
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [
|
||||
mockSettings['Category.Setting1'],
|
||||
mockSettings['Category.Setting4']
|
||||
]
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty settings store', () => {
|
||||
mockSettingStore.settingsById = {}
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('test')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('handles settings with undefined category', () => {
|
||||
mockSettings['NoCategorySetting'] = {
|
||||
id: 'NoCategorySetting',
|
||||
name: 'No Category',
|
||||
type: 'text',
|
||||
defaultValue: 'default'
|
||||
}
|
||||
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('category')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('NoCategorySetting')
|
||||
})
|
||||
|
||||
it('handles special characters in search query', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Search for part of the ID that contains a dot
|
||||
search.handleSearch('category.setting')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Category.Setting1')
|
||||
})
|
||||
|
||||
it('handles very long search queries', () => {
|
||||
const search = useSettingSearch()
|
||||
const longQuery = 'a'.repeat(1000)
|
||||
|
||||
search.handleSearch(longQuery)
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('handles rapid consecutive searches', async () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('setting')
|
||||
search.handleSearch('other')
|
||||
search.handleSearch('category')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining(['Category.Setting1', 'Category.Setting2'])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
442
tests-ui/tests/services/newUserService.test.ts
Normal file
442
tests-ui/tests/services/newUserService.test.ts
Normal file
@@ -0,0 +1,442 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockLocalStorage = vi.hoisted(() => ({
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn()
|
||||
}))
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage,
|
||||
writable: true
|
||||
})
|
||||
|
||||
vi.mock('@/config/version', () => ({
|
||||
__COMFYUI_FRONTEND_VERSION__: '1.24.0'
|
||||
}))
|
||||
|
||||
//@ts-expect-error Define global for the test
|
||||
global.__COMFYUI_FRONTEND_VERSION__ = '1.24.0'
|
||||
|
||||
describe('newUserService', () => {
|
||||
let service: ReturnType<
|
||||
typeof import('@/services/newUserService').newUserService
|
||||
>
|
||||
let mockSettingStore: any
|
||||
let newUserService: typeof import('@/services/newUserService').newUserService
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.resetModules()
|
||||
|
||||
const module = await import('@/services/newUserService')
|
||||
newUserService = module.newUserService
|
||||
|
||||
service = newUserService()
|
||||
|
||||
mockSettingStore = {
|
||||
settingValues: {},
|
||||
get: vi.fn(),
|
||||
set: vi.fn()
|
||||
}
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
})
|
||||
|
||||
describe('checkIsNewUser logic', () => {
|
||||
it('should identify new user when all conditions are met', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
})
|
||||
|
||||
it('should identify new user when settings exist but TutorialCompleted is undefined', async () => {
|
||||
mockSettingStore.settingValues = { 'some.setting': 'value' }
|
||||
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
})
|
||||
|
||||
it('should identify existing user when tutorial is completed', async () => {
|
||||
mockSettingStore.settingValues = { 'Comfy.TutorialCompleted': true }
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return true
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(false)
|
||||
})
|
||||
|
||||
it('should identify existing user when workflow exists', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockImplementation((key: string) => {
|
||||
if (key === 'workflow') return 'some-workflow'
|
||||
return null
|
||||
})
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(false)
|
||||
})
|
||||
|
||||
it('should identify existing user when previous workflow exists', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.PreviousWorkflow') return 'some-previous-workflow'
|
||||
return null
|
||||
})
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(false)
|
||||
})
|
||||
|
||||
it('should identify new user when tutorial is explicitly false', async () => {
|
||||
mockSettingStore.settingValues = { 'Comfy.TutorialCompleted': false }
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return false
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
})
|
||||
|
||||
it('should identify existing user when has both settings and tutorial completed', async () => {
|
||||
mockSettingStore.settingValues = {
|
||||
'some.setting': 'value',
|
||||
'Comfy.TutorialCompleted': true
|
||||
}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return true
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(false)
|
||||
})
|
||||
|
||||
it('should identify existing user when only one condition fails', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockImplementation((key: string) => {
|
||||
if (key === 'workflow') return 'some-workflow'
|
||||
if (key === 'Comfy.PreviousWorkflow') return null
|
||||
return null
|
||||
})
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('registerInitCallback', () => {
|
||||
it('should execute callback immediately if new user is already determined', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
|
||||
await service.registerInitCallback(mockCallback)
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should queue callbacks when user status is not determined', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await service.registerInitCallback(mockCallback)
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled()
|
||||
expect(service.isNewUser()).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle callback errors gracefully', async () => {
|
||||
const mockCallback = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Callback error'))
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
await service.registerInitCallback(mockCallback)
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'New user initialization callback failed:',
|
||||
expect.any(Error)
|
||||
)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('initializeIfNewUser', () => {
|
||||
it('should set installed version for new users', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.InstalledVersion',
|
||||
'1.24.0'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not set installed version for existing users', async () => {
|
||||
mockSettingStore.settingValues = { 'some.setting': 'value' }
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return true
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(mockSettingStore.set).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute pending callbacks for new users', async () => {
|
||||
const mockCallback1 = vi.fn().mockResolvedValue(undefined)
|
||||
const mockCallback2 = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await service.registerInitCallback(mockCallback1)
|
||||
await service.registerInitCallback(mockCallback2)
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(mockCallback1).toHaveBeenCalledTimes(1)
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not execute pending callbacks for existing users', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await service.registerInitCallback(mockCallback)
|
||||
|
||||
mockSettingStore.settingValues = { 'some.setting': 'value' }
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return true
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle callback errors during initialization', async () => {
|
||||
const mockCallback = vi.fn().mockRejectedValue(new Error('Init error'))
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
await service.registerInitCallback(mockCallback)
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'New user initialization callback failed:',
|
||||
expect.any(Error)
|
||||
)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should not reinitialize if already determined', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
expect(mockSettingStore.set).toHaveBeenCalledTimes(1)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
expect(mockSettingStore.set).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should correctly determine new user status', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
// Before initialization, isNewUser should return null
|
||||
expect(service.isNewUser()).toBeNull()
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
// After initialization, isNewUser should return true for a new user
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
|
||||
// Should set the installed version for new users
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.InstalledVersion',
|
||||
expect.any(String)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isNewUser', () => {
|
||||
it('should return null before determination', () => {
|
||||
expect(service.isNewUser()).toBeNull()
|
||||
})
|
||||
|
||||
it('should return cached result after determination', async () => {
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockReturnValue(undefined)
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle settingStore.get returning false as not completed', async () => {
|
||||
mockSettingStore.settingValues = { 'Comfy.TutorialCompleted': false }
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return false
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service.isNewUser()).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle multiple callback registrations after initialization', async () => {
|
||||
const mockCallback1 = vi.fn().mockResolvedValue(undefined)
|
||||
const mockCallback2 = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
await service.registerInitCallback(mockCallback1)
|
||||
await service.registerInitCallback(mockCallback2)
|
||||
|
||||
expect(mockCallback1).toHaveBeenCalledTimes(1)
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('state sharing between instances', () => {
|
||||
it('should share state between multiple service instances', async () => {
|
||||
const service1 = newUserService()
|
||||
const service2 = newUserService()
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service1.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(service2.isNewUser()).toBe(true)
|
||||
expect(service1.isNewUser()).toBe(service2.isNewUser())
|
||||
})
|
||||
|
||||
it('should execute callbacks registered on different instances', async () => {
|
||||
const service1 = newUserService()
|
||||
const service2 = newUserService()
|
||||
|
||||
const mockCallback1 = vi.fn().mockResolvedValue(undefined)
|
||||
const mockCallback2 = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await service1.registerInitCallback(mockCallback1)
|
||||
await service2.registerInitCallback(mockCallback2)
|
||||
|
||||
mockSettingStore.settingValues = {}
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.TutorialCompleted') return undefined
|
||||
return undefined
|
||||
})
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
await service1.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
expect(mockCallback1).toHaveBeenCalledTimes(1)
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -72,6 +72,14 @@ describe('useComfyRegistryStore', () => {
|
||||
error: ReturnType<typeof ref<string | null>>
|
||||
listAllPacks: ReturnType<typeof vi.fn>
|
||||
getPackById: ReturnType<typeof vi.fn>
|
||||
inferPackFromNodeName: ReturnType<typeof vi.fn>
|
||||
search: ReturnType<typeof vi.fn>
|
||||
getPackVersions: ReturnType<typeof vi.fn>
|
||||
getPackByVersion: ReturnType<typeof vi.fn>
|
||||
getPublisherById: ReturnType<typeof vi.fn>
|
||||
listPacksForPublisher: ReturnType<typeof vi.fn>
|
||||
getNodeDefs: ReturnType<typeof vi.fn>
|
||||
postPackReview: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -106,7 +114,15 @@ describe('useComfyRegistryStore', () => {
|
||||
// Otherwise return paginated results
|
||||
return Promise.resolve(mockListResult)
|
||||
}),
|
||||
getPackById: vi.fn().mockResolvedValue(mockNodePack)
|
||||
getPackById: vi.fn().mockResolvedValue(mockNodePack),
|
||||
inferPackFromNodeName: vi.fn().mockResolvedValue(mockNodePack),
|
||||
search: vi.fn().mockResolvedValue(mockListResult),
|
||||
getPackVersions: vi.fn().mockResolvedValue([]),
|
||||
getPackByVersion: vi.fn().mockResolvedValue({}),
|
||||
getPublisherById: vi.fn().mockResolvedValue({}),
|
||||
listPacksForPublisher: vi.fn().mockResolvedValue([]),
|
||||
getNodeDefs: vi.fn().mockResolvedValue({}),
|
||||
postPackReview: vi.fn().mockResolvedValue({})
|
||||
}
|
||||
|
||||
vi.mocked(useComfyRegistryService).mockReturnValue(
|
||||
@@ -186,4 +202,58 @@ describe('useComfyRegistryStore', () => {
|
||||
expect.any(Object) // abort signal
|
||||
)
|
||||
})
|
||||
|
||||
describe('inferPackFromNodeName', () => {
|
||||
it('should fetch a pack by comfy node name', async () => {
|
||||
const store = useComfyRegistryStore()
|
||||
const nodeName = 'KSampler'
|
||||
|
||||
const result = await store.inferPackFromNodeName.call(nodeName)
|
||||
|
||||
expect(result).toEqual(mockNodePack)
|
||||
expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledWith(
|
||||
nodeName,
|
||||
expect.any(Object) // abort signal
|
||||
)
|
||||
})
|
||||
|
||||
it('should cache results', async () => {
|
||||
const store = useComfyRegistryStore()
|
||||
const nodeName = 'KSampler'
|
||||
|
||||
// First call
|
||||
const result1 = await store.inferPackFromNodeName.call(nodeName)
|
||||
expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call - should use cache
|
||||
const result2 = await store.inferPackFromNodeName.call(nodeName)
|
||||
expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(1)
|
||||
expect(result2).toEqual(result1)
|
||||
})
|
||||
|
||||
it('should handle null results when node is not found', async () => {
|
||||
mockRegistryService.inferPackFromNodeName.mockResolvedValueOnce(null)
|
||||
|
||||
const store = useComfyRegistryStore()
|
||||
const result = await store.inferPackFromNodeName.call('NonExistentNode')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should clear cache when clearCache is called', async () => {
|
||||
const store = useComfyRegistryStore()
|
||||
const nodeName = 'KSampler'
|
||||
|
||||
// First call to populate cache
|
||||
await store.inferPackFromNodeName.call(nodeName)
|
||||
expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Clear cache
|
||||
store.clearCache()
|
||||
|
||||
// Call again - should hit the service again
|
||||
await store.inferPackFromNodeName.call(nodeName)
|
||||
expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,6 +61,12 @@ describe('useReleaseStore', () => {
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
|
||||
|
||||
// Default showVersionUpdates to true
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
store = useReleaseStore()
|
||||
})
|
||||
|
||||
@@ -114,6 +120,107 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('showVersionUpdates setting', () => {
|
||||
beforeEach(() => {
|
||||
store.releases = [mockRelease]
|
||||
})
|
||||
|
||||
describe('when notifications are enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
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 = {
|
||||
...mockRelease,
|
||||
id: 2,
|
||||
attention: 'medium' as const
|
||||
}
|
||||
store.releases = [mockRelease, mediumRelease]
|
||||
|
||||
expect(store.shouldShowToast).toBe(true)
|
||||
})
|
||||
|
||||
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', async () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
|
||||
it('should fetch releases during initialization', async () => {
|
||||
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
|
||||
project: 'comfyui',
|
||||
current_version: '1.0.0',
|
||||
form_factor: 'git-windows'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when notifications are disabled', () => {
|
||||
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', 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', 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', async () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(false)
|
||||
})
|
||||
|
||||
it('should skip fetching releases during initialization', async () => {
|
||||
await store.initialize()
|
||||
|
||||
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch releases when calling fetchReleases directly', async () => {
|
||||
await store.fetchReleases()
|
||||
|
||||
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
|
||||
expect(store.isLoading).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('release initialization', () => {
|
||||
it('should fetch releases successfully', async () => {
|
||||
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
|
||||
@@ -184,6 +291,17 @@ describe('useReleaseStore', () => {
|
||||
|
||||
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not set loading state when notifications disabled', async () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
|
||||
return null
|
||||
})
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(store.isLoading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('action handlers', () => {
|
||||
@@ -248,6 +366,7 @@ describe('useReleaseStore', () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Release.Version') return null
|
||||
if (key === 'Comfy.Release.Status') return null
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -267,7 +386,10 @@ describe('useReleaseStore', () => {
|
||||
it('should show red dot for new versions', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
mockSettingStore.get.mockReturnValue(null)
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
store.releases = [mockRelease]
|
||||
|
||||
@@ -276,7 +398,10 @@ describe('useReleaseStore', () => {
|
||||
|
||||
it('should show popup for latest version', async () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release
|
||||
mockSettingStore.get.mockReturnValue(null)
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0) // versions are equal (latest version)
|
||||
@@ -286,4 +411,37 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing system stats gracefully', async () => {
|
||||
mockSystemStatsStore.systemStats = null
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
|
||||
return null
|
||||
})
|
||||
|
||||
await store.initialize()
|
||||
|
||||
// Should not fetch system stats when notifications disabled
|
||||
expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle concurrent fetchReleases calls', async () => {
|
||||
mockReleaseService.getReleases.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve([mockRelease]), 100)
|
||||
)
|
||||
)
|
||||
|
||||
// Start two concurrent calls
|
||||
const promise1 = store.fetchReleases()
|
||||
const promise2 = store.fetchReleases()
|
||||
|
||||
await Promise.all([promise1, promise2])
|
||||
|
||||
// Should only call API once due to loading check
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,6 +109,241 @@ describe('useSettingStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDefaultValue', () => {
|
||||
beforeEach(() => {
|
||||
// Set up installed version for most tests
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.30.0'
|
||||
})
|
||||
|
||||
it('should return regular default value when no defaultsByInstallVersion', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default'
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
expect(result).toBe('regular-default')
|
||||
})
|
||||
|
||||
it('should return versioned default when user version matches', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
// installedVersion is 1.30.0, so should get 1.21.3 default
|
||||
expect(result).toBe('version-1.21.3-default')
|
||||
})
|
||||
|
||||
it('should return latest versioned default when user version is higher', () => {
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.50.0'
|
||||
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
// installedVersion is 1.50.0, so should get 1.40.3 default
|
||||
expect(result).toBe('version-1.40.3-default')
|
||||
})
|
||||
|
||||
it('should return regular default when user version is lower than all versioned defaults', () => {
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.10.0'
|
||||
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
// installedVersion is 1.10.0, lower than all versioned defaults
|
||||
expect(result).toBe('regular-default')
|
||||
})
|
||||
|
||||
it('should return regular default when no installed version (existing users)', () => {
|
||||
// Clear installed version to simulate existing user
|
||||
delete store.settingValues['Comfy.InstalledVersion']
|
||||
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
// No installed version, should use backward compatibility
|
||||
expect(result).toBe('regular-default')
|
||||
})
|
||||
|
||||
it('should handle function-based versioned defaults', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': () => 'dynamic-version-1.21.3-default',
|
||||
'1.40.3': () => 'dynamic-version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
// installedVersion is 1.30.0, so should get 1.21.3 default (executed)
|
||||
expect(result).toBe('dynamic-version-1.21.3-default')
|
||||
})
|
||||
|
||||
it('should handle function-based regular defaults with versioned defaults', () => {
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.10.0'
|
||||
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: () => 'dynamic-regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
const result = store.getDefaultValue('test.setting')
|
||||
// installedVersion is 1.10.0, should fallback to function-based regular default
|
||||
expect(result).toBe('dynamic-regular-default')
|
||||
})
|
||||
|
||||
it('should handle complex version comparison correctly', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.21.10': 'version-1.21.10-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
// Test with 1.21.5 - should get 1.21.3 default
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.21.5'
|
||||
expect(store.getDefaultValue('test.setting')).toBe(
|
||||
'version-1.21.3-default'
|
||||
)
|
||||
|
||||
// Test with 1.21.15 - should get 1.21.10 default
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.21.15'
|
||||
expect(store.getDefaultValue('test.setting')).toBe(
|
||||
'version-1.21.10-default'
|
||||
)
|
||||
|
||||
// Test with 1.21.3 exactly - should get 1.21.3 default
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.21.3'
|
||||
expect(store.getDefaultValue('test.setting')).toBe(
|
||||
'version-1.21.3-default'
|
||||
)
|
||||
})
|
||||
|
||||
it('should work with get() method using versioned defaults', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': 'version-1.21.3-default',
|
||||
'1.40.3': 'version-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
// get() should use getDefaultValue internally
|
||||
const result = store.get('test.setting')
|
||||
expect(result).toBe('version-1.21.3-default')
|
||||
})
|
||||
|
||||
it('should handle mixed function and static versioned defaults', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.21.3': () => 'dynamic-1.21.3-default',
|
||||
'1.40.3': 'static-1.40.3-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
// Test with 1.30.0 - should get dynamic 1.21.3 default
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.30.0'
|
||||
expect(store.getDefaultValue('test.setting')).toBe(
|
||||
'dynamic-1.21.3-default'
|
||||
)
|
||||
|
||||
// Test with 1.50.0 - should get static 1.40.3 default
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.50.0'
|
||||
expect(store.getDefaultValue('test.setting')).toBe(
|
||||
'static-1.40.3-default'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle version sorting correctly', () => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'regular-default',
|
||||
defaultsByInstallVersion: {
|
||||
'1.40.3': 'version-1.40.3-default',
|
||||
'1.21.3': 'version-1.21.3-default', // Unsorted order
|
||||
'1.35.0': 'version-1.35.0-default'
|
||||
}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
|
||||
// Test with 1.37.0 - should get 1.35.0 default (highest version <= 1.37.0)
|
||||
store.settingValues['Comfy.InstalledVersion'] = '1.37.0'
|
||||
expect(store.getDefaultValue('test.setting')).toBe(
|
||||
'version-1.35.0-default'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('get and set', () => {
|
||||
it('should get default value when setting not exists', () => {
|
||||
const setting: SettingParams = {
|
||||
|
||||
Reference in New Issue
Block a user