Compare commits

...

3 Commits

Author SHA1 Message Date
Glary-Bot
4d6a743ea2 test: add explicit coverage for null nodeDef in showNodeHelp 2026-05-12 04:53:19 +00:00
Glary-Bot
861d3af9c0 fix: gate Node Info on right-panel Info-tab visibility predicate
Match the right side panel's exact visibility rule (single, non-subgraph
node) so the context menu entry never appears when the panel cannot
render the Info tab.
2026-05-06 07:17:51 +00:00
Glary-Bot
1f18bd4d6f fix: route Node Info context menu to right side panel Info tab
The Vue right-click context menu's Node Info entry opened the legacy
left-side Node Library sidebar with help content. Reuse the right
side panel Info tab flow already used by the SelectionToolbox info
button so both surfaces share a single code path.
2026-05-06 03:46:15 +00:00
7 changed files with 224 additions and 58 deletions

View File

@@ -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()
})
}
)

View File

@@ -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)
})
})

View File

@@ -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>

View File

@@ -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) {

View 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)
})
})
})

View File

@@ -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()
})
})
})

View File

@@ -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 {