mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-31 05:19:53 +00:00
This pull request introduces improvements to the workflow template selector and search box components, focusing on better user experience and more accurate terminology. The most significant changes include adding debounced search input handling, updating sorting option labels, and refining UI styling for consistency. **Search functionality improvements:** * Refactored `SearchBox.vue` to use an internal search query state and a debounced update mechanism, reducing unnecessary parent updates and improving responsiveness. The parent model is updated only after the user stops typing for 300ms. (`src/components/input/SearchBox.vue`) [[1]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bL6-R6) [[2]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bR39-R62) * Updated the search box in `WorkflowTemplateSelectorDialog.vue` to use the new debounced search model and increased its size for better visibility. (`src/components/custom/widget/WorkflowTemplateSelectorDialog.vue`) **Sorting and terminology updates:** * Changed sorting option labels to use more precise terminology, such as "VRAM Usage (Low to High)" and added new locale strings for sorting options. (`src/components/custom/widget/WorkflowTemplateSelectorDialog.vue`, `src/locales/en/main.json`) [[1]](diffhunk://#diff-2c860bdc48e907b1b85dbef846599d8376dd02cff90f49e490eebe61371fecedL623-R623) [[2]](diffhunk://#diff-bbf3da78aeff5b4d868a17a6960d109cb0627316cda2f9b5fa7c08e9abd93be6L1032-R1035) **UI and styling adjustments:** * Adjusted the width of the sorting dropdown for better alignment and consistency. (`src/components/custom/widget/WorkflowTemplateSelectorDialog.vue`) * Updated active navigation item background color for improved visual clarity. (`src/components/widget/nav/NavItem.vue`) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5975-Workflow-templates-review-2866d73d365081419257f9df2bab9c5b) by [Unito](https://www.unito.io) https://github.com/user-attachments/assets/4f72d515-f114-4cd4-8a76-6abbe906e5bb
193 lines
5.7 KiB
TypeScript
193 lines
5.7 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { nextTick } from 'vue'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
import SearchBox from './SearchBox.vue'
|
|
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: {
|
|
en: {
|
|
templateWidgets: {
|
|
sort: {
|
|
searchPlaceholder: 'Search...'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
describe('SearchBox', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.useFakeTimers()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
const createWrapper = (props = {}) => {
|
|
return mount(SearchBox, {
|
|
props: {
|
|
modelValue: '',
|
|
...props
|
|
},
|
|
global: {
|
|
plugins: [i18n]
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('debounced search functionality', () => {
|
|
it('should debounce search input by 300ms', async () => {
|
|
const wrapper = createWrapper()
|
|
const input = wrapper.find('input')
|
|
|
|
// Type search query
|
|
await input.setValue('test')
|
|
|
|
// Model should not update immediately
|
|
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
|
|
// Advance timers by 299ms (just before debounce delay)
|
|
vi.advanceTimersByTime(299)
|
|
await nextTick()
|
|
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
|
|
// Advance timers by 1ms more (reaching 300ms)
|
|
vi.advanceTimersByTime(1)
|
|
await nextTick()
|
|
|
|
// Model should now be updated
|
|
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['test'])
|
|
})
|
|
|
|
it('should reset debounce timer on each keystroke', async () => {
|
|
const wrapper = createWrapper()
|
|
const input = wrapper.find('input')
|
|
|
|
// Type first character
|
|
await input.setValue('t')
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
|
|
// Type second character (should reset timer)
|
|
await input.setValue('te')
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
|
|
// Type third character (should reset timer again)
|
|
await input.setValue('tes')
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
|
|
// Should not have emitted yet (only 200ms passed since last keystroke)
|
|
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
|
|
// Advance final 100ms to reach 300ms
|
|
vi.advanceTimersByTime(100)
|
|
await nextTick()
|
|
|
|
// Should now emit with final value
|
|
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['tes'])
|
|
})
|
|
|
|
it('should only emit final value after rapid typing', async () => {
|
|
const wrapper = createWrapper()
|
|
const input = wrapper.find('input')
|
|
|
|
// Simulate rapid typing
|
|
const searchTerms = ['s', 'se', 'sea', 'sear', 'searc', 'search']
|
|
for (const term of searchTerms) {
|
|
await input.setValue(term)
|
|
vi.advanceTimersByTime(50) // Less than debounce delay
|
|
}
|
|
|
|
// Should not have emitted yet
|
|
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
|
|
// Complete the debounce delay
|
|
vi.advanceTimersByTime(300)
|
|
await nextTick()
|
|
|
|
// Should emit only once with final value
|
|
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['search'])
|
|
})
|
|
|
|
describe('bidirectional model sync', () => {
|
|
it('should sync external model changes to internal state', async () => {
|
|
const wrapper = createWrapper({ modelValue: 'initial' })
|
|
const input = wrapper.find('input')
|
|
|
|
expect(input.element.value).toBe('initial')
|
|
|
|
// Update model externally
|
|
await wrapper.setProps({ modelValue: 'external update' })
|
|
await nextTick()
|
|
|
|
// Internal state should sync
|
|
expect(input.element.value).toBe('external update')
|
|
})
|
|
})
|
|
|
|
describe('placeholder', () => {
|
|
it('should use custom placeholder when provided', () => {
|
|
const wrapper = createWrapper({ placeholder: 'Custom search...' })
|
|
const input = wrapper.find('input')
|
|
|
|
expect(input.attributes('placeholder')).toBe('Custom search...')
|
|
expect(input.attributes('aria-label')).toBe('Custom search...')
|
|
})
|
|
|
|
it('should use default placeholder when not provided', () => {
|
|
const wrapper = createWrapper()
|
|
const input = wrapper.find('input')
|
|
|
|
expect(input.attributes('placeholder')).toBe('Search...')
|
|
expect(input.attributes('aria-label')).toBe('Search...')
|
|
})
|
|
})
|
|
|
|
describe('autofocus', () => {
|
|
it('should focus input when autofocus is true', async () => {
|
|
const wrapper = createWrapper({ autofocus: true })
|
|
await nextTick()
|
|
|
|
const input = wrapper.find('input')
|
|
const inputElement = input.element as HTMLInputElement
|
|
|
|
// Note: In JSDOM, focus() doesn't actually set document.activeElement
|
|
// We can only verify that the focus method exists and doesn't throw
|
|
expect(inputElement.focus).toBeDefined()
|
|
})
|
|
|
|
it('should not autofocus when autofocus is false', () => {
|
|
const wrapper = createWrapper({ autofocus: false })
|
|
const input = wrapper.find('input')
|
|
|
|
expect(document.activeElement).not.toBe(input.element)
|
|
})
|
|
})
|
|
|
|
describe('click to focus', () => {
|
|
it('should focus input when wrapper is clicked', async () => {
|
|
const wrapper = createWrapper()
|
|
const wrapperDiv = wrapper.find('[class*="flex"]')
|
|
|
|
await wrapperDiv.trigger('click')
|
|
await nextTick()
|
|
|
|
// Input should receive focus
|
|
const input = wrapper.find('input').element as HTMLInputElement
|
|
expect(input.focus).toBeDefined()
|
|
})
|
|
})
|
|
})
|
|
})
|