Files
ComfyUI_frontend/src/components/common/TextTickerMultiLine.test.ts
Yourz 6ee3803770 feat: implement NodeLibrarySidebarTabV2 with Reka UI components (#8548)
## Summary

Implement a redesigned Node Library sidebar using Reka UI components
with virtualized tree rendering and improved UX.

## Changes

- **What**: 
  - Add three-tab structure (Essential, All, Custom) using Reka UI Tabs
- Implement TreeExplorerV2 with virtualized tree using
TreeRoot/TreeVirtualizer for performance
  - Add node hover preview with teleport to show NodePreview component
  - Implement context menu for toggling favorites on nodes
  - Add search functionality that auto-expands matching folders
- Create panel components: EssentialNodesPanel, AllNodesPanel,
CustomNodesPanel
  - Add 'Open Manager' button in CustomNodesPanel
  - Use custom icons: comfy--node for nodes, ph--folder-fill for folders
  - New node preview component: `NodePreviewCard`
  - Api node folder icon
  - Node drag preview

- **Feature Flag**: Enabled via URL parameter `?nodeRedesign=true`

## Review Focus

- TreeExplorerV2.vue uses `[...expandedKeys]` to prevent internal
mutation by Reka UI TreeRoot
- Context menu injection key is exported from TreeExplorerV2Node.vue and
imported by TreeExplorerV2.vue
- Hover preview uses teleport to
`#node-library-node-preview-container-v2`

## Screenshots (if applicable)

| Feature | Screenshot | 
|---|---|
| All nodes tab |<img width="323" height="761" alt="image"
src="https://github.com/user-attachments/assets/1976222b-83dc-4a1b-838a-2d49aedea3b8"
/>|
| Custom nodes tab | <img width="308" height="748" alt="image"
src="https://github.com/user-attachments/assets/2c23bffb-bdaa-4c6c-8cac-7610fb7f3fb7"
/>|
|Api nodes icon | <img width="299" height="523" alt="image"
src="https://github.com/user-attachments/assets/e9ca05b0-1143-44cf-b227-6462173c7cd0"
/>|
| node preview|<img width="499" height="544" alt="image"
src="https://github.com/user-attachments/assets/8961a7b4-77ae-4e57-99cf-62d9e4e17088"
/>|
| node drag preview | <img width="434" height="289" alt="image"
src="https://github.com/user-attachments/assets/b5838c90-65d4-4bee-b2b3-c41b57870da8"
/>|



Test by adding `?nodeRedesign=true` to the URL

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8548-WIP-feat-implement-NodeLibrarySidebarTabV2-with-Reka-UI-components-2fb6d73d36508134b7e0f75a2c9b976a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
2026-02-20 22:06:09 -08:00

106 lines
2.8 KiB
TypeScript

import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import { afterEach, describe, expect, it, vi } from 'vitest'
import MarqueeLine from './MarqueeLine.vue'
import TextTickerMultiLine from './TextTickerMultiLine.vue'
type Callback = () => void
const resizeCallbacks: Callback[] = []
const mutationCallbacks: Callback[] = []
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core')
return {
...actual,
useResizeObserver: (_target: unknown, cb: Callback) => {
resizeCallbacks.push(cb)
return { stop: vi.fn() }
},
useMutationObserver: (_target: unknown, cb: Callback) => {
mutationCallbacks.push(cb)
return { stop: vi.fn() }
}
}
})
function mockElementSize(
el: HTMLElement,
clientWidth: number,
scrollWidth: number
) {
Object.defineProperty(el, 'clientWidth', {
value: clientWidth,
configurable: true
})
Object.defineProperty(el, 'scrollWidth', {
value: scrollWidth,
configurable: true
})
}
describe(TextTickerMultiLine, () => {
let wrapper: ReturnType<typeof mount>
afterEach(() => {
wrapper?.unmount()
resizeCallbacks.length = 0
mutationCallbacks.length = 0
})
function mountComponent(text: string) {
wrapper = mount(TextTickerMultiLine, {
slots: { default: text }
})
return wrapper
}
function getMeasureEl(): HTMLElement {
return wrapper.find('[aria-hidden="true"]').element as HTMLElement
}
async function triggerSplitLines() {
resizeCallbacks.forEach((cb) => cb())
await nextTick()
}
it('renders slot content', () => {
mountComponent('Load Checkpoint')
expect(wrapper.text()).toContain('Load Checkpoint')
})
it('renders a single MarqueeLine when text fits', async () => {
mountComponent('Short')
mockElementSize(getMeasureEl(), 200, 100)
await triggerSplitLines()
expect(wrapper.findAllComponents(MarqueeLine)).toHaveLength(1)
})
it('renders two MarqueeLines when text overflows', async () => {
mountComponent('Load Checkpoint Loader Simple')
mockElementSize(getMeasureEl(), 100, 300)
await triggerSplitLines()
expect(wrapper.findAllComponents(MarqueeLine)).toHaveLength(2)
})
it('splits text at word boundary when overflowing', async () => {
mountComponent('Load Checkpoint Loader')
mockElementSize(getMeasureEl(), 100, 200)
await triggerSplitLines()
const lines = wrapper.findAllComponents(MarqueeLine)
expect(lines[0].text()).toBe('Load')
expect(lines[1].text()).toBe('Checkpoint Loader')
})
it('has hidden measurement element with aria-hidden', () => {
mountComponent('Test')
const measureEl = wrapper.find('[aria-hidden="true"]')
expect(measureEl.exists()).toBe(true)
expect(measureEl.classes()).toContain('invisible')
})
})