V2 Node Search (+ hidden Node Library changes) (#8987)

## Summary

Redesigned node search with categories

## Changes

- **What**: Adds a v2 search component, leaving the existing
implementation untouched
- It also brings onboard the incomplete node library & preview changes,
disabled and behind a hidden setting
- **Breaking**: Changes the 'default' value of the node search setting
to v2, adding v1 (legacy) as an option

## Screenshots (if applicable)




https://github.com/user-attachments/assets/2ab797df-58f0-48e8-8b20-2a1809e3735f

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8987-V2-Node-Search-hidden-Node-Library-changes-30c6d73d36508160902bcb92553f147c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Yourz <crazilou@vip.qq.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
pythongosssss
2026-02-20 09:10:03 +00:00
committed by GitHub
parent 8f5cdead73
commit 6902e38e6a
183 changed files with 7972 additions and 127 deletions

View File

@@ -0,0 +1,207 @@
import { flushPromises, mount } from '@vue/test-utils'
import { nextTick, ref } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import EssentialNodesPanel from './EssentialNodesPanel.vue'
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: vi.fn().mockReturnValue('left')
})
}))
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
useNodeDragToCanvas: () => ({
startDrag: vi.fn(),
handleNativeDrop: vi.fn(),
cancelDrag: vi.fn()
})
}))
vi.mock('@/components/node/NodePreviewCard.vue', () => ({
default: { template: '<div />' }
}))
describe('EssentialNodesPanel', () => {
function createMockNode(
name: string
): RenderedTreeExplorerNode<ComfyNodeDefImpl> {
return {
key: `node-${name}`,
label: name,
icon: 'icon-[comfy--node]',
type: 'node',
totalLeaves: 1,
data: {
name,
display_name: name
} as ComfyNodeDefImpl
}
}
function createMockFolder(
name: string,
children: RenderedTreeExplorerNode<ComfyNodeDefImpl>[]
): RenderedTreeExplorerNode<ComfyNodeDefImpl> {
return {
key: `folder-${name}`,
label: name,
icon: 'icon-[lucide--folder]',
type: 'folder',
totalLeaves: children.length,
children
}
}
function createMockRoot(): RenderedTreeExplorerNode<ComfyNodeDefImpl> {
return {
key: 'root',
label: 'Root',
icon: '',
type: 'folder',
totalLeaves: 6,
children: [
createMockFolder('images', [
createMockNode('LoadImage'),
createMockNode('SaveImage')
]),
createMockFolder('video', [
createMockNode('LoadVideo'),
createMockNode('SaveVideo')
]),
createMockFolder('audio', [
createMockNode('LoadAudio'),
createMockNode('SaveAudio')
])
]
}
}
function mountComponent(
root = createMockRoot(),
expandedKeys: string[] = []
) {
const WrapperComponent = {
template: `<EssentialNodesPanel :root="root" v-model:expandedKeys="keys" />`,
components: { EssentialNodesPanel },
setup() {
const keys = ref(expandedKeys)
return { root, keys }
}
}
return mount(WrapperComponent, {
global: {
stubs: {
Teleport: true,
TabsContent: {
template: '<div class="tabs-content"><slot /></div>'
},
CollapsibleRoot: {
template:
'<div class="collapsible-root" :data-state="open ? \'open\' : \'closed\'"><slot /></div>',
props: ['open'],
emits: ['update:open']
},
CollapsibleTrigger: {
template:
'<button class="collapsible-trigger" @click="$emit(\'click\')"><slot /></button>'
},
CollapsibleContent: {
template: '<div class="collapsible-content"><slot /></div>'
}
}
}
})
}
describe('folder rendering', () => {
it('should render all top-level folders', () => {
const wrapper = mountComponent()
const triggers = wrapper.findAll('.collapsible-trigger')
expect(triggers).toHaveLength(3)
})
it('should display folder labels', () => {
const wrapper = mountComponent()
expect(wrapper.text()).toContain('images')
expect(wrapper.text()).toContain('video')
expect(wrapper.text()).toContain('audio')
})
})
describe('default expansion', () => {
it('should expand all folders by default when expandedKeys is empty', async () => {
const wrapper = mountComponent(createMockRoot(), [])
await nextTick()
await flushPromises()
await nextTick()
const roots = wrapper.findAll('.collapsible-root')
expect(roots[0].attributes('data-state')).toBe('open')
expect(roots[1].attributes('data-state')).toBe('open')
expect(roots[2].attributes('data-state')).toBe('open')
})
it('should respect provided expandedKeys', async () => {
const wrapper = mountComponent(createMockRoot(), ['folder-audio'])
await nextTick()
const roots = wrapper.findAll('.collapsible-root')
expect(roots[0].attributes('data-state')).toBe('closed')
expect(roots[1].attributes('data-state')).toBe('closed')
expect(roots[2].attributes('data-state')).toBe('open')
})
it('should expand all provided keys', async () => {
const wrapper = mountComponent(createMockRoot(), [
'folder-images',
'folder-video',
'folder-audio'
])
await nextTick()
const roots = wrapper.findAll('.collapsible-root')
expect(roots[0].attributes('data-state')).toBe('open')
expect(roots[1].attributes('data-state')).toBe('open')
expect(roots[2].attributes('data-state')).toBe('open')
})
})
describe('with single folder', () => {
it('should expand only one folder when there is only one', async () => {
const root: RenderedTreeExplorerNode<ComfyNodeDefImpl> = {
key: 'root',
label: 'Root',
icon: '',
type: 'folder',
totalLeaves: 2,
children: [
createMockFolder('images', [
createMockNode('LoadImage'),
createMockNode('SaveImage')
])
]
}
const wrapper = mountComponent(root, [])
await nextTick()
await flushPromises()
await nextTick()
const roots = wrapper.findAll('.collapsible-root')
expect(roots).toHaveLength(1)
expect(roots[0].attributes('data-state')).toBe('open')
})
})
describe('node cards', () => {
it('should render node cards for each node in expanded folders', () => {
const wrapper = mountComponent(createMockRoot(), ['folder-images'])
const cards = wrapper.findAllComponents({ name: 'EssentialNodeCard' })
expect(cards.length).toBeGreaterThanOrEqual(2)
})
})
})