diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts new file mode 100644 index 000000000..69a40ced9 --- /dev/null +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -0,0 +1,289 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.describe('Settings Search functionality', () => { + test.beforeEach(async ({ comfyPage }) => { + // Register test settings to verify hidden/deprecated filtering + await comfyPage.page.evaluate(() => { + window['app'].registerExtension({ + name: 'TestSettingsExtension', + settings: [ + { + id: 'TestHiddenSetting', + name: 'Test Hidden Setting', + type: 'hidden', + defaultValue: 'hidden_value', + category: ['Test', 'Hidden'] + }, + { + id: 'TestDeprecatedSetting', + name: 'Test Deprecated Setting', + type: 'text', + defaultValue: 'deprecated_value', + deprecated: true, + category: ['Test', 'Deprecated'] + }, + { + id: 'TestVisibleSetting', + name: 'Test Visible Setting', + type: 'text', + defaultValue: 'visible_value', + category: ['Test', 'Visible'] + } + ] + }) + }) + }) + + test('can open settings dialog and use search box', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await expect(searchBox).toBeVisible() + + // Verify search box has the correct placeholder + await expect(searchBox).toHaveAttribute( + 'placeholder', + expect.stringContaining('Search') + ) + }) + + test('search box is functional and accepts input', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find and interact with the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Comfy') + + // Verify the input was accepted + await expect(searchBox).toHaveValue('Comfy') + }) + + test('search box clears properly', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find and interact with the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('test') + await expect(searchBox).toHaveValue('test') + + // Clear the search box + await searchBox.clear() + await expect(searchBox).toHaveValue('') + }) + + test('settings categories are visible in sidebar', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Check that the sidebar has categories + const categories = comfyPage.page.locator( + '.settings-sidebar .p-listbox-option' + ) + expect(await categories.count()).toBeGreaterThan(0) + + // Check that at least one category is visible + await expect(categories.first()).toBeVisible() + }) + + test('can select different categories in sidebar', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Get categories and click on different ones + const categories = comfyPage.page.locator( + '.settings-sidebar .p-listbox-option' + ) + const categoryCount = await categories.count() + + if (categoryCount > 1) { + // Click on the second category + await categories.nth(1).click() + + // Verify the category is selected + await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/) + } + }) + + test('settings content area is visible', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Check that the content area is visible + const contentArea = comfyPage.page.locator('.settings-content') + await expect(contentArea).toBeVisible() + + // Check that tab panels are visible + const tabPanels = comfyPage.page.locator('.settings-tab-panels') + await expect(tabPanels).toBeVisible() + }) + + test('search functionality affects UI state', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + + // Type in search box + await searchBox.fill('graph') + await comfyPage.page.waitForTimeout(200) // Wait for debounce + + // Verify that the search input is handled + await expect(searchBox).toHaveValue('graph') + }) + + test('settings dialog can be closed', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Close with escape key + await comfyPage.page.keyboard.press('Escape') + + // Verify dialog is closed + await expect(settingsDialog).not.toBeVisible() + }) + + test('search box has proper debouncing behavior', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Type rapidly in search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('a') + await searchBox.fill('ab') + await searchBox.fill('abc') + await searchBox.fill('abcd') + + // Wait for debounce + await comfyPage.page.waitForTimeout(200) + + // Verify final value + await expect(searchBox).toHaveValue('abcd') + }) + + test('search excludes hidden settings from results', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Search for our test settings + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Test') + await comfyPage.page.waitForTimeout(300) // Wait for debounce + + // Get all settings content + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Should show visible setting but not hidden setting + await expect(settingsContent).toContainText('Test Visible Setting') + await expect(settingsContent).not.toContainText('Test Hidden Setting') + }) + + test('search excludes deprecated settings from results', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Search for our test settings + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Test') + await comfyPage.page.waitForTimeout(300) // Wait for debounce + + // Get all settings content + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Should show visible setting but not deprecated setting + await expect(settingsContent).toContainText('Test Visible Setting') + await expect(settingsContent).not.toContainText('Test Deprecated Setting') + }) + + test('search shows visible settings but excludes hidden and deprecated', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Search for our test settings + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Test') + await comfyPage.page.waitForTimeout(300) // Wait for debounce + + // Get all settings content + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Should only show the visible setting + await expect(settingsContent).toContainText('Test Visible Setting') + + // Should not show hidden or deprecated settings + await expect(settingsContent).not.toContainText('Test Hidden Setting') + await expect(settingsContent).not.toContainText('Test Deprecated Setting') + }) + + test('search by setting name excludes hidden and deprecated', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + const searchBox = comfyPage.page.locator('.settings-search-box input') + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Search specifically for hidden setting by name + await searchBox.clear() + await searchBox.fill('Hidden') + await comfyPage.page.waitForTimeout(300) + + // Should not show the hidden setting even when searching by name + await expect(settingsContent).not.toContainText('Test Hidden Setting') + + // Search specifically for deprecated setting by name + await searchBox.clear() + await searchBox.fill('Deprecated') + await comfyPage.page.waitForTimeout(300) + + // Should not show the deprecated setting even when searching by name + await expect(settingsContent).not.toContainText('Test Deprecated Setting') + + // Search for visible setting by name - should work + await searchBox.clear() + await searchBox.fill('Visible') + await comfyPage.page.waitForTimeout(300) + + // Should show the visible setting + await expect(settingsContent).toContainText('Test Visible Setting') + }) +}) diff --git a/src/composables/setting/useSettingSearch.ts b/src/composables/setting/useSettingSearch.ts index e3e51890d..259eef736 100644 --- a/src/composables/setting/useSettingSearch.ts +++ b/src/composables/setting/useSettingSearch.ts @@ -53,6 +53,11 @@ export function useSettingSearch() { const queryLower = query.toLocaleLowerCase() const allSettings = Object.values(settingStore.settingsById) const filteredSettings = allSettings.filter((setting) => { + // Filter out hidden and deprecated settings, just like in normal settings tree + if (setting.type === 'hidden' || setting.deprecated) { + return false + } + const idLower = setting.id.toLowerCase() const nameLower = setting.name.toLowerCase() const translatedName = st( diff --git a/tests-ui/tests/composables/useSettingSearch.test.ts b/tests-ui/tests/composables/useSettingSearch.test.ts new file mode 100644 index 000000000..91d09142a --- /dev/null +++ b/tests-ui/tests/composables/useSettingSearch.test.ts @@ -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']) + ) + }) + }) +})