Files
ComfyUI_frontend/src/components/sidebar/tabs/AssetsSidebarListView.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

135 lines
3.8 KiB
TypeScript

import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import AssetsSidebarListView from './AssetsSidebarListView.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) => key
})
}))
vi.mock('@/stores/assetsStore', () => ({
useAssetsStore: () => ({
isAssetDeleting: () => false
})
}))
const VirtualGridStub = defineComponent({
name: 'VirtualGrid',
props: {
items: {
type: Array,
default: () => []
}
},
template:
'<div><slot v-for="item in items" :key="item.key" name="item" :item="item" /></div>'
})
const buildAsset = (id: string, name: string): AssetItem =>
({
id,
name,
tags: []
}) satisfies AssetItem
const buildOutputItem = (asset: AssetItem): OutputStackListItem => ({
key: `asset-${asset.id}`,
asset
})
const mountListView = (assetItems: OutputStackListItem[] = []) =>
mount(AssetsSidebarListView, {
props: {
assetItems,
selectableAssets: [],
isSelected: () => false,
isStackExpanded: () => false,
toggleStack: async () => {}
},
global: {
stubs: {
VirtualGrid: VirtualGridStub
}
}
})
describe('AssetsSidebarListView', () => {
it('marks mp4 assets as video previews', () => {
const videoAsset = {
...buildAsset('video-asset', 'clip.mp4'),
preview_url: '/api/view/clip.mp4',
user_metadata: {}
} satisfies AssetItem
const wrapper = mountListView([buildOutputItem(videoAsset)])
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
const assetListItem = listItems.at(-1)
expect(assetListItem).toBeDefined()
expect(assetListItem?.props('previewUrl')).toBe('/api/view/clip.mp4')
expect(assetListItem?.props('isVideoPreview')).toBe(true)
})
it('uses icon fallback for text assets even when preview_url exists', () => {
const textAsset = {
...buildAsset('text-asset', 'notes.txt'),
preview_url: '/api/view/notes.txt',
user_metadata: {}
} satisfies AssetItem
const wrapper = mountListView([buildOutputItem(textAsset)])
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
const assetListItem = listItems.at(-1)
expect(assetListItem).toBeDefined()
expect(assetListItem?.props('previewUrl')).toBe('')
expect(assetListItem?.props('isVideoPreview')).toBe(false)
})
it('emits preview-asset when item preview is clicked', async () => {
const imageAsset = {
...buildAsset('image-asset', 'image.png'),
preview_url: '/api/view/image.png',
user_metadata: {}
} satisfies AssetItem
const wrapper = mountListView([buildOutputItem(imageAsset)])
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
const assetListItem = listItems.at(-1)
expect(assetListItem).toBeDefined()
assetListItem!.vm.$emit('preview-click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('preview-asset')).toEqual([[imageAsset]])
})
it('emits preview-asset when item is double-clicked', async () => {
const imageAsset = {
...buildAsset('image-asset-dbl', 'image.png'),
preview_url: '/api/view/image.png',
user_metadata: {}
} satisfies AssetItem
const wrapper = mountListView([buildOutputItem(imageAsset)])
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
const assetListItem = listItems.at(-1)
expect(assetListItem).toBeDefined()
await assetListItem!.trigger('dblclick')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('preview-asset')).toEqual([[imageAsset]])
})
})