mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Implement 11 Figma design discrepancies for the Node Library sidebar and V2 Node Search dialog, aligning the UI with the [Toolbox Figma design](https://www.figma.com/design/xMFxCziXJe6Denz4dpDGTq/Toolbox?node-id=2074-21394&m=dev). ## Changes - **What**: Sidebar: reorder tabs (All/Essentials/Blueprints), rename Custom→Blueprints, uppercase section headers, chevron-left of folder icon, bookmark-on-hover for node rows, filter dropdown with checkbox items, sort labels (Categorized/A-Z) with label-left/check-right layout, hide section headers in A-Z mode. Search dialog: expand filter chips from 3→6, add Recents and source categories to sidebar, remove "Filter by" label. Pull foundation V2 components from merged PR #8548. - **Dependencies**: Depends on #8987 (V2 Node Search) and #8548 (NodeLibrarySidebarTabV2) ## Review Focus - Filter dropdown (`filterOptions`) is UI-scaffolded but not yet wired to filtering logic (pending V2 integration) - "Recents" category currently returns frequency-based results as placeholder until a usage-tracking store is implemented - Pre-existing type errors from V2 PR dependencies not in the base commit (SearchBoxV2, usePerTabState, TextTicker, getProviderIcon, getLinkTypeColor, SidebarContainerKey) are expected and will resolve when rebased onto main after parent PRs land ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9085-feat-Node-Library-sidebar-and-V2-Search-dialog-Figma-design-improvements-30f6d73d36508175bf72d716f5904476) by [Unito](https://www.unito.io) --------- Co-authored-by: Yourz <crazilou@vip.qq.com> Co-authored-by: github-actions <github-actions@github.com>
266 lines
8.5 KiB
TypeScript
266 lines
8.5 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { nextTick } from 'vue'
|
|
|
|
import NodeSearchCategorySidebar from '@/components/searchbox/v2/NodeSearchCategorySidebar.vue'
|
|
import {
|
|
createMockNodeDef,
|
|
setupTestPinia,
|
|
testI18n
|
|
} from '@/components/searchbox/v2/__test__/testUtils'
|
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
|
|
|
vi.mock('@/platform/settings/settingStore', () => ({
|
|
useSettingStore: vi.fn(() => ({
|
|
get: vi.fn(() => undefined),
|
|
set: vi.fn()
|
|
}))
|
|
}))
|
|
|
|
describe('NodeSearchCategorySidebar', () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks()
|
|
setupTestPinia()
|
|
})
|
|
|
|
async function createWrapper(props = {}) {
|
|
const wrapper = mount(NodeSearchCategorySidebar, {
|
|
props: { selectedCategory: 'most-relevant', ...props },
|
|
global: { plugins: [testI18n] }
|
|
})
|
|
await nextTick()
|
|
return wrapper
|
|
}
|
|
|
|
async function clickCategory(
|
|
wrapper: ReturnType<typeof mount>,
|
|
text: string,
|
|
exact = false
|
|
) {
|
|
const btn = wrapper
|
|
.findAll('button')
|
|
.find((b) => (exact ? b.text().trim() === text : b.text().includes(text)))
|
|
expect(btn, `Expected to find a button with text "${text}"`).toBeDefined()
|
|
await btn!.trigger('click')
|
|
await nextTick()
|
|
}
|
|
|
|
describe('preset categories', () => {
|
|
it('should render all preset categories', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({
|
|
name: 'EssentialNode',
|
|
essentials_category: 'basic',
|
|
python_module: 'comfy_essentials'
|
|
})
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
expect(wrapper.text()).toContain('Most relevant')
|
|
expect(wrapper.text()).toContain('Recents')
|
|
expect(wrapper.text()).toContain('Favorites')
|
|
expect(wrapper.text()).toContain('Essentials')
|
|
expect(wrapper.text()).toContain('Blueprints')
|
|
expect(wrapper.text()).toContain('Partner')
|
|
expect(wrapper.text()).toContain('Comfy')
|
|
expect(wrapper.text()).toContain('Extensions')
|
|
})
|
|
|
|
it('should mark the selected preset category as selected', async () => {
|
|
const wrapper = await createWrapper({ selectedCategory: 'most-relevant' })
|
|
|
|
const mostRelevantBtn = wrapper.find(
|
|
'[data-testid="category-most-relevant"]'
|
|
)
|
|
|
|
expect(mostRelevantBtn.attributes('aria-current')).toBe('true')
|
|
})
|
|
|
|
it('should emit update:selectedCategory when preset is clicked', async () => {
|
|
const wrapper = await createWrapper({ selectedCategory: 'most-relevant' })
|
|
|
|
await clickCategory(wrapper, 'Favorites')
|
|
|
|
expect(wrapper.emitted('update:selectedCategory')).toBeTruthy()
|
|
expect(wrapper.emitted('update:selectedCategory')![0]).toEqual([
|
|
'favorites'
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('category tree', () => {
|
|
it('should render top-level categories from node definitions', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'loaders' }),
|
|
createMockNodeDef({ name: 'Node3', category: 'conditioning' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
expect(wrapper.text()).toContain('sampling')
|
|
expect(wrapper.text()).toContain('loaders')
|
|
expect(wrapper.text()).toContain('conditioning')
|
|
})
|
|
|
|
it('should emit update:selectedCategory when category is clicked', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
await clickCategory(wrapper, 'sampling')
|
|
|
|
expect(wrapper.emitted('update:selectedCategory')![0]).toEqual([
|
|
'sampling'
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('expand/collapse functionality', () => {
|
|
it('should expand category when clicked and show subcategories', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
|
createMockNodeDef({ name: 'Node3', category: 'sampling/basic' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
expect(wrapper.text()).not.toContain('advanced')
|
|
|
|
await clickCategory(wrapper, 'sampling')
|
|
|
|
expect(wrapper.text()).toContain('advanced')
|
|
expect(wrapper.text()).toContain('basic')
|
|
})
|
|
|
|
it('should collapse sibling category when another is expanded', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
|
createMockNodeDef({ name: 'Node3', category: 'image' }),
|
|
createMockNodeDef({ name: 'Node4', category: 'image/upscale' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
// Expand sampling
|
|
await clickCategory(wrapper, 'sampling', true)
|
|
expect(wrapper.text()).toContain('advanced')
|
|
|
|
// Expand image — sampling should collapse
|
|
await clickCategory(wrapper, 'image', true)
|
|
|
|
expect(wrapper.text()).toContain('upscale')
|
|
expect(wrapper.text()).not.toContain('advanced')
|
|
})
|
|
|
|
it('should emit update:selectedCategory when subcategory is clicked', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
// Expand sampling category
|
|
await clickCategory(wrapper, 'sampling', true)
|
|
|
|
// Click on advanced subcategory
|
|
await clickCategory(wrapper, 'advanced')
|
|
|
|
const emitted = wrapper.emitted('update:selectedCategory')!
|
|
expect(emitted[emitted.length - 1]).toEqual(['sampling/advanced'])
|
|
})
|
|
})
|
|
|
|
describe('category selection highlighting', () => {
|
|
it('should mark selected top-level category as selected', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper({ selectedCategory: 'sampling' })
|
|
|
|
expect(
|
|
wrapper
|
|
.find('[data-testid="category-sampling"]')
|
|
.attributes('aria-current')
|
|
).toBe('true')
|
|
})
|
|
|
|
it('should emit selected subcategory when expanded', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper({ selectedCategory: 'most-relevant' })
|
|
|
|
// Expand and click subcategory
|
|
await clickCategory(wrapper, 'sampling', true)
|
|
await clickCategory(wrapper, 'advanced')
|
|
|
|
const emitted = wrapper.emitted('update:selectedCategory')!
|
|
expect(emitted[emitted.length - 1]).toEqual(['sampling/advanced'])
|
|
})
|
|
})
|
|
|
|
it('should support deeply nested categories (3+ levels)', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'api' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'api/image' }),
|
|
createMockNodeDef({ name: 'Node3', category: 'api/image/BFL' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
// Only top-level visible initially
|
|
expect(wrapper.text()).toContain('api')
|
|
expect(wrapper.text()).not.toContain('image')
|
|
expect(wrapper.text()).not.toContain('BFL')
|
|
|
|
// Expand api
|
|
await clickCategory(wrapper, 'api', true)
|
|
|
|
expect(wrapper.text()).toContain('image')
|
|
expect(wrapper.text()).not.toContain('BFL')
|
|
|
|
// Expand image
|
|
await clickCategory(wrapper, 'image', true)
|
|
|
|
expect(wrapper.text()).toContain('BFL')
|
|
|
|
// Click BFL and verify emission
|
|
await clickCategory(wrapper, 'BFL', true)
|
|
|
|
const emitted = wrapper.emitted('update:selectedCategory')!
|
|
expect(emitted[emitted.length - 1]).toEqual(['api/image/BFL'])
|
|
})
|
|
|
|
it('should emit category without root/ prefix', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
await clickCategory(wrapper, 'sampling')
|
|
|
|
expect(wrapper.emitted('update:selectedCategory')![0][0]).toBe('sampling')
|
|
})
|
|
})
|