diff --git a/tests-ui/tests/store/settingStore.test.ts b/tests-ui/tests/store/settingStore.test.ts index 45ffa578f..60d7acd1c 100644 --- a/tests-ui/tests/store/settingStore.test.ts +++ b/tests-ui/tests/store/settingStore.test.ts @@ -1,98 +1,559 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { useSettingStore } from '@/platform/settings/settingStore' +import { + getSettingInfo, + useSettingStore +} from '@/platform/settings/settingStore' +import type { SettingParams } from '@/platform/settings/types' +import { api } from '@/scripts/api' +import { app } from '@/scripts/app' -const hoisted = vi.hoisted(() => ({ - trackSettingChanged: vi.fn(), - storeSetting: vi.fn().mockResolvedValue(undefined) +// Mock the api +vi.mock('@/scripts/api', () => ({ + api: { + getSettings: vi.fn(), + storeSetting: vi.fn() + } })) -let isSettingsDialogOpen = false - -vi.mock('@/platform/telemetry', () => { - return { - useTelemetry: () => ({ - trackSettingChanged: hoisted.trackSettingChanged - }) - } -}) - -vi.mock('@/stores/dialogStore', () => { - return { - useDialogStore: () => ({ - isDialogOpen: (key: string) => - isSettingsDialogOpen && key === 'global-settings' - }) - } -}) - -vi.mock('@/scripts/api', () => { - return { - api: { - storeSetting: hoisted.storeSetting, - getSettings: vi.fn().mockResolvedValue({}) - } - } -}) - -vi.mock('@/scripts/app', () => { - return { - app: { - ui: { - settings: { - dispatchChange: vi.fn() - } +// Mock the app +vi.mock('@/scripts/app', () => ({ + app: { + ui: { + settings: { + dispatchChange: vi.fn() } } } -}) +})) + +describe('useSettingStore', () => { + let store: ReturnType -describe('useSettingStore telemetry', () => { beforeEach(() => { setActivePinia(createPinia()) - hoisted.trackSettingChanged.mockReset() - hoisted.storeSetting.mockReset().mockResolvedValue(undefined) - isSettingsDialogOpen = false + store = useSettingStore() + vi.clearAllMocks() }) - it('tracks telemetry when settings dialog is open', async () => { - isSettingsDialogOpen = true + it('should initialize with empty settings', () => { + expect(store.settingValues).toEqual({}) + expect(store.settingsById).toEqual({}) + }) - const store = useSettingStore() - store.addSetting({ - id: 'main.sub.setting.name', - name: 'Test Setting', - type: 'text', - defaultValue: 'old' + describe('loadSettingValues', () => { + it('should load settings from API', async () => { + const mockSettings = { 'test.setting': 'value' } + vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any) + + await store.loadSettingValues() + + expect(store.settingValues).toEqual(mockSettings) + expect(api.getSettings).toHaveBeenCalled() }) - await store.set('main.sub.setting.name', 'new') + it('should throw error if settings are loaded after registration', async () => { + const setting: SettingParams = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default' + } + store.addSetting(setting) - expect(hoisted.trackSettingChanged).toHaveBeenCalledTimes(1) - expect(hoisted.trackSettingChanged).toHaveBeenCalledWith({ - setting_id: 'main.sub.setting.name', - input_type: 'text', - category: 'main', - sub_category: 'sub', - previous_value: 'old', - new_value: 'new' + await expect(store.loadSettingValues()).rejects.toThrow( + 'Setting values must be loaded before any setting is registered.' + ) }) }) - it('does not track telemetry when settings dialog is closed', async () => { - isSettingsDialogOpen = false + describe('addSetting', () => { + it('should register a new setting', () => { + const setting: SettingParams = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default' + } - const store = useSettingStore() - store.addSetting({ - id: 'single.setting', - name: 'Another Setting', - type: 'text', - defaultValue: 'x' + store.addSetting(setting) + + expect(store.settingsById['test.setting']).toEqual(setting) }) - await store.set('single.setting', 'y') + it('should throw error for duplicate setting ID', () => { + const setting: SettingParams = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default' + } - expect(hoisted.trackSettingChanged).not.toHaveBeenCalled() + store.addSetting(setting) + expect(() => store.addSetting(setting)).toThrow( + 'Setting test.setting must have a unique ID.' + ) + }) + + it('should migrate deprecated values', () => { + const setting: SettingParams = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default', + migrateDeprecatedValue: (value: string) => value.toUpperCase() + } + + store.settingValues['test.setting'] = 'oldvalue' + store.addSetting(setting) + + expect(store.settingValues['test.setting']).toBe('OLDVALUE') + }) + }) + + 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 = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default' + } + store.addSetting(setting) + + expect(store.get('test.setting')).toBe('default') + }) + + it('should set value and trigger onChange', async () => { + const onChangeMock = vi.fn() + const dispatchChangeMock = vi.mocked(app.ui.settings.dispatchChange) + const setting: SettingParams = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default', + onChange: onChangeMock + } + store.addSetting(setting) + // Adding the new setting should trigger onChange + expect(onChangeMock).toHaveBeenCalledTimes(1) + expect(dispatchChangeMock).toHaveBeenCalledTimes(1) + + await store.set('test.setting', 'newvalue') + + expect(store.get('test.setting')).toBe('newvalue') + expect(onChangeMock).toHaveBeenCalledWith('newvalue', 'default') + expect(onChangeMock).toHaveBeenCalledTimes(2) + expect(dispatchChangeMock).toHaveBeenCalledTimes(2) + expect(api.storeSetting).toHaveBeenCalledWith('test.setting', 'newvalue') + + // Set the same value again, it should not trigger onChange + await store.set('test.setting', 'newvalue') + expect(onChangeMock).toHaveBeenCalledTimes(2) + expect(dispatchChangeMock).toHaveBeenCalledTimes(2) + + // Set a different value, it should trigger onChange + await store.set('test.setting', 'differentvalue') + expect(onChangeMock).toHaveBeenCalledWith('differentvalue', 'newvalue') + expect(onChangeMock).toHaveBeenCalledTimes(3) + expect(dispatchChangeMock).toHaveBeenCalledTimes(3) + expect(api.storeSetting).toHaveBeenCalledWith( + 'test.setting', + 'differentvalue' + ) + }) + + describe('object mutation prevention', () => { + beforeEach(() => { + const setting: SettingParams = { + id: 'test.setting', + name: 'Test setting', + type: 'hidden', + defaultValue: {} + } + store.addSetting(setting) + }) + + it('should prevent mutations of objects after set', async () => { + const originalObject = { foo: 'bar', nested: { value: 123 } } + + await store.set('test.setting', originalObject) + + // Attempt to mutate the original object + originalObject.foo = 'changed' + originalObject.nested.value = 456 + + // Get the stored value + const storedValue = store.get('test.setting') + + // Verify the stored value wasn't affected by the mutation + expect(storedValue).toEqual({ foo: 'bar', nested: { value: 123 } }) + }) + + it('should prevent mutations of retrieved objects', async () => { + const initialValue = { foo: 'bar', nested: { value: 123 } } + + // Set initial value + await store.set('test.setting', initialValue) + + // Get the value and try to mutate it + const retrievedValue = store.get('test.setting') + retrievedValue.foo = 'changed' + if (retrievedValue.nested) { + retrievedValue.nested.value = 456 + } + + // Get the value again + const newRetrievedValue = store.get('test.setting') + + // Verify the stored value wasn't affected by the mutation + expect(newRetrievedValue).toEqual({ + foo: 'bar', + nested: { value: 123 } + }) + }) + + it('should prevent mutations of arrays after set', async () => { + const originalArray = [1, 2, { value: 3 }] + + await store.set('test.setting', originalArray) + + // Attempt to mutate the original array + originalArray.push(4) + if (typeof originalArray[2] === 'object') { + originalArray[2].value = 999 + } + + // Get the stored value + const storedValue = store.get('test.setting') + + // Verify the stored value wasn't affected by the mutation + expect(storedValue).toEqual([1, 2, { value: 3 }]) + }) + + it('should prevent mutations of retrieved arrays', async () => { + const initialArray = [1, 2, { value: 3 }] + + // Set initial value + await store.set('test.setting', initialArray) + + // Get the value and try to mutate it + const retrievedArray = store.get('test.setting') + retrievedArray.push(4) + if (typeof retrievedArray[2] === 'object') { + retrievedArray[2].value = 999 + } + + // Get the value again + const newRetrievedValue = store.get('test.setting') + + // Verify the stored value wasn't affected by the mutation + expect(newRetrievedValue).toEqual([1, 2, { value: 3 }]) + }) + }) + }) +}) + +describe('getSettingInfo', () => { + const baseSetting: SettingParams = { + id: 'test.setting', + name: 'test.setting', + type: 'text', + defaultValue: 'default' + } + + it('should handle settings with explicit category array', () => { + const setting: SettingParams = { + ...baseSetting, + id: 'test.setting', + category: ['Main', 'Sub', 'Detail'] + } + + const result = getSettingInfo(setting) + + expect(result).toEqual({ + category: 'Main', + subCategory: 'Sub' + }) + }) + + it('should handle settings with id-based categorization', () => { + const setting: SettingParams = { + ...baseSetting, + id: 'main.sub.setting.name' + } + + const result = getSettingInfo(setting) + + expect(result).toEqual({ + category: 'main', + subCategory: 'sub' + }) + }) + + it('should use "Other" as default subCategory when missing', () => { + const setting: SettingParams = { + ...baseSetting, + id: 'single.setting', + category: ['single'] + } + + const result = getSettingInfo(setting) + + expect(result).toEqual({ + category: 'single', + subCategory: 'Other' + }) + }) + + it('should use "Other" as default category when missing', () => { + const setting: SettingParams = { + ...baseSetting, + id: 'single.setting', + category: [] + } + + const result = getSettingInfo(setting) + + expect(result).toEqual({ + category: 'Other', + subCategory: 'Other' + }) }) })