mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 15:10:06 +00:00
## Summary - Adds subgraph title button to Vue node headers (matching LiteGraph behavior) - Fixes Vue node lifecycle issues during subgraph navigation and tab switching - Extracts reusable `useSubgraphNavigation` composable with callback-based API - Adds comprehensive tests for subgraph functionality - Ensures proper graph context restoration during tab switches https://github.com/user-attachments/assets/fd4ff16a-4071-4da6-903f-b2be8dd6e672 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5572-feat-Add-Vue-node-subgraph-title-button-with-lifecycle-management-26f6d73d365081bfbd9cfd7d2775e1ef) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
224 lines
5.7 KiB
TypeScript
224 lines
5.7 KiB
TypeScript
/**
|
|
* Tests for NodeHeader subgraph functionality
|
|
*/
|
|
import { createTestingPinia } from '@pinia/testing'
|
|
import { mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
|
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
|
|
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/scripts/app', () => ({
|
|
app: {
|
|
graph: null as any
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/utils/graphTraversalUtil', () => ({
|
|
getNodeByLocatorId: vi.fn(),
|
|
getLocatorIdFromNodeData: vi.fn((nodeData) =>
|
|
nodeData.subgraphId
|
|
? `${nodeData.subgraphId}:${String(nodeData.id)}`
|
|
: String(nodeData.id)
|
|
)
|
|
}))
|
|
|
|
vi.mock('@/composables/useErrorHandling', () => ({
|
|
useErrorHandling: () => ({
|
|
toastErrorHandler: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: () => ({
|
|
t: vi.fn((key) => key)
|
|
}),
|
|
createI18n: vi.fn(() => ({
|
|
global: {
|
|
t: vi.fn((key) => key)
|
|
}
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/i18n', () => ({
|
|
st: vi.fn((key) => key),
|
|
t: vi.fn((key) => key),
|
|
i18n: {
|
|
global: {
|
|
t: vi.fn((key) => key)
|
|
}
|
|
}
|
|
}))
|
|
|
|
describe('NodeHeader - Subgraph Functionality', () => {
|
|
// Helper to setup common mocks
|
|
const setupMocks = async (isSubgraph = true, hasGraph = true) => {
|
|
const { app } = await import('@/scripts/app')
|
|
|
|
if (hasGraph) {
|
|
;(app as any).graph = { rootGraph: {} }
|
|
} else {
|
|
;(app as any).graph = null
|
|
}
|
|
|
|
vi.mocked(getNodeByLocatorId).mockReturnValue({
|
|
isSubgraphNode: () => isSubgraph
|
|
} as any)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
const createMockNodeData = (
|
|
id: string,
|
|
subgraphId?: string
|
|
): VueNodeData => ({
|
|
id,
|
|
title: 'Test Node',
|
|
type: 'TestNode',
|
|
mode: 0,
|
|
selected: false,
|
|
executing: false,
|
|
subgraphId,
|
|
widgets: [],
|
|
inputs: [],
|
|
outputs: [],
|
|
hasErrors: false,
|
|
flags: {}
|
|
})
|
|
|
|
const createWrapper = (props = {}) => {
|
|
return mount(NodeHeader, {
|
|
props,
|
|
global: {
|
|
plugins: [createTestingPinia({ createSpy: vi.fn })],
|
|
mocks: {
|
|
$t: vi.fn((key: string) => key),
|
|
$primevue: { config: {} }
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
it('should show subgraph button for subgraph nodes', async () => {
|
|
await setupMocks(true) // isSubgraph = true
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1'),
|
|
readonly: false
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
expect(subgraphButton.exists()).toBe(true)
|
|
})
|
|
|
|
it('should not show subgraph button for regular nodes', async () => {
|
|
await setupMocks(false) // isSubgraph = false
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1'),
|
|
readonly: false
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
expect(subgraphButton.exists()).toBe(false)
|
|
})
|
|
|
|
it('should not show subgraph button in readonly mode', async () => {
|
|
await setupMocks(true) // isSubgraph = true
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1'),
|
|
readonly: true
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
expect(subgraphButton.exists()).toBe(false)
|
|
})
|
|
|
|
it('should emit enter-subgraph event when button is clicked', async () => {
|
|
await setupMocks(true) // isSubgraph = true
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1'),
|
|
readonly: false
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
await subgraphButton.trigger('click')
|
|
|
|
expect(wrapper.emitted('enter-subgraph')).toBeTruthy()
|
|
expect(wrapper.emitted('enter-subgraph')).toHaveLength(1)
|
|
})
|
|
|
|
it('should handle subgraph context correctly', async () => {
|
|
await setupMocks(true) // isSubgraph = true
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1', 'subgraph-id'),
|
|
readonly: false
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
// Should call getNodeByLocatorId with correct locator ID
|
|
expect(vi.mocked(getNodeByLocatorId)).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
'subgraph-id:test-node-1'
|
|
)
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
expect(subgraphButton.exists()).toBe(true)
|
|
})
|
|
|
|
it('should handle missing graph gracefully', async () => {
|
|
await setupMocks(true, false) // isSubgraph = true, hasGraph = false
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1'),
|
|
readonly: false
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
expect(subgraphButton.exists()).toBe(false)
|
|
})
|
|
|
|
it('should prevent event propagation on double click', async () => {
|
|
await setupMocks(true) // isSubgraph = true
|
|
|
|
const wrapper = createWrapper({
|
|
nodeData: createMockNodeData('test-node-1'),
|
|
readonly: false
|
|
})
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
|
|
// Mock event object
|
|
const mockEvent = {
|
|
stopPropagation: vi.fn()
|
|
}
|
|
|
|
// Trigger dblclick event
|
|
await subgraphButton.trigger('dblclick', mockEvent)
|
|
|
|
// Should prevent propagation (handled by @dblclick.stop directive)
|
|
// This is tested by ensuring the component doesn't error and renders correctly
|
|
expect(subgraphButton.exists()).toBe(true)
|
|
})
|
|
})
|