mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
Compare commits
3 Commits
v1.45.7
...
glary/node
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d6a743ea2 | ||
|
|
861d3af9c0 | ||
|
|
1f18bd4d6f |
@@ -0,0 +1,32 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
||||
|
||||
test.describe(
|
||||
'Properties panel - Node Info via context menu',
|
||||
{ tag: '@vue-nodes' },
|
||||
() => {
|
||||
let panel: PropertiesPanelHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
panel = new PropertiesPanelHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test('opens the right side panel Info tab when clicked from the node context menu', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await expect(panel.root).toBeHidden()
|
||||
|
||||
const fixture = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await comfyPage.contextMenu.openForVueNode(fixture.header)
|
||||
await comfyPage.contextMenu.clickMenuItemExact('Node Info')
|
||||
|
||||
await expect(panel.root).toBeVisible()
|
||||
await expect(panel.getTab('Info')).toBeVisible()
|
||||
await expect(
|
||||
panel.contentArea.getByRole('heading', { name: 'Inputs' })
|
||||
).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -9,13 +9,13 @@ import { createI18n } from 'vue-i18n'
|
||||
import InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
const { openPanelMock } = vi.hoisted(() => ({
|
||||
openPanelMock: vi.fn()
|
||||
const { showNodeHelpMock } = vi.hoisted(() => ({
|
||||
showNodeHelpMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/rightSidePanelStore', () => ({
|
||||
useRightSidePanelStore: () => ({
|
||||
openPanel: openPanelMock
|
||||
vi.mock('@/composables/graph/useSelectionState', () => ({
|
||||
useSelectionState: () => ({
|
||||
showNodeHelp: showNodeHelpMock
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -53,12 +53,12 @@ describe('InfoButton', () => {
|
||||
})
|
||||
}
|
||||
|
||||
it('should open the info panel on click', async () => {
|
||||
it('should call showNodeHelp on click', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Node Info' }))
|
||||
|
||||
expect(openPanelMock).toHaveBeenCalledWith('info')
|
||||
expect(showNodeHelpMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,18 +15,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { showNodeHelp } = useSelectionState()
|
||||
|
||||
/**
|
||||
* Track node info button click and toggle node help.
|
||||
*/
|
||||
const onInfoClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'selection_toolbox_node_info_opened'
|
||||
})
|
||||
rightSidePanelStore.openPanel('info')
|
||||
showNodeHelp()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -126,6 +126,8 @@ export function useMoreOptionsMenu() {
|
||||
selectedNodes,
|
||||
nodeDef,
|
||||
showNodeHelp,
|
||||
isSingleNode,
|
||||
isSingleSubgraph,
|
||||
hasSubgraphs: hasSubgraphsComputed,
|
||||
hasImageNode,
|
||||
hasOutputNodesSelected,
|
||||
@@ -243,7 +245,8 @@ export function useMoreOptionsMenu() {
|
||||
options.push({ type: 'divider' })
|
||||
|
||||
// Section 4: Node properties (Node Info, Shape, Color)
|
||||
if (nodeDef.value) {
|
||||
// Match the right side panel's Info tab visibility: single non-subgraph node.
|
||||
if (nodeDef.value && isSingleNode.value && !isSingleSubgraph.value) {
|
||||
options.push(getNodeInfoOption(showNodeHelp))
|
||||
}
|
||||
if (groupContext) {
|
||||
|
||||
61
src/composables/graph/useNodeMenuOptions.test.ts
Normal file
61
src/composables/graph/useNodeMenuOptions.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { useNodeMenuOptions } from '@/composables/graph/useNodeMenuOptions'
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as object),
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key.split('.').pop() ?? key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/composables/graph/useNodeCustomization', () => ({
|
||||
useNodeCustomization: () => ({
|
||||
shapeOptions: [],
|
||||
applyShape: vi.fn(),
|
||||
applyColor: vi.fn(),
|
||||
colorOptions: [],
|
||||
isLightTheme: { value: false }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useSelectedNodeActions', () => ({
|
||||
useSelectedNodeActions: () => ({
|
||||
adjustNodeSize: vi.fn(),
|
||||
toggleNodeCollapse: vi.fn(),
|
||||
toggleNodePin: vi.fn(),
|
||||
toggleNodeBypass: vi.fn(),
|
||||
runBranch: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useNodeMenuOptions', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ createSpy: vi.fn }))
|
||||
})
|
||||
|
||||
describe('getNodeInfoOption', () => {
|
||||
test('builds a menu option labeled "Node Info"', () => {
|
||||
const { getNodeInfoOption } = useNodeMenuOptions()
|
||||
const option = getNodeInfoOption(vi.fn())
|
||||
|
||||
expect(option.label).toBe('Node Info')
|
||||
expect(option.icon).toBe('icon-[lucide--info]')
|
||||
})
|
||||
|
||||
test('invokes the supplied showNodeHelp callback when the option is activated', () => {
|
||||
const showNodeHelp = vi.fn()
|
||||
const { getNodeInfoOption } = useNodeMenuOptions()
|
||||
const option = getNodeInfoOption(showNodeHelp)
|
||||
|
||||
option.action?.()
|
||||
|
||||
expect(showNodeHelp).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,9 +3,10 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
|
||||
import {
|
||||
@@ -13,14 +14,10 @@ import {
|
||||
createMockPositionable
|
||||
} from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
// Mock composables
|
||||
vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({
|
||||
useNodeLibrarySidebarTab: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isLGraphNode: vi.fn(),
|
||||
isImageNode: vi.fn()
|
||||
isImageNode: vi.fn(),
|
||||
isLoad3dNode: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/nodeFilterUtil', () => ({
|
||||
@@ -43,22 +40,13 @@ describe('useSelectionState', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create testing Pinia instance
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn
|
||||
createSpy: vi.fn,
|
||||
stubActions: false
|
||||
})
|
||||
)
|
||||
|
||||
// Setup mock composables
|
||||
vi.mocked(useNodeLibrarySidebarTab).mockReturnValue({
|
||||
id: 'node-library-tab',
|
||||
title: 'Node Library',
|
||||
type: 'custom',
|
||||
render: () => null
|
||||
} as ReturnType<typeof useNodeLibrarySidebarTab>)
|
||||
|
||||
// Setup mock utility functions
|
||||
vi.mocked(isLGraphNode).mockImplementation((item: unknown) => {
|
||||
const typedItem = item as { isNode?: boolean }
|
||||
return typedItem?.isNode !== false
|
||||
@@ -187,4 +175,110 @@ describe('useSelectionState', () => {
|
||||
expect(newIsPinned).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('showNodeHelp', () => {
|
||||
test('opens the right side panel Info tab when a single node is selected', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const node = createMockLGraphNode({ id: 10, type: 'KSampler' })
|
||||
canvasStore.$state.selectedItems = [node]
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue({
|
||||
nodePath: 'KSampler'
|
||||
} as ReturnType<typeof nodeDefStore.fromLGraphNode>)
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const openPanelSpy = vi
|
||||
.spyOn(rightSidePanelStore, 'openPanel')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { showNodeHelp } = useSelectionState()
|
||||
showNodeHelp()
|
||||
|
||||
expect(openPanelSpy).toHaveBeenCalledWith('info')
|
||||
})
|
||||
|
||||
test('does nothing when no single node is selected', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
canvasStore.$state.selectedItems = []
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const openPanelSpy = vi
|
||||
.spyOn(rightSidePanelStore, 'openPanel')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { showNodeHelp } = useSelectionState()
|
||||
showNodeHelp()
|
||||
|
||||
expect(openPanelSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does nothing when selection includes more than one item', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const node = createMockLGraphNode({ id: 11, type: 'KSampler' })
|
||||
const otherItem = { id: 12, isNode: false } as unknown as Parameters<
|
||||
typeof canvasStore.$state.selectedItems.push
|
||||
>[0]
|
||||
canvasStore.$state.selectedItems = [node, otherItem]
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue({
|
||||
nodePath: 'KSampler'
|
||||
} as ReturnType<typeof nodeDefStore.fromLGraphNode>)
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const openPanelSpy = vi
|
||||
.spyOn(rightSidePanelStore, 'openPanel')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { showNodeHelp } = useSelectionState()
|
||||
showNodeHelp()
|
||||
|
||||
expect(openPanelSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does nothing when the selected node is a subgraph node', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const subgraphNode = createMockLGraphNode({
|
||||
id: 13,
|
||||
type: 'Subgraph'
|
||||
})
|
||||
Object.assign(subgraphNode, { isSubgraphNode: () => true })
|
||||
canvasStore.$state.selectedItems = [subgraphNode]
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue({
|
||||
nodePath: 'Subgraph'
|
||||
} as ReturnType<typeof nodeDefStore.fromLGraphNode>)
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const openPanelSpy = vi
|
||||
.spyOn(rightSidePanelStore, 'openPanel')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { showNodeHelp } = useSelectionState()
|
||||
showNodeHelp()
|
||||
|
||||
expect(openPanelSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does nothing when no node definition is available', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const node = createMockLGraphNode({ id: 14, type: 'UnknownType' })
|
||||
canvasStore.$state.selectedItems = [node]
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(null)
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const openPanelSpy = vi
|
||||
.spyOn(rightSidePanelStore, 'openPanel')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { showNodeHelp } = useSelectionState()
|
||||
showNodeHelp()
|
||||
|
||||
expect(openPanelSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { isImageNode, isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
|
||||
|
||||
@@ -25,9 +23,7 @@ export interface NodeSelectionState {
|
||||
export function useSelectionState() {
|
||||
const canvasStore = useCanvasStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
const nodeHelpStore = useNodeHelpStore()
|
||||
const { id: nodeLibraryTabId } = useNodeLibrarySidebarTab()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
|
||||
const { selectedItems } = storeToRefs(canvasStore)
|
||||
|
||||
@@ -98,27 +94,10 @@ export function useSelectionState() {
|
||||
const computeSelectionFlags = (): NodeSelectionState =>
|
||||
computeSelectionStatesFromNodes(selectedNodes.value)
|
||||
|
||||
/** Toggle node help sidebar/panel for the single selected node (if any). */
|
||||
/** Open the right side panel Info tab for the selected node. */
|
||||
const showNodeHelp = () => {
|
||||
const def = nodeDef.value
|
||||
if (!def) return
|
||||
|
||||
const isSidebarActive =
|
||||
sidebarTabStore.activeSidebarTabId === nodeLibraryTabId
|
||||
const currentHelpNode = nodeHelpStore.currentHelpNode
|
||||
const isSameNodeHelpOpen =
|
||||
isSidebarActive &&
|
||||
nodeHelpStore.isHelpOpen &&
|
||||
currentHelpNode?.nodePath === def.nodePath
|
||||
|
||||
if (isSameNodeHelpOpen) {
|
||||
nodeHelpStore.closeHelp()
|
||||
sidebarTabStore.toggleSidebarTab(nodeLibraryTabId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSidebarActive) sidebarTabStore.toggleSidebarTab(nodeLibraryTabId)
|
||||
nodeHelpStore.openHelp(def)
|
||||
if (!nodeDef.value || !isSingleNode.value || isSingleSubgraph.value) return
|
||||
rightSidePanelStore.openPanel('info')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user