mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
expand and simplify tests
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { render, screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import NodeSearchCategorySidebar from '@/components/searchbox/v2/NodeSearchCategorySidebar.vue'
|
||||
import {
|
||||
@@ -11,16 +10,12 @@ import {
|
||||
} from '@/components/searchbox/v2/__test__/testUtils'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'Comfy.NodeLibrary.Bookmarks.V2') return []
|
||||
if (key === 'Comfy.NodeLibrary.BookmarksCustomization') return {}
|
||||
return undefined
|
||||
}),
|
||||
set: vi.fn()
|
||||
}))
|
||||
}))
|
||||
type SidebarProps = Partial<{
|
||||
selectedCategory: string
|
||||
hidePresets: boolean
|
||||
rootLabel: string
|
||||
rootKey: string
|
||||
}>
|
||||
|
||||
describe('NodeSearchCategorySidebar', () => {
|
||||
beforeEach(() => {
|
||||
@@ -28,35 +23,30 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
setupTestPinia()
|
||||
})
|
||||
|
||||
async function createRender(props = {}) {
|
||||
function createRender(props: SidebarProps = {}) {
|
||||
const user = userEvent.setup()
|
||||
const onUpdateSelectedCategory = vi.fn()
|
||||
const baseProps = { selectedCategory: 'most-relevant', ...props }
|
||||
|
||||
let currentProps = { ...baseProps }
|
||||
let rerenderFn: (
|
||||
p: typeof baseProps & Record<string, unknown>
|
||||
) => void = () => {}
|
||||
|
||||
function makeProps(overrides = {}) {
|
||||
const merged = { ...currentProps, ...overrides }
|
||||
return {
|
||||
...merged,
|
||||
'onUpdate:selectedCategory': (val: string) => {
|
||||
onUpdateSelectedCategory(val)
|
||||
currentProps = { ...currentProps, selectedCategory: val }
|
||||
rerenderFn(makeProps())
|
||||
}
|
||||
}
|
||||
const onUpdateSelectedCategory = vi.fn<(value: string) => void>()
|
||||
const initialProps: SidebarProps & { selectedCategory: string } = {
|
||||
selectedCategory: 'most-relevant',
|
||||
...props
|
||||
}
|
||||
|
||||
const result = render(NodeSearchCategorySidebar, {
|
||||
props: makeProps(),
|
||||
props: {
|
||||
...initialProps,
|
||||
'onUpdate:selectedCategory': onUpdateSelectedCategory
|
||||
},
|
||||
global: { plugins: [testI18n] }
|
||||
})
|
||||
rerenderFn = (p) => result.rerender(p)
|
||||
await nextTick()
|
||||
return { user, onUpdateSelectedCategory }
|
||||
|
||||
const rerender = (overrides: SidebarProps) =>
|
||||
result.rerender({
|
||||
...initialProps,
|
||||
...overrides,
|
||||
'onUpdate:selectedCategory': onUpdateSelectedCategory
|
||||
})
|
||||
|
||||
return { user, onUpdateSelectedCategory, rerender }
|
||||
}
|
||||
|
||||
async function clickCategory(
|
||||
@@ -73,18 +63,17 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
)
|
||||
expect(btn, `Expected to find a button with text "${text}"`).toBeDefined()
|
||||
await user.click(btn!)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
describe('preset categories', () => {
|
||||
it('should render Most relevant preset category', async () => {
|
||||
await createRender()
|
||||
it('should render Most relevant preset category', () => {
|
||||
createRender()
|
||||
|
||||
expect(screen.getByText('Most relevant')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should mark the selected preset category as selected', async () => {
|
||||
await createRender({ selectedCategory: 'most-relevant' })
|
||||
it('should mark the selected preset category as selected', () => {
|
||||
createRender({ selectedCategory: 'most-relevant' })
|
||||
|
||||
expect(screen.getByTestId('category-most-relevant')).toHaveAttribute(
|
||||
'aria-current',
|
||||
@@ -93,15 +82,15 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
})
|
||||
|
||||
it('should emit update:selectedCategory when preset is clicked', async () => {
|
||||
const { user, onUpdateSelectedCategory } = await createRender({
|
||||
const { user, onUpdateSelectedCategory } = createRender({
|
||||
selectedCategory: 'most-relevant'
|
||||
})
|
||||
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
await screen.findByText('sampling')
|
||||
await clickCategory(user, 'sampling')
|
||||
|
||||
expect(onUpdateSelectedCategory).toHaveBeenCalledWith('sampling')
|
||||
@@ -109,15 +98,14 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
})
|
||||
|
||||
describe('category tree', () => {
|
||||
it('should render top-level categories from node definitions', async () => {
|
||||
it('should render top-level categories from node definitions', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'loaders' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'conditioning' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
await createRender()
|
||||
createRender()
|
||||
|
||||
expect(screen.getByText('sampling')).toBeInTheDocument()
|
||||
expect(screen.getByText('loaders')).toBeInTheDocument()
|
||||
@@ -128,9 +116,8 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user, onUpdateSelectedCategory } = await createRender()
|
||||
const { user, onUpdateSelectedCategory } = createRender()
|
||||
|
||||
await clickCategory(user, 'sampling')
|
||||
|
||||
@@ -146,18 +133,15 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node3', category: 'sampling/basic' }),
|
||||
createMockNodeDef({ name: 'Node4', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
|
||||
expect(screen.queryByText('advanced')).not.toBeInTheDocument()
|
||||
|
||||
await clickCategory(user, 'sampling')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
expect(screen.getByText('basic')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
expect(screen.getByText('basic')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should collapse sibling category when another is expanded', async () => {
|
||||
@@ -167,15 +151,12 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node3', category: 'image' }),
|
||||
createMockNodeDef({ name: 'Node4', category: 'image/upscale' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
|
||||
// Expand sampling
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
|
||||
// Expand image — sampling should collapse
|
||||
await clickCategory(user, 'image', true)
|
||||
@@ -192,15 +173,12 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user, onUpdateSelectedCategory } = await createRender()
|
||||
const { user, onUpdateSelectedCategory } = createRender()
|
||||
|
||||
// Expand sampling category
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
|
||||
// Click on advanced subcategory
|
||||
await clickCategory(user, 'advanced')
|
||||
@@ -211,13 +189,12 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
})
|
||||
|
||||
describe('category selection highlighting', () => {
|
||||
it('should mark selected top-level category as selected', async () => {
|
||||
it('should mark selected top-level category as selected', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
await createRender({ selectedCategory: 'sampling' })
|
||||
createRender({ selectedCategory: 'sampling' })
|
||||
|
||||
expect(screen.getByTestId('category-sampling')).toHaveAttribute(
|
||||
'aria-current',
|
||||
@@ -231,17 +208,14 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user, onUpdateSelectedCategory } = await createRender({
|
||||
const { user, onUpdateSelectedCategory } = createRender({
|
||||
selectedCategory: 'most-relevant'
|
||||
})
|
||||
|
||||
// Expand and click subcategory
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
await clickCategory(user, 'advanced')
|
||||
|
||||
const calls = onUpdateSelectedCategory.mock.calls
|
||||
@@ -250,8 +224,8 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
})
|
||||
|
||||
describe('hidePresets prop', () => {
|
||||
it('should hide preset categories when hidePresets is true', async () => {
|
||||
await createRender({ hidePresets: true })
|
||||
it('should hide preset categories when hidePresets is true', () => {
|
||||
createRender({ hidePresets: true })
|
||||
|
||||
expect(screen.queryByText('Most relevant')).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -261,21 +235,88 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user, onUpdateSelectedCategory } = await createRender()
|
||||
const { user, onUpdateSelectedCategory } = createRender()
|
||||
|
||||
await clickCategory(user, 'sampling')
|
||||
|
||||
expect(onUpdateSelectedCategory).toHaveBeenCalledWith('sampling')
|
||||
})
|
||||
|
||||
it('should emit autoExpand when there is a single root category', async () => {
|
||||
describe('rootLabel wrapping', () => {
|
||||
it('should wrap multiple top-level categories under rootLabel key', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'N1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'N2', category: 'loaders' })
|
||||
])
|
||||
|
||||
const { user, onUpdateSelectedCategory } = createRender({
|
||||
rootLabel: 'Extensions',
|
||||
rootKey: 'custom'
|
||||
})
|
||||
|
||||
expect(screen.getByText('Extensions')).toBeInTheDocument()
|
||||
|
||||
// Expand the wrapper root
|
||||
const customBtn = screen.getByTestId('category-custom')
|
||||
expect(customBtn).toBeInTheDocument()
|
||||
await user.click(customBtn)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('sampling')).toBeInTheDocument()
|
||||
expect(screen.getByText('loaders')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Subcategories should be prefixed with the root key
|
||||
expect(screen.getByTestId('category-custom/sampling')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTestId('category-custom/sampling'))
|
||||
const calls = onUpdateSelectedCategory.mock.calls
|
||||
expect(calls[calls.length - 1]).toEqual(['custom/sampling'])
|
||||
})
|
||||
|
||||
it('should derive root key from rootLabel when rootKey is not provided', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'N1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'N2', category: 'loaders' })
|
||||
])
|
||||
|
||||
const { user, onUpdateSelectedCategory } = createRender({
|
||||
rootLabel: 'Custom'
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('category-custom'))
|
||||
await user.click(await screen.findByTestId('category-custom/sampling'))
|
||||
|
||||
const calls = onUpdateSelectedCategory.mock.calls
|
||||
expect(calls[calls.length - 1]).toEqual(['custom/sampling'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('external selectedCategory updates', () => {
|
||||
it('should update expanded state when selectedCategory changes externally', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
|
||||
const { rerender } = createRender({
|
||||
selectedCategory: 'most-relevant'
|
||||
})
|
||||
|
||||
expect(screen.queryByText('advanced')).not.toBeInTheDocument()
|
||||
|
||||
await rerender({ selectedCategory: 'sampling' })
|
||||
|
||||
await screen.findByText('advanced')
|
||||
})
|
||||
})
|
||||
|
||||
it('should emit autoExpand when there is a single root category', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'api' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'api/image' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const onAutoExpand = vi.fn()
|
||||
render(NodeSearchCategorySidebar, {
|
||||
@@ -285,7 +326,6 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
},
|
||||
global: { plugins: [testI18n] }
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(onAutoExpand).toHaveBeenCalledWith('api')
|
||||
})
|
||||
@@ -296,9 +336,8 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'api/image' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'api/image/BFL' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user, onUpdateSelectedCategory } = await createRender()
|
||||
const { user, onUpdateSelectedCategory } = createRender()
|
||||
|
||||
// Only top-level visible initially
|
||||
expect(screen.getByText('api')).toBeInTheDocument()
|
||||
@@ -307,16 +346,12 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
|
||||
// Expand api
|
||||
await clickCategory(user, 'api', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('image')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('image')
|
||||
expect(screen.queryByText('BFL')).not.toBeInTheDocument()
|
||||
|
||||
// Expand image
|
||||
await clickCategory(user, 'image', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('BFL')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('BFL')
|
||||
|
||||
// Click BFL and verify emission
|
||||
await clickCategory(user, 'BFL', true)
|
||||
@@ -332,16 +367,14 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user, onUpdateSelectedCategory } = await createRender()
|
||||
const { user, onUpdateSelectedCategory } = createRender()
|
||||
|
||||
expect(screen.queryByText('advanced')).not.toBeInTheDocument()
|
||||
|
||||
const samplingBtn = screen.getByTestId('category-sampling')
|
||||
samplingBtn.focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
await nextTick()
|
||||
|
||||
// Should have emitted select for sampling, expanding it
|
||||
expect(onUpdateSelectedCategory).toHaveBeenCalledWith('sampling')
|
||||
@@ -353,23 +386,21 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
|
||||
// First expand sampling by clicking
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
|
||||
const samplingBtn = screen.getByTestId('category-sampling')
|
||||
samplingBtn.focus()
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
await nextTick()
|
||||
|
||||
// Collapse toggles internal state; children should be hidden
|
||||
expect(screen.queryByText('advanced')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('advanced')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should focus first child on ArrowRight when already expanded', async () => {
|
||||
@@ -378,20 +409,18 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
|
||||
const samplingBtn = screen.getByTestId('category-sampling')
|
||||
samplingBtn.focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getByTestId('category-sampling/advanced')).toHaveFocus()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('category-sampling/advanced')).toHaveFocus()
|
||||
})
|
||||
})
|
||||
|
||||
it('should focus parent on ArrowLeft from a leaf or collapsed node', async () => {
|
||||
@@ -400,19 +429,17 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('advanced')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('advanced')
|
||||
|
||||
screen.getByTestId('category-sampling/advanced').focus()
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getByTestId('category-sampling')).toHaveFocus()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('category-sampling')).toHaveFocus()
|
||||
})
|
||||
})
|
||||
|
||||
it('should collapse sampling on ArrowLeft, not just its expanded child', async () => {
|
||||
@@ -428,33 +455,28 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
}),
|
||||
createMockNodeDef({ name: 'Node4', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
|
||||
// Step 1: Expand sampling
|
||||
await clickCategory(user, 'sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('custom_sampling')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('custom_sampling')
|
||||
|
||||
// Step 2: Expand custom_sampling
|
||||
await clickCategory(user, 'custom_sampling', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('child')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('child')
|
||||
|
||||
// Step 3: Navigate back to sampling (keyboard focus only)
|
||||
const samplingBtn = screen.getByTestId('category-sampling')
|
||||
samplingBtn.focus()
|
||||
await nextTick()
|
||||
|
||||
// Step 4: Press left on sampling
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
await nextTick()
|
||||
|
||||
// Sampling should collapse entirely — custom_sampling should not be visible
|
||||
expect(screen.queryByText('custom_sampling')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('custom_sampling')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should collapse 4-deep tree to parent of level 2 on ArrowLeft', async () => {
|
||||
@@ -465,50 +487,42 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
createMockNodeDef({ name: 'N4', category: 'a/b/c/d' }),
|
||||
createMockNodeDef({ name: 'N5', category: 'other' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await createRender()
|
||||
const { user } = createRender()
|
||||
|
||||
// Expand a → a/b → a/b/c
|
||||
await clickCategory(user, 'a', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('b')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('b')
|
||||
|
||||
await clickCategory(user, 'b', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('c')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('c')
|
||||
|
||||
await clickCategory(user, 'c', true)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('d')).toBeInTheDocument()
|
||||
})
|
||||
await screen.findByText('d')
|
||||
|
||||
// Focus level 2 (a/b) and press ArrowLeft
|
||||
const bBtn = screen.getByTestId('category-a/b')
|
||||
bBtn.focus()
|
||||
await nextTick()
|
||||
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
await nextTick()
|
||||
|
||||
// Level 2 and below should collapse, but level 1 (a) stays expanded
|
||||
// so 'b' is still visible but 'c' and 'd' are not
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('c')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('b')).toBeInTheDocument()
|
||||
expect(screen.queryByText('c')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('d')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should set aria-expanded on tree nodes with children', async () => {
|
||||
it('should set aria-expanded on tree nodes with children', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
await createRender()
|
||||
createRender()
|
||||
|
||||
expect(screen.getByTestId('category-sampling')).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { render, screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import NodeSearchContent from '@/components/searchbox/v2/NodeSearchContent.vue'
|
||||
import {
|
||||
@@ -9,33 +8,28 @@ import {
|
||||
setupTestPinia,
|
||||
testI18n
|
||||
} from '@/components/searchbox/v2/__test__/testUtils'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import { NodeSourceType } from '@/types/nodeSource'
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'Comfy.NodeLibrary.Bookmarks.V2') return []
|
||||
if (key === 'Comfy.NodeLibrary.BookmarksCustomization') return {}
|
||||
return undefined
|
||||
}),
|
||||
set: vi.fn()
|
||||
}))
|
||||
}))
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
describe('NodeSearchContent', () => {
|
||||
beforeEach(() => {
|
||||
setupTestPinia()
|
||||
vi.restoreAllMocks()
|
||||
const settings = useSettingStore()
|
||||
settings.settingValues['Comfy.NodeLibrary.Bookmarks.V2'] = []
|
||||
settings.settingValues['Comfy.NodeLibrary.BookmarksCustomization'] = {}
|
||||
})
|
||||
|
||||
async function renderComponent(props = {}) {
|
||||
function renderComponent(props = {}) {
|
||||
const user = userEvent.setup()
|
||||
const onAddNode = vi.fn()
|
||||
const onHoverNode = vi.fn()
|
||||
const onRemoveFilter = vi.fn()
|
||||
const onRemoveFilter =
|
||||
vi.fn<(f: FuseFilterWithValue<ComfyNodeDefImpl, string>) => void>()
|
||||
const onAddFilter = vi.fn()
|
||||
render(NodeSearchContent, {
|
||||
props: {
|
||||
@@ -63,7 +57,6 @@ describe('NodeSearchContent', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
return { user, onAddNode, onHoverNode, onRemoveFilter, onAddFilter }
|
||||
}
|
||||
|
||||
@@ -96,9 +89,8 @@ describe('NodeSearchContent', () => {
|
||||
) {
|
||||
useNodeDefStore().updateNodeDefs(nodes.map(createMockNodeDef))
|
||||
mockBookmarks(true, ['placeholder'])
|
||||
const result = await renderComponent()
|
||||
const result = renderComponent()
|
||||
await clickFilterBarButton(result.user, 'Bookmarked')
|
||||
await nextTick()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -116,11 +108,13 @@ describe('NodeSearchContent', () => {
|
||||
useNodeDefStore().nodeDefsByName['FrequentNode']
|
||||
])
|
||||
|
||||
await renderComponent()
|
||||
renderComponent()
|
||||
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Frequent Node')
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Frequent Node')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show only bookmarked nodes when Favorites is selected', async () => {
|
||||
@@ -139,13 +133,14 @@ describe('NodeSearchContent', () => {
|
||||
['BookmarkedNode']
|
||||
)
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Bookmarked')
|
||||
await nextTick()
|
||||
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Bookmarked')
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Bookmarked')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show empty state when no bookmarks exist', async () => {
|
||||
@@ -154,11 +149,10 @@ describe('NodeSearchContent', () => {
|
||||
])
|
||||
mockBookmarks(false, ['placeholder'])
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Bookmarked')
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getByText('No results')).toBeInTheDocument()
|
||||
expect(await screen.findByText('No Results')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show only CustomNodes when Extensions is selected', async () => {
|
||||
@@ -174,7 +168,6 @@ describe('NodeSearchContent', () => {
|
||||
python_module: 'custom_nodes.my_extension'
|
||||
})
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
expect(useNodeDefStore().nodeDefsByName['CoreNode'].nodeSource.type).toBe(
|
||||
NodeSourceType.Core
|
||||
@@ -183,16 +176,17 @@ describe('NodeSearchContent', () => {
|
||||
useNodeDefStore().nodeDefsByName['CustomNode'].nodeSource.type
|
||||
).toBe(NodeSourceType.CustomNodes)
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Extensions')
|
||||
await nextTick()
|
||||
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Custom Node')
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Custom Node')
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide Essentials filter button when no essential nodes exist', async () => {
|
||||
it('should hide Essentials filter button when no essential nodes exist', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'RegularNode',
|
||||
@@ -200,7 +194,7 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
])
|
||||
|
||||
await renderComponent()
|
||||
renderComponent()
|
||||
const texts = screen
|
||||
.getAllByRole('button')
|
||||
.map((b) => b.textContent?.trim())
|
||||
@@ -219,15 +213,15 @@ describe('NodeSearchContent', () => {
|
||||
display_name: 'Regular Node'
|
||||
})
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Essentials')
|
||||
await nextTick()
|
||||
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Essential Node')
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Essential Node')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show only API nodes when Partner Nodes filter is active', async () => {
|
||||
@@ -243,13 +237,14 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
])
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Partner')
|
||||
await nextTick()
|
||||
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('API Node')
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('API Node')
|
||||
})
|
||||
})
|
||||
|
||||
it('should toggle filter off when clicking the active filter button again', async () => {
|
||||
@@ -265,22 +260,23 @@ describe('NodeSearchContent', () => {
|
||||
python_module: 'custom_nodes.my_extension'
|
||||
})
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
vi.spyOn(useNodeFrequencyStore(), 'topNodeDefs', 'get').mockReturnValue([
|
||||
useNodeDefStore().nodeDefsByName['CoreNode'],
|
||||
useNodeDefStore().nodeDefsByName['CustomNode']
|
||||
])
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
|
||||
await clickFilterBarButton(user, 'Extensions')
|
||||
await nextTick()
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(1)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(1)
|
||||
})
|
||||
|
||||
await clickFilterBarButton(user, 'Extensions')
|
||||
await nextTick()
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(2)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should include subcategory nodes when parent category is selected', async () => {
|
||||
@@ -302,12 +298,13 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
])
|
||||
|
||||
const { user } = await renderComponent()
|
||||
await user.click(screen.getByTestId('category-sampling'))
|
||||
await nextTick()
|
||||
const { user } = renderComponent()
|
||||
await user.click(await screen.findByTestId('category-sampling'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(2)
|
||||
})
|
||||
const texts = screen.getAllByTestId('node-item').map((i) => i.textContent)
|
||||
expect(texts).toHaveLength(2)
|
||||
expect(texts).toContain('KSampler')
|
||||
expect(texts).toContain('KSampler Advanced')
|
||||
})
|
||||
@@ -328,20 +325,22 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
])
|
||||
|
||||
const { user } = await renderComponent()
|
||||
await user.click(screen.getByTestId('category-sampling'))
|
||||
await nextTick()
|
||||
const { user } = renderComponent()
|
||||
await user.click(await screen.findByTestId('category-sampling'))
|
||||
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(1)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('node-item')).toHaveLength(1)
|
||||
})
|
||||
|
||||
const input = screen.getByRole('combobox')
|
||||
await user.type(input, 'Load')
|
||||
await nextTick()
|
||||
|
||||
const texts = screen
|
||||
.queryAllByTestId('node-item')
|
||||
.map((i) => i.textContent)
|
||||
expect(texts.some((t) => t?.includes('Load Checkpoint'))).toBe(false)
|
||||
await waitFor(() => {
|
||||
const texts = screen
|
||||
.queryAllByTestId('node-item')
|
||||
.map((i) => i.textContent)
|
||||
expect(texts.some((t) => t?.includes('Load Checkpoint'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should preserve search query when category changes', async () => {
|
||||
@@ -350,15 +349,13 @@ describe('NodeSearchContent', () => {
|
||||
])
|
||||
mockBookmarks(true, ['placeholder'])
|
||||
|
||||
const { user } = await renderComponent()
|
||||
const { user } = renderComponent()
|
||||
|
||||
const input = screen.getByRole('combobox')
|
||||
await user.type(input, 'test query')
|
||||
await nextTick()
|
||||
expect(input).toHaveValue('test query')
|
||||
|
||||
await clickFilterBarButton(user, 'Bookmarked')
|
||||
await nextTick()
|
||||
expect(input).toHaveValue('test query')
|
||||
})
|
||||
|
||||
@@ -371,18 +368,20 @@ describe('NodeSearchContent', () => {
|
||||
const input = screen.getByRole('combobox')
|
||||
await user.click(input)
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await nextTick()
|
||||
expect(screen.getAllByTestId('result-item')[1]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('result-item')[1]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
await user.type(input, 'Node')
|
||||
await nextTick()
|
||||
expect(screen.getAllByTestId('result-item')[0]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('result-item')[0]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should reset selected index when category changes', async () => {
|
||||
@@ -393,17 +392,16 @@ describe('NodeSearchContent', () => {
|
||||
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await nextTick()
|
||||
|
||||
await clickFilterBarButton(user, 'Bookmarked')
|
||||
await nextTick()
|
||||
await clickFilterBarButton(user, 'Bookmarked')
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getAllByTestId('result-item')[0]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('result-item')[0]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -424,24 +422,19 @@ describe('NodeSearchContent', () => {
|
||||
expect(selectedIndex()).toBe(0)
|
||||
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await nextTick()
|
||||
expect(selectedIndex()).toBe(1)
|
||||
await waitFor(() => expect(selectedIndex()).toBe(1))
|
||||
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await nextTick()
|
||||
expect(selectedIndex()).toBe(2)
|
||||
await waitFor(() => expect(selectedIndex()).toBe(2))
|
||||
|
||||
await user.keyboard('{ArrowUp}')
|
||||
await nextTick()
|
||||
expect(selectedIndex()).toBe(1)
|
||||
await waitFor(() => expect(selectedIndex()).toBe(1))
|
||||
|
||||
// Navigate to first, then try going above — should clamp
|
||||
await user.keyboard('{ArrowUp}')
|
||||
await nextTick()
|
||||
expect(selectedIndex()).toBe(0)
|
||||
await waitFor(() => expect(selectedIndex()).toBe(0))
|
||||
|
||||
await user.keyboard('{ArrowUp}')
|
||||
await nextTick()
|
||||
expect(selectedIndex()).toBe(0)
|
||||
})
|
||||
|
||||
@@ -452,7 +445,6 @@ describe('NodeSearchContent', () => {
|
||||
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.keyboard('{Enter}')
|
||||
await nextTick()
|
||||
|
||||
expect(onAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'TestNode' })
|
||||
@@ -467,9 +459,10 @@ describe('NodeSearchContent', () => {
|
||||
|
||||
const results = screen.getAllByTestId('result-item')
|
||||
await user.hover(results[1])
|
||||
await nextTick()
|
||||
|
||||
expect(results[1]).toHaveAttribute('aria-selected', 'true')
|
||||
await waitFor(() => {
|
||||
expect(results[1]).toHaveAttribute('aria-selected', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
it('should add node on click', async () => {
|
||||
@@ -478,7 +471,6 @@ describe('NodeSearchContent', () => {
|
||||
])
|
||||
|
||||
await user.click(screen.getAllByTestId('result-item')[0])
|
||||
await nextTick()
|
||||
|
||||
expect(onAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'TestNode' }),
|
||||
@@ -496,21 +488,23 @@ describe('NodeSearchContent', () => {
|
||||
const results = screen.getAllByTestId('result-item')
|
||||
results[0].focus()
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getAllByTestId('result-item')[1]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('result-item')[1]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
screen.getAllByTestId('result-item')[1].focus()
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getAllByTestId('result-item')[2]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('result-item')[2]).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should select node with Enter from a focused result item', async () => {
|
||||
@@ -520,7 +514,6 @@ describe('NodeSearchContent', () => {
|
||||
|
||||
screen.getAllByTestId('result-item')[0].focus()
|
||||
await user.keyboard('{Enter}')
|
||||
await nextTick()
|
||||
|
||||
expect(onAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'TestNode' })
|
||||
@@ -534,26 +527,27 @@ describe('NodeSearchContent', () => {
|
||||
{ name: 'HoverNode', display_name: 'Hover Node' }
|
||||
])
|
||||
|
||||
const calls = onHoverNode.mock.calls
|
||||
expect(calls[calls.length - 1][0]).toMatchObject({
|
||||
name: 'HoverNode'
|
||||
await waitFor(() => {
|
||||
const calls = onHoverNode.mock.calls
|
||||
expect(calls[calls.length - 1][0]).toMatchObject({ name: 'HoverNode' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should emit null hoverNode when no results', async () => {
|
||||
mockBookmarks(false, ['placeholder'])
|
||||
const { user, onHoverNode } = await renderComponent()
|
||||
const { user, onHoverNode } = renderComponent()
|
||||
|
||||
await clickFilterBarButton(user, 'Bookmarked')
|
||||
await nextTick()
|
||||
|
||||
const calls = onHoverNode.mock.calls
|
||||
expect(calls[calls.length - 1][0]).toBeNull()
|
||||
await waitFor(() => {
|
||||
const calls = onHoverNode.mock.calls
|
||||
expect(calls[calls.length - 1][0]).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter integration', () => {
|
||||
it('should display active filters in the input area', async () => {
|
||||
it('should display active filters in the input area', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'ImageNode',
|
||||
@@ -562,7 +556,7 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
])
|
||||
|
||||
await renderComponent({
|
||||
renderComponent({
|
||||
filters: [
|
||||
{
|
||||
filterDef: useNodeDefStore().nodeSearchService.inputTypeFilter,
|
||||
@@ -597,13 +591,11 @@ describe('NodeSearchContent', () => {
|
||||
|
||||
it('should emit removeFilter on backspace', async () => {
|
||||
const filters = createFilters(1)
|
||||
const { user, onRemoveFilter } = await renderComponent({ filters })
|
||||
const { user, onRemoveFilter } = renderComponent({ filters })
|
||||
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.keyboard('{Backspace}')
|
||||
await nextTick()
|
||||
await user.keyboard('{Backspace}')
|
||||
await nextTick()
|
||||
|
||||
expect(onRemoveFilter).toHaveBeenCalledTimes(1)
|
||||
expect(onRemoveFilter).toHaveBeenCalledWith(
|
||||
@@ -612,26 +604,102 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
|
||||
it('should not interact with chips when no filters exist', async () => {
|
||||
const { user, onRemoveFilter } = await renderComponent({ filters: [] })
|
||||
const { user, onRemoveFilter } = renderComponent({ filters: [] })
|
||||
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.keyboard('{Backspace}')
|
||||
await nextTick()
|
||||
|
||||
expect(onRemoveFilter).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove chip when clicking its delete button', async () => {
|
||||
const filters = createFilters(1)
|
||||
const { user, onRemoveFilter } = await renderComponent({ filters })
|
||||
const { user, onRemoveFilter } = renderComponent({ filters })
|
||||
|
||||
await user.click(screen.getByTestId('chip-delete'))
|
||||
await nextTick()
|
||||
|
||||
expect(onRemoveFilter).toHaveBeenCalledTimes(1)
|
||||
expect(onRemoveFilter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ value: 'IMAGE' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should emit removeFilter for every filter in a group when cleared', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'ImageNode',
|
||||
display_name: 'Image Node',
|
||||
input: { required: { image: ['IMAGE', {}] } }
|
||||
}),
|
||||
createMockNodeDef({
|
||||
name: 'LatentNode',
|
||||
display_name: 'Latent Node',
|
||||
input: { required: { latent: ['LATENT', {}] } }
|
||||
})
|
||||
])
|
||||
const inputFilter = useNodeDefStore().nodeSearchService.inputTypeFilter
|
||||
const filters = [
|
||||
{ filterDef: inputFilter, value: 'IMAGE' },
|
||||
{ filterDef: inputFilter, value: 'LATENT' }
|
||||
]
|
||||
|
||||
const { user, onRemoveFilter } = renderComponent({ filters })
|
||||
|
||||
const inputBtn = screen.getByRole('button', { name: /Input/ })
|
||||
await user.click(inputBtn)
|
||||
|
||||
const clearBtn = await screen.findByRole('button', { name: 'Clear all' })
|
||||
await user.click(clearBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onRemoveFilter).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
const removedValues = onRemoveFilter.mock.calls.map(([f]) => f.value)
|
||||
expect(removedValues).toEqual(expect.arrayContaining(['IMAGE', 'LATENT']))
|
||||
})
|
||||
})
|
||||
|
||||
describe('rootFilter + category + search combination', () => {
|
||||
it('should intersect rootFilter, selected category, and search query', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'CustomSampler',
|
||||
display_name: 'Custom Sampler',
|
||||
category: 'sampling',
|
||||
python_module: 'custom_nodes.my_extension'
|
||||
}),
|
||||
createMockNodeDef({
|
||||
name: 'CustomLoader',
|
||||
display_name: 'Custom Loader',
|
||||
category: 'loaders',
|
||||
python_module: 'custom_nodes.my_extension'
|
||||
}),
|
||||
createMockNodeDef({
|
||||
name: 'CoreSampler',
|
||||
display_name: 'Core Sampler',
|
||||
category: 'sampling',
|
||||
python_module: 'nodes'
|
||||
})
|
||||
])
|
||||
|
||||
const { user } = renderComponent()
|
||||
|
||||
await clickFilterBarButton(user, 'Extensions')
|
||||
const samplingBtn = await screen.findByTestId('category-custom/sampling')
|
||||
await user.click(samplingBtn)
|
||||
|
||||
const input = screen.getByRole('combobox')
|
||||
await user.type(input, 'Custom')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('node-item')).toHaveLength(1)
|
||||
})
|
||||
const texts = screen
|
||||
.queryAllByTestId('node-item')
|
||||
.map((i) => i.textContent)
|
||||
expect(texts).toContain('Custom Sampler')
|
||||
expect(texts).not.toContain('Core Sampler')
|
||||
expect(texts).not.toContain('Custom Loader')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
269
src/components/searchbox/v2/NodeSearchListItem.test.ts
Normal file
269
src/components/searchbox/v2/NodeSearchListItem.test.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
import NodeSearchListItem from '@/components/searchbox/v2/NodeSearchListItem.vue'
|
||||
import {
|
||||
createMockNodeDef,
|
||||
setupTestPinia,
|
||||
testI18n
|
||||
} from '@/components/searchbox/v2/__test__/testUtils'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
|
||||
function renderItem(
|
||||
props: Partial<ComponentProps<typeof NodeSearchListItem>> = {}
|
||||
) {
|
||||
return render(NodeSearchListItem, {
|
||||
props: { nodeDef: createMockNodeDef(), currentQuery: '', ...props },
|
||||
global: {
|
||||
plugins: [testI18n],
|
||||
stubs: {
|
||||
NodePricingBadge: {
|
||||
template: '<div data-testid="pricing-badge" />',
|
||||
props: ['nodeDef']
|
||||
},
|
||||
ComfyLogo: { template: '<div data-testid="comfy-logo" />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('NodeSearchListItem', () => {
|
||||
beforeEach(() => {
|
||||
setupTestPinia()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('id name badge', () => {
|
||||
it('shows id name when ShowIdName setting is enabled', () => {
|
||||
useSettingStore().settingValues['Comfy.NodeSearchBoxImpl.ShowIdName'] =
|
||||
true
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({
|
||||
name: 'KSamplerNode',
|
||||
display_name: 'KSampler'
|
||||
})
|
||||
})
|
||||
expect(screen.getByText('KSamplerNode')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides id name by default', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({
|
||||
name: 'KSamplerNode',
|
||||
display_name: 'KSampler'
|
||||
})
|
||||
})
|
||||
expect(screen.queryByText('KSamplerNode')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('showDescription mode', () => {
|
||||
it('renders description text', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ description: 'A sampler node' }),
|
||||
showDescription: true
|
||||
})
|
||||
expect(screen.getByText('A sampler node')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders category when ShowCategory setting is enabled', () => {
|
||||
useSettingStore().settingValues['Comfy.NodeSearchBoxImpl.ShowCategory'] =
|
||||
true
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ category: 'sampling/advanced' }),
|
||||
showDescription: true
|
||||
})
|
||||
expect(screen.getByText('sampling / advanced')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides category by default', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ category: 'sampling' }),
|
||||
showDescription: true
|
||||
})
|
||||
expect(screen.queryByText('sampling')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('source badge', () => {
|
||||
it('renders core comfy badge for non-custom node when showSourceBadge is true', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ python_module: 'nodes' }),
|
||||
showDescription: true,
|
||||
showSourceBadge: true
|
||||
})
|
||||
expect(screen.getByTestId('comfy-logo')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders custom node badge for custom node when showSourceBadge is true', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({
|
||||
python_module: 'custom_nodes.my_extension',
|
||||
display_name: 'CustomNode'
|
||||
}),
|
||||
showDescription: true,
|
||||
showSourceBadge: true
|
||||
})
|
||||
expect(screen.getByText('my_extension')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render source badge when showSourceBadge is false', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ python_module: 'nodes' }),
|
||||
showDescription: true,
|
||||
showSourceBadge: false
|
||||
})
|
||||
expect(screen.queryByTestId('comfy-logo')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('API node provider badge', () => {
|
||||
it('renders provider badge only when nodeDef.api_node is true', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({
|
||||
api_node: true,
|
||||
category: 'api/image/BFL'
|
||||
})
|
||||
})
|
||||
expect(screen.getByText('BFL')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render provider badge when nodeDef.api_node is false', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({
|
||||
api_node: false,
|
||||
category: 'api/image/BFL'
|
||||
})
|
||||
})
|
||||
expect(screen.queryByText('BFL')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('status flags', () => {
|
||||
it('shows deprecated label when deprecated', () => {
|
||||
renderItem({ nodeDef: createMockNodeDef({ deprecated: true }) })
|
||||
expect(screen.getByText('DEPR')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows experimental label when experimental', () => {
|
||||
renderItem({ nodeDef: createMockNodeDef({ experimental: true }) })
|
||||
expect(screen.getByText('BETA')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows devOnly label when dev_only is set', () => {
|
||||
renderItem({ nodeDef: createMockNodeDef({ dev_only: true }) })
|
||||
expect(screen.getByText('DEV')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show flags in description mode', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ deprecated: true, experimental: true }),
|
||||
showDescription: true
|
||||
})
|
||||
expect(screen.queryByText('DEPR')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('BETA')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('node frequency badge', () => {
|
||||
it('shows frequency when ShowNodeFrequency is enabled and frequency > 0', () => {
|
||||
useSettingStore().settingValues[
|
||||
'Comfy.NodeSearchBoxImpl.ShowNodeFrequency'
|
||||
] = true
|
||||
vi.spyOn(useNodeFrequencyStore(), 'getNodeFrequency').mockReturnValue(
|
||||
1500
|
||||
)
|
||||
renderItem({ nodeDef: createMockNodeDef() })
|
||||
const badge = screen.getByTestId('frequency-badge')
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge.textContent).toMatch(/1\.5k/i)
|
||||
})
|
||||
|
||||
it('hides frequency when frequency is 0 even if setting is enabled', () => {
|
||||
useSettingStore().settingValues[
|
||||
'Comfy.NodeSearchBoxImpl.ShowNodeFrequency'
|
||||
] = true
|
||||
vi.spyOn(useNodeFrequencyStore(), 'getNodeFrequency').mockReturnValue(0)
|
||||
renderItem({ nodeDef: createMockNodeDef() })
|
||||
expect(screen.queryByTestId('frequency-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides frequency when setting is disabled even if frequency > 0', () => {
|
||||
useSettingStore().settingValues[
|
||||
'Comfy.NodeSearchBoxImpl.ShowNodeFrequency'
|
||||
] = false
|
||||
vi.spyOn(useNodeFrequencyStore(), 'getNodeFrequency').mockReturnValue(
|
||||
9999
|
||||
)
|
||||
renderItem({ nodeDef: createMockNodeDef() })
|
||||
expect(screen.queryByTestId('frequency-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('bookmark icon', () => {
|
||||
it('shows bookmark icon when node is bookmarked', () => {
|
||||
useSettingStore().settingValues['Comfy.NodeLibrary.Bookmarks.V2'] = [
|
||||
'TestNode'
|
||||
]
|
||||
renderItem({ nodeDef: createMockNodeDef({ name: 'TestNode' }) })
|
||||
expect(
|
||||
screen.getByRole('img', { name: 'Bookmarked' })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show bookmark icon when node is not bookmarked', () => {
|
||||
renderItem({ nodeDef: createMockNodeDef({ name: 'TestNode' }) })
|
||||
expect(
|
||||
screen.queryByRole('img', { name: 'Bookmarked' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides bookmark icon when hideBookmarkIcon prop is true', () => {
|
||||
useSettingStore().settingValues['Comfy.NodeLibrary.Bookmarks.V2'] = [
|
||||
'TestNode'
|
||||
]
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ name: 'TestNode' }),
|
||||
hideBookmarkIcon: true
|
||||
})
|
||||
expect(
|
||||
screen.queryByRole('img', { name: 'Bookmarked' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('query highlighting', () => {
|
||||
it('wraps matching portion of display_name in a highlight span', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ display_name: 'KSampler Advanced' }),
|
||||
currentQuery: 'Sampler'
|
||||
})
|
||||
expect(
|
||||
screen.getByText('Sampler', { selector: 'span.highlight' })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not wrap anything when currentQuery is empty', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({ display_name: 'KSampler' }),
|
||||
currentQuery: ''
|
||||
})
|
||||
expect(
|
||||
screen.queryByText('KSampler', { selector: 'span.highlight' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('node source display text', () => {
|
||||
it('shows custom node source displayText in non-description mode', () => {
|
||||
renderItem({
|
||||
nodeDef: createMockNodeDef({
|
||||
python_module: 'custom_nodes.my_extension'
|
||||
})
|
||||
})
|
||||
expect(screen.getByText('my_extension')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,8 +5,12 @@
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-1 overflow-hidden">
|
||||
<!-- Row 1: Name (left) + badges (right) -->
|
||||
<div class="text-foreground flex items-center gap-2 text-sm">
|
||||
<span v-if="isBookmarked && !hideBookmarkIcon">
|
||||
<i class="pi pi-bookmark-fill mr-1 text-sm" />
|
||||
<span
|
||||
v-if="isBookmarked && !hideBookmarkIcon"
|
||||
role="img"
|
||||
:aria-label="$t('g.bookmarked')"
|
||||
>
|
||||
<i aria-hidden="true" class="pi pi-bookmark-fill mr-1 text-sm" />
|
||||
</span>
|
||||
<span
|
||||
class="truncate"
|
||||
@@ -95,6 +99,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="showNodeFrequency && nodeFrequency > 0"
|
||||
data-testid="frequency-badge"
|
||||
class="rounded-sm bg-secondary-background px-1.5 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
{{ formatNumberWithSuffix(nodeFrequency, { roundToInt: true }) }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { render, screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
@@ -40,19 +40,21 @@ describe(NodeSearchTypeFilterPopover, () => {
|
||||
const user = userEvent.setup()
|
||||
const onToggle = vi.fn()
|
||||
const onClear = vi.fn()
|
||||
const onEscapeClose = vi.fn()
|
||||
render(NodeSearchTypeFilterPopover, {
|
||||
props: {
|
||||
chip: props.chip ?? createMockChip(),
|
||||
selectedValues: props.selectedValues ?? [],
|
||||
onToggle,
|
||||
onClear
|
||||
onClear,
|
||||
onEscapeClose
|
||||
},
|
||||
slots: {
|
||||
default: '<button data-testid="trigger">Input</button>'
|
||||
},
|
||||
global: { plugins: [testI18n] }
|
||||
})
|
||||
return { user, onToggle, onClear }
|
||||
return { user, onToggle, onClear, onEscapeClose }
|
||||
}
|
||||
|
||||
async function openPopover(user: ReturnType<typeof userEvent.setup>) {
|
||||
@@ -156,6 +158,19 @@ describe(NodeSearchTypeFilterPopover, () => {
|
||||
await nextTick()
|
||||
|
||||
expect(screen.queryAllByRole('option')).toHaveLength(0)
|
||||
expect(screen.getByText('No results')).toBeInTheDocument()
|
||||
expect(screen.getByText('No Results')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should emit escapeClose and close the popover when Escape is pressed', async () => {
|
||||
const { user, onEscapeClose } = createRender()
|
||||
await openPopover(user)
|
||||
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(onEscapeClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,12 +2,14 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
export function createMockNodeDef(
|
||||
overrides: Partial<ComfyNodeDef> = {}
|
||||
): ComfyNodeDef {
|
||||
return {
|
||||
): ComfyNodeDefImpl {
|
||||
return new ComfyNodeDefImpl({
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'test',
|
||||
@@ -21,7 +23,7 @@ export function createMockNodeDef(
|
||||
deprecated: false,
|
||||
experimental: false,
|
||||
...overrides
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setupTestPinia() {
|
||||
@@ -31,34 +33,5 @@ export function setupTestPinia() {
|
||||
export const testI18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
addNode: 'Add a node...',
|
||||
filterBy: 'Filter by:',
|
||||
mostRelevant: 'Most relevant',
|
||||
recents: 'Recents',
|
||||
favorites: 'Favorites',
|
||||
bookmarked: 'Bookmarked',
|
||||
essentials: 'Essentials',
|
||||
category: 'Category',
|
||||
custom: 'Custom',
|
||||
comfy: 'Comfy',
|
||||
partner: 'Partner',
|
||||
extensions: 'Extensions',
|
||||
noResults: 'No results',
|
||||
filterByType: 'Filter by {type}...',
|
||||
input: 'Input',
|
||||
output: 'Output',
|
||||
source: 'Source',
|
||||
search: 'Search',
|
||||
blueprints: 'Blueprints',
|
||||
partnerNodes: 'Partner Nodes',
|
||||
remove: 'Remove',
|
||||
itemsSelected:
|
||||
'No items selected | {count} item selected | {count} items selected',
|
||||
clearAll: 'Clear all'
|
||||
}
|
||||
}
|
||||
}
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user