Files
ComfyUI_frontend/src/components/sidebar/tabs/NodeLibrarySidebarTabV2.test.ts
Jin Yi 10b0350d01 feat: unify sidebar panel header layout with SidebarTopArea component (#9740)
## Summary

Unify the search bar + action buttons layout across all left sidebar
panels (Node Library, Workflows, Model Library, Media Assets) using a
shared `SidebarTopArea` presentation component.

## Changes

- **What**:
- Add `SidebarTopArea.vue` — layout component with `flex-1` default slot
(search) and `#actions` slot (buttons), plus optional `bottomDivider`
prop
- Replace raw `<button>` elements in Node Library with `<Button
variant="secondary" size="icon">`
- Replace reka-ui `TabsTrigger` with shared `Tab/TabList` component in
Node Library
- Move Media Assets tab list from hover-only `#tool-buttons` to
always-visible header below search area
- Unify spacing (`gap-2`, `p-2 2xl:px-4`) and divider styles across all
sidebar panels
- Remove unused `assetType` prop and header from
`AssetsSidebarGridView`/`AssetsSidebarListView`

## Review Focus

- `SidebarTopArea` API simplicity — just slots + one optional prop
- Node Library still requires `TabsRoot` in the body for reka-ui
`TabsContent` in child panels
- Media Assets tabs are now always visible instead of hover-only

[screen-capture
(1).webm](https://github.com/user-attachments/assets/fe1d8f7b-5674-4bb3-9842-569e4c3af6c9)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9740-feat-unify-sidebar-panel-header-layout-with-SidebarTopArea-component-3206d73d365081ea8ba7fd6ac54e0169)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-03-13 06:32:18 -07:00

128 lines
3.2 KiB
TypeScript

import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import NodeLibrarySidebarTabV2 from './NodeLibrarySidebarTabV2.vue'
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core')
return {
...actual,
useLocalStorage: vi.fn((_key: string, defaultValue: unknown) =>
ref(defaultValue)
)
}
})
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
useNodeDragToCanvas: () => ({
isDragging: { value: false },
draggedNode: { value: null },
cursorPosition: { value: { x: 0, y: 0 } },
startDrag: vi.fn(),
cancelDrag: vi.fn(),
setupGlobalListeners: vi.fn(),
cleanupGlobalListeners: vi.fn()
})
}))
vi.mock('@/services/nodeOrganizationService', () => ({
DEFAULT_TAB_ID: 'essentials',
DEFAULT_SORTING_ID: 'alphabetical',
nodeOrganizationService: {
organizeNodesByTab: vi.fn(() => []),
getSortingStrategies: vi.fn(() => [])
}
}))
vi.mock('./nodeLibrary/AllNodesPanel.vue', () => ({
default: {
name: 'AllNodesPanel',
template: '<div data-testid="all-panel"><slot /></div>',
props: ['sections', 'expandedKeys', 'fillNodeInfo']
}
}))
vi.mock('./nodeLibrary/BlueprintsPanel.vue', () => ({
default: {
name: 'BlueprintsPanel',
template: '<div data-testid="blueprints-panel"><slot /></div>',
props: ['sections', 'expandedKeys']
}
}))
vi.mock('./nodeLibrary/EssentialNodesPanel.vue', () => ({
default: {
name: 'EssentialNodesPanel',
template: '<div data-testid="essential-panel"><slot /></div>',
props: ['root', 'expandedKeys', 'flatNodes']
}
}))
vi.mock('./nodeLibrary/NodeDragPreview.vue', () => ({
default: {
name: 'NodeDragPreview',
template: '<div />'
}
}))
vi.mock('@/components/ui/search-input/SearchInput.vue', () => ({
default: {
name: 'SearchBox',
template: '<input data-testid="search-box" />',
props: ['modelValue', 'placeholder'],
setup() {
return { focus: vi.fn() }
},
expose: ['focus']
}
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: {} }
})
describe('NodeLibrarySidebarTabV2', () => {
beforeEach(() => {
vi.clearAllMocks()
})
function mountComponent() {
return mount(NodeLibrarySidebarTabV2, {
global: {
plugins: [createTestingPinia({ stubActions: false }), i18n],
stubs: {
teleport: true
}
}
})
}
it('should render with tabs', () => {
const wrapper = mountComponent()
const triggers = wrapper.findAll('[role="tab"]')
expect(triggers).toHaveLength(3)
})
it('should render search box', () => {
const wrapper = mountComponent()
expect(wrapper.find('[data-testid="search-box"]').exists()).toBe(true)
})
it('should render only the selected panel', () => {
const wrapper = mountComponent()
expect(wrapper.find('[data-testid="essential-panel"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="all-panel"]').exists()).toBe(false)
expect(wrapper.find('[data-testid="blueprints-panel"]').exists()).toBe(
false
)
})
})