mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: update for reviews
This commit is contained in:
@@ -33,7 +33,9 @@ describe('BadgePill', () => {
|
||||
const wrapper = mount(BadgePill, {
|
||||
props: { text: 'Default' }
|
||||
})
|
||||
expect(wrapper.attributes('style')).toContain('border-color: #525252')
|
||||
expect(wrapper.attributes('style')).toContain(
|
||||
'border-color: var(--border-color)'
|
||||
)
|
||||
})
|
||||
|
||||
it('applies solid border color when borderStyle is a color', () => {
|
||||
@@ -64,18 +66,18 @@ describe('BadgePill', () => {
|
||||
expect(style).toContain('color: #f59e0b')
|
||||
})
|
||||
|
||||
it('has white text when not filled', () => {
|
||||
it('has foreground text when not filled', () => {
|
||||
const wrapper = mount(BadgePill, {
|
||||
props: { text: 'Not Filled', borderStyle: '#f59e0b' }
|
||||
})
|
||||
expect(wrapper.classes()).toContain('text-white')
|
||||
expect(wrapper.classes()).toContain('text-foreground')
|
||||
})
|
||||
|
||||
it('does not have white text class when filled', () => {
|
||||
it('does not have foreground text class when filled', () => {
|
||||
const wrapper = mount(BadgePill, {
|
||||
props: { text: 'Filled', borderStyle: '#f59e0b', filled: true }
|
||||
})
|
||||
expect(wrapper.classes()).not.toContain('text-white')
|
||||
expect(wrapper.classes()).not.toContain('text-foreground')
|
||||
})
|
||||
|
||||
it('renders slot content', () => {
|
||||
|
||||
@@ -23,12 +23,12 @@ const { borderStyle, filled } = defineProps<{
|
||||
}>()
|
||||
|
||||
const textColorClass = computed(() =>
|
||||
borderStyle && filled ? '' : 'text-white'
|
||||
borderStyle && filled ? '' : 'text-foreground'
|
||||
)
|
||||
|
||||
const customStyle = computed(() => {
|
||||
if (!borderStyle) {
|
||||
return { borderColor: '#525252' }
|
||||
return { borderColor: 'var(--border-color)' }
|
||||
}
|
||||
|
||||
const isGradient = borderStyle.includes('linear-gradient')
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
>
|
||||
<TreeExplorerV2Node
|
||||
:item="item as FlattenedItem<RenderedTreeExplorerNode>"
|
||||
:show-context-menu="showContextMenu"
|
||||
@node-click="
|
||||
(node: RenderedTreeExplorerNode, e: MouseEvent) =>
|
||||
emit('nodeClick', node, e)
|
||||
|
||||
@@ -145,10 +145,9 @@ describe('TreeExplorerV2Node', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not render ContextMenuTrigger when showContextMenu is false', () => {
|
||||
it('does not render ContextMenuTrigger for folder items', () => {
|
||||
const { wrapper } = mountComponent({
|
||||
item: createMockItem('node'),
|
||||
showContextMenu: false
|
||||
item: createMockItem('folder')
|
||||
})
|
||||
|
||||
expect(
|
||||
|
||||
@@ -6,14 +6,10 @@
|
||||
as-child
|
||||
>
|
||||
<!-- Node with context menu -->
|
||||
<ContextMenuTrigger
|
||||
v-if="showContextMenu && item.value.type === 'node'"
|
||||
as-child
|
||||
>
|
||||
<ContextMenuTrigger v-if="item.value.type === 'node'" as-child>
|
||||
<div
|
||||
class="group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight"
|
||||
:class="{ 'bg-highlight': isSelected }"
|
||||
:style="{ paddingLeft: `${16 + (item.level - 1) * 24}px` }"
|
||||
:class="cn(ROW_CLASS, isSelected && 'bg-highlight')"
|
||||
:style="rowStyle"
|
||||
@click.stop="handleClick($event, handleToggle, handleSelect)"
|
||||
@contextmenu="handleContextMenu"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@@ -28,30 +24,11 @@
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<!-- Node without context menu -->
|
||||
<div
|
||||
v-else-if="item.value.type === 'node'"
|
||||
class="group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight"
|
||||
:class="{ 'bg-highlight': isSelected }"
|
||||
:style="{ paddingLeft: `${16 + (item.level - 1) * 24}px` }"
|
||||
@click.stop="handleClick($event, handleToggle, handleSelect)"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<i class="icon-[comfy--node] size-4 shrink-0 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate text-sm text-foreground">
|
||||
<slot name="node" :node="item.value">
|
||||
{{ item.value.label }}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Folder -->
|
||||
<div
|
||||
v-else
|
||||
class="group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight"
|
||||
:class="{ 'bg-highlight': isSelected }"
|
||||
:style="{ paddingLeft: `${16 + (item.level - 1) * 24}px` }"
|
||||
:class="cn(ROW_CLASS, isSelected && 'bg-highlight')"
|
||||
:style="rowStyle"
|
||||
@click.stop="handleClick($event, handleToggle, handleSelect)"
|
||||
>
|
||||
<i
|
||||
@@ -96,9 +73,11 @@ import { InjectKeyContextMenuNode } from '@/types/treeExplorerTypes'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { item, showContextMenu = false } = defineProps<{
|
||||
const ROW_CLASS =
|
||||
'group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: FlattenedItem<RenderedTreeExplorerNode>
|
||||
showContextMenu?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -118,6 +97,10 @@ const nodePreviewStyle = ref<CSSProperties>({
|
||||
zIndex: 1001
|
||||
})
|
||||
|
||||
const rowStyle = computed(() => ({
|
||||
paddingLeft: `${16 + (item.level - 1) * 24}px`
|
||||
}))
|
||||
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
@@ -151,26 +134,20 @@ function handleMouseEnter(e: MouseEvent) {
|
||||
const viewportHeight = window.innerHeight
|
||||
const viewportWidth = window.innerWidth
|
||||
|
||||
// Calculate horizontal position based on sidebar location
|
||||
let left: number
|
||||
if (sidebarLocation.value === 'left') {
|
||||
left = rect.right + PREVIEW_MARGIN
|
||||
// If preview would overflow right edge, flip to left side
|
||||
if (left + PREVIEW_WIDTH > viewportWidth) {
|
||||
left = rect.left - PREVIEW_MARGIN - PREVIEW_WIDTH
|
||||
}
|
||||
} else {
|
||||
left = rect.left - PREVIEW_MARGIN - PREVIEW_WIDTH
|
||||
// If preview would overflow left edge, flip to right side
|
||||
if (left < 0) {
|
||||
left = rect.right + PREVIEW_MARGIN
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate mouse Y position (center of hovered item)
|
||||
const mouseY = rect.top + rect.height / 2
|
||||
|
||||
// Initial top position - will be adjusted after render
|
||||
let top = rect.top
|
||||
|
||||
nodePreviewStyle.value = {
|
||||
@@ -182,17 +159,13 @@ function handleMouseEnter(e: MouseEvent) {
|
||||
}
|
||||
isHovered.value = true
|
||||
|
||||
// After render, adjust position to ensure mouse is within preview height
|
||||
requestAnimationFrame(() => {
|
||||
if (previewRef.value) {
|
||||
const previewRect = previewRef.value.getBoundingClientRect()
|
||||
const previewHeight = previewRect.height
|
||||
|
||||
// Ensure mouse Y is within preview's vertical range
|
||||
// Position preview so mouse is roughly in the upper third
|
||||
top = mouseY - previewHeight * 0.3
|
||||
|
||||
// Clamp to viewport bounds
|
||||
const minTop = PREVIEW_MARGIN
|
||||
const maxTop = viewportHeight - previewHeight - PREVIEW_MARGIN
|
||||
top = Math.max(minTop, Math.min(top, maxTop))
|
||||
|
||||
@@ -1,94 +1,130 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
|
||||
import { ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { TabId } from '@/types/nodeOrganizationTypes'
|
||||
import NodeLibrarySidebarTabV2 from './NodeLibrarySidebarTabV2.vue'
|
||||
|
||||
describe('NodeLibrarySidebarTabV2 expandedKeys logic', () => {
|
||||
describe('per-tab expandedKeys', () => {
|
||||
function createExpandedKeysState(initialTab: TabId = 'essentials') {
|
||||
const selectedTab = ref<TabId>(initialTab)
|
||||
const expandedKeysByTab = ref<Record<TabId, string[]>>({
|
||||
essentials: [],
|
||||
all: [],
|
||||
custom: []
|
||||
})
|
||||
const expandedKeys = computed({
|
||||
get: () => expandedKeysByTab.value[selectedTab.value],
|
||||
set: (value) => {
|
||||
expandedKeysByTab.value[selectedTab.value] = value
|
||||
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',
|
||||
nodeOrganizationService: {
|
||||
organizeNodesByTab: 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/CustomNodesPanel.vue', () => ({
|
||||
default: {
|
||||
name: 'CustomNodesPanel',
|
||||
template: '<div data-testid="custom-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']
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./nodeLibrary/NodeDragPreview.vue', () => ({
|
||||
default: {
|
||||
name: 'NodeDragPreview',
|
||||
template: '<div />'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/common/SearchBoxV2.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],
|
||||
components: {
|
||||
TabsRoot,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent
|
||||
},
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
})
|
||||
|
||||
return { selectedTab, expandedKeysByTab, expandedKeys }
|
||||
}
|
||||
|
||||
it('should initialize with empty arrays for all tabs', () => {
|
||||
const { expandedKeysByTab } = createExpandedKeysState()
|
||||
|
||||
expect(expandedKeysByTab.value.essentials).toEqual([])
|
||||
expect(expandedKeysByTab.value.all).toEqual([])
|
||||
expect(expandedKeysByTab.value.custom).toEqual([])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('should return keys for the current tab', () => {
|
||||
const { selectedTab, expandedKeysByTab, expandedKeys } =
|
||||
createExpandedKeysState('essentials')
|
||||
it('should render with tabs', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expandedKeysByTab.value.essentials = ['key1', 'key2']
|
||||
expandedKeysByTab.value.all = ['key3']
|
||||
const triggers = wrapper.findAllComponents(TabsTrigger)
|
||||
expect(triggers.length).toBe(3)
|
||||
})
|
||||
|
||||
expect(expandedKeys.value).toEqual(['key1', 'key2'])
|
||||
it('should render search box', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
selectedTab.value = 'all'
|
||||
expect(expandedKeys.value).toEqual(['key3'])
|
||||
})
|
||||
expect(wrapper.find('[data-testid="search-box"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should set keys only for the current tab', () => {
|
||||
const { expandedKeysByTab, expandedKeys } =
|
||||
createExpandedKeysState('essentials')
|
||||
it('should render all panel components', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expandedKeys.value = ['new-key1', 'new-key2']
|
||||
|
||||
expect(expandedKeysByTab.value.essentials).toEqual([
|
||||
'new-key1',
|
||||
'new-key2'
|
||||
])
|
||||
expect(expandedKeysByTab.value.all).toEqual([])
|
||||
expect(expandedKeysByTab.value.custom).toEqual([])
|
||||
})
|
||||
|
||||
it('should preserve keys when switching tabs', () => {
|
||||
const { selectedTab, expandedKeysByTab, expandedKeys } =
|
||||
createExpandedKeysState('essentials')
|
||||
|
||||
expandedKeys.value = ['essentials-key']
|
||||
selectedTab.value = 'all'
|
||||
expandedKeys.value = ['all-key']
|
||||
selectedTab.value = 'custom'
|
||||
expandedKeys.value = ['custom-key']
|
||||
|
||||
expect(expandedKeysByTab.value.essentials).toEqual(['essentials-key'])
|
||||
expect(expandedKeysByTab.value.all).toEqual(['all-key'])
|
||||
expect(expandedKeysByTab.value.custom).toEqual(['custom-key'])
|
||||
|
||||
selectedTab.value = 'essentials'
|
||||
expect(expandedKeys.value).toEqual(['essentials-key'])
|
||||
})
|
||||
|
||||
it('should not share keys between tabs', () => {
|
||||
const { selectedTab, expandedKeys } =
|
||||
createExpandedKeysState('essentials')
|
||||
|
||||
expandedKeys.value = ['shared-key']
|
||||
|
||||
selectedTab.value = 'all'
|
||||
expect(expandedKeys.value).toEqual([])
|
||||
|
||||
selectedTab.value = 'custom'
|
||||
expect(expandedKeys.value).toEqual([])
|
||||
|
||||
selectedTab.value = 'essentials'
|
||||
expect(expandedKeys.value).toEqual(['shared-key'])
|
||||
})
|
||||
expect(wrapper.find('[data-testid="essential-panel"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="all-panel"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="custom-panel"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -126,13 +126,15 @@ const filteredNodeDefs = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const sections = computed(() => {
|
||||
const nodes =
|
||||
filteredNodeDefs.value.length > 0
|
||||
? filteredNodeDefs.value
|
||||
: nodeDefStore.visibleNodeDefs
|
||||
const activeNodes = computed(() =>
|
||||
filteredNodeDefs.value.length > 0
|
||||
? filteredNodeDefs.value
|
||||
: nodeDefStore.visibleNodeDefs
|
||||
)
|
||||
|
||||
return nodeOrganizationService.organizeNodesByTab(nodes, 'all')
|
||||
const sections = computed(() => {
|
||||
if (selectedTab.value !== 'all') return []
|
||||
return nodeOrganizationService.organizeNodesByTab(activeNodes.value, 'all')
|
||||
})
|
||||
|
||||
function getFolderIcon(node: TreeNode): string {
|
||||
@@ -183,11 +185,11 @@ const renderedSections = computed(() => {
|
||||
})
|
||||
|
||||
const essentialSections = computed(() => {
|
||||
const nodes =
|
||||
filteredNodeDefs.value.length > 0
|
||||
? filteredNodeDefs.value
|
||||
: nodeDefStore.visibleNodeDefs
|
||||
return nodeOrganizationService.organizeNodesByTab(nodes, 'essentials')
|
||||
if (selectedTab.value !== 'essentials') return []
|
||||
return nodeOrganizationService.organizeNodesByTab(
|
||||
activeNodes.value,
|
||||
'essentials'
|
||||
)
|
||||
})
|
||||
|
||||
const renderedEssentialRoot = computed(() => {
|
||||
@@ -198,12 +200,8 @@ const renderedEssentialRoot = computed(() => {
|
||||
})
|
||||
|
||||
const customSections = computed(() => {
|
||||
const nodes =
|
||||
filteredNodeDefs.value.length > 0
|
||||
? filteredNodeDefs.value
|
||||
: nodeDefStore.visibleNodeDefs
|
||||
|
||||
return nodeOrganizationService.organizeNodesByTab(nodes, 'custom')
|
||||
if (selectedTab.value !== 'custom') return []
|
||||
return nodeOrganizationService.organizeNodesByTab(activeNodes.value, 'custom')
|
||||
})
|
||||
|
||||
const renderedCustomSections = computed(() => {
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<div v-for="(section, index) in sections" :key="section.title ?? index">
|
||||
<h3
|
||||
v-if="section.title"
|
||||
class="px-4 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
||||
class="px-4 py-2 text-xs font-medium tracking-wide text-muted-foreground"
|
||||
>
|
||||
{{ $t(section.title) }}
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<TreeExplorerV2
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<!-- Section header -->
|
||||
<h3
|
||||
v-if="section.title"
|
||||
class="px-4 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
||||
class="px-4 py-2 text-xs font-medium tracking-wide text-muted-foreground"
|
||||
>
|
||||
{{ $t(section.title) }}
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<!-- Section tree -->
|
||||
<TreeExplorerV2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TabsContent value="essential" class="min-h-0 flex-1 overflow-y-auto">
|
||||
<TabsContent value="essentials" class="min-h-0 flex-1 overflow-y-auto">
|
||||
<TreeExplorerV2
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
:root="root"
|
||||
|
||||
@@ -793,8 +793,7 @@
|
||||
"alphabeticalDesc": "Sort alphabetically within groups"
|
||||
},
|
||||
"sections": {
|
||||
"favorites": "Favorites",
|
||||
"basics": "Basics"
|
||||
"favorites": "Favorites"
|
||||
}
|
||||
},
|
||||
"modelLibrary": "Model Library",
|
||||
|
||||
Reference in New Issue
Block a user