mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
351 lines
11 KiB
TypeScript
351 lines
11 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 { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
|
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()
|
|
vi.spyOn(useNodeBookmarkStore(), 'bookmarks', 'get').mockReturnValue([])
|
|
})
|
|
|
|
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 () => {
|
|
vi.spyOn(useNodeBookmarkStore(), 'bookmarks', 'get').mockReturnValue([
|
|
'placeholder'
|
|
])
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({
|
|
name: 'EssentialNode',
|
|
essentials_category: 'basic'
|
|
})
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper()
|
|
|
|
expect(wrapper.text()).toContain('Most relevant')
|
|
expect(wrapper.text()).toContain('Favorites')
|
|
expect(wrapper.text()).toContain('Essentials')
|
|
expect(wrapper.text()).toContain('Custom')
|
|
})
|
|
|
|
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 () => {
|
|
vi.spyOn(useNodeBookmarkStore(), 'bookmarks', 'get').mockReturnValue([
|
|
'placeholder'
|
|
])
|
|
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'])
|
|
})
|
|
|
|
describe('rootLabel wrapping', () => {
|
|
it('should wrap multiple roots under a parent when rootLabel is provided', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'loaders' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper({
|
|
hidePresets: true,
|
|
rootLabel: 'Extensions',
|
|
rootKey: 'custom'
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Extensions')
|
|
})
|
|
|
|
it('should auto-expand the synthetic root and keep it expanded when selecting a child', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'loaders' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper({
|
|
hidePresets: true,
|
|
rootLabel: 'Extensions',
|
|
rootKey: 'custom'
|
|
})
|
|
|
|
// Auto-expanded: children should be visible
|
|
expect(wrapper.text()).toContain('sampling')
|
|
expect(wrapper.text()).toContain('loaders')
|
|
|
|
// Select a child category
|
|
await clickCategory(wrapper, 'sampling')
|
|
await nextTick()
|
|
|
|
// Parent should stay expanded (children still visible)
|
|
expect(wrapper.text()).toContain('Extensions')
|
|
expect(wrapper.text()).toContain('sampling')
|
|
expect(wrapper.text()).toContain('loaders')
|
|
})
|
|
|
|
it('should prefix child keys with rootKey', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'loaders' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper({
|
|
hidePresets: true,
|
|
rootLabel: 'Extensions',
|
|
rootKey: 'custom'
|
|
})
|
|
|
|
await clickCategory(wrapper, 'sampling')
|
|
|
|
const emitted = wrapper.emitted('update:selectedCategory')!
|
|
expect(emitted[emitted.length - 1]).toEqual(['custom/sampling'])
|
|
})
|
|
|
|
it('should not wrap when there is only one root category', async () => {
|
|
useNodeDefStore().updateNodeDefs([
|
|
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
|
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' })
|
|
])
|
|
await nextTick()
|
|
|
|
const wrapper = await createWrapper({
|
|
hidePresets: true,
|
|
rootLabel: 'Extensions',
|
|
rootKey: 'custom'
|
|
})
|
|
|
|
// No wrapping — "Extensions" parent should not appear
|
|
expect(wrapper.text()).not.toContain('Extensions')
|
|
expect(wrapper.text()).toContain('sampling')
|
|
})
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|