mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-02 03:30:04 +00:00
Add functions for mapping node locations to an id
These locations differ from either local ids (like `5`) or execution ids (like `3:4:5`) in that all nodes which are 'linked' such that changes to one node will apply changes to other nodes will map to the same id. This is specifically relevant for multiple instances of the same subgraph. These IDs will actually get used in a followup commit
This commit is contained in:
@@ -2,10 +2,11 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
// Remove any previous global types
|
||||
declare global {
|
||||
// Empty interface to override any previous declarations
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface Window {}
|
||||
}
|
||||
|
||||
@@ -80,3 +81,93 @@ describe('executionStore - display_component handling', () => {
|
||||
expect(mockShowChatHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
let workflowStore: ReturnType<typeof useWorkflowStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
store = useExecutionStore()
|
||||
workflowStore = useWorkflowStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('executionIdToNodeLocatorId', () => {
|
||||
it('should convert hierarchical execution ID to NodeLocatorId', () => {
|
||||
const mockNodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
vi.spyOn(workflowStore, 'hierarchicalIdToNodeLocatorId').mockReturnValue(
|
||||
mockNodeLocatorId as any
|
||||
)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId('123:456')
|
||||
|
||||
expect(workflowStore.hierarchicalIdToNodeLocatorId).toHaveBeenCalledWith(
|
||||
'123:456'
|
||||
)
|
||||
expect(result).toBe(mockNodeLocatorId)
|
||||
})
|
||||
|
||||
it('should convert simple node ID to NodeLocatorId', () => {
|
||||
const mockNodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
|
||||
vi.spyOn(workflowStore, 'nodeIdToNodeLocatorId').mockReturnValue(
|
||||
mockNodeLocatorId as any
|
||||
)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId('123')
|
||||
|
||||
expect(workflowStore.nodeIdToNodeLocatorId).toHaveBeenCalledWith('123')
|
||||
expect(result).toBe(mockNodeLocatorId)
|
||||
})
|
||||
|
||||
it('should handle numeric node IDs', () => {
|
||||
const mockNodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
|
||||
vi.spyOn(workflowStore, 'nodeIdToNodeLocatorId').mockReturnValue(
|
||||
mockNodeLocatorId as any
|
||||
)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId(123)
|
||||
|
||||
expect(workflowStore.nodeIdToNodeLocatorId).toHaveBeenCalledWith('123')
|
||||
expect(result).toBe(mockNodeLocatorId)
|
||||
})
|
||||
|
||||
it('should return null when conversion fails', () => {
|
||||
vi.spyOn(workflowStore, 'hierarchicalIdToNodeLocatorId').mockReturnValue(
|
||||
null
|
||||
)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId('123:456')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToExecutionId', () => {
|
||||
it('should convert NodeLocatorId to hierarchical execution ID', () => {
|
||||
const mockHierarchicalId = '123:456'
|
||||
vi.spyOn(workflowStore, 'nodeLocatorIdToHierarchicalId').mockReturnValue(
|
||||
mockHierarchicalId as any
|
||||
)
|
||||
|
||||
const result = store.nodeLocatorIdToExecutionId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
|
||||
expect(workflowStore.nodeLocatorIdToHierarchicalId).toHaveBeenCalledWith(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe(mockHierarchicalId)
|
||||
})
|
||||
|
||||
it('should return null when conversion fails', () => {
|
||||
vi.spyOn(workflowStore, 'nodeLocatorIdToHierarchicalId').mockReturnValue(
|
||||
null
|
||||
)
|
||||
|
||||
const result = store.nodeLocatorIdToExecutionId('invalid:format')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Subgraph } from '@comfyorg/litegraph'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
useWorkflowBookmarkStore,
|
||||
useWorkflowStore
|
||||
} from '@/stores/workflowStore'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
// Add mock for api at the top of the file
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
@@ -26,10 +28,15 @@ vi.mock('@/scripts/api', () => ({
|
||||
// Mock comfyApp globally for the store setup
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: null // Start with canvas potentially undefined or null
|
||||
canvas: {} // Start with empty canvas object
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock isSubgraph
|
||||
vi.mock('@/utils/typeGuardUtil', () => ({
|
||||
isSubgraph: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
describe('useWorkflowStore', () => {
|
||||
let store: ReturnType<typeof useWorkflowStore>
|
||||
let bookmarkStore: ReturnType<typeof useWorkflowBookmarkStore>
|
||||
@@ -569,4 +576,131 @@ describe('useWorkflowStore', () => {
|
||||
expect(store.activeSubgraph).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeLocatorId conversions', () => {
|
||||
beforeEach(() => {
|
||||
// Setup mock graph structure with subgraphs
|
||||
const mockSubgraph = {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
_nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
id: 123,
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: mockSubgraph
|
||||
}
|
||||
|
||||
const mockRootGraph = {
|
||||
_nodes: [mockNode],
|
||||
subgraphs: new Map([[mockSubgraph.id, mockSubgraph]]),
|
||||
getNodeById: (id: string | number) => {
|
||||
if (String(id) === '123') return mockNode
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
vi.mocked(comfyApp).graph = mockRootGraph as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
|
||||
store.activeSubgraph = mockSubgraph as any
|
||||
})
|
||||
|
||||
describe('nodeIdToNodeLocatorId', () => {
|
||||
it('should convert node ID to NodeLocatorId for subgraph nodes', () => {
|
||||
const result = store.nodeIdToNodeLocatorId(456)
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should return simple node ID for root graph nodes', () => {
|
||||
store.activeSubgraph = undefined
|
||||
const result = store.nodeIdToNodeLocatorId(123)
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should use provided subgraph instead of active one', () => {
|
||||
const customSubgraph = {
|
||||
id: 'custom-uuid-1234-5678-90ab-cdef12345678'
|
||||
} as any
|
||||
const result = store.nodeIdToNodeLocatorId(789, customSubgraph)
|
||||
expect(result).toBe('custom-uuid-1234-5678-90ab-cdef12345678:789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hierarchicalIdToNodeLocatorId', () => {
|
||||
it('should convert hierarchical ID to NodeLocatorId', () => {
|
||||
const result = store.hierarchicalIdToNodeLocatorId('123:456')
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should return simple node ID for root level nodes', () => {
|
||||
const result = store.hierarchicalIdToNodeLocatorId('123')
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null for invalid hierarchical IDs', () => {
|
||||
const result = store.hierarchicalIdToNodeLocatorId('999:456')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToNodeId', () => {
|
||||
it('should extract node ID from NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe(456)
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const result = store.nodeLocatorIdToNodeId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:node_1'
|
||||
)
|
||||
expect(result).toBe('node_1')
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = store.nodeLocatorIdToNodeId('123')
|
||||
expect(result).toBe(123)
|
||||
|
||||
const stringResult = store.nodeLocatorIdToNodeId('node_1')
|
||||
expect(stringResult).toBe('node_1')
|
||||
})
|
||||
|
||||
it('should return null for invalid NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeId('invalid:format')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToHierarchicalId', () => {
|
||||
it('should convert NodeLocatorId to hierarchical ID', () => {
|
||||
// Need to mock isSubgraph to identify our mockSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation((obj): obj is Subgraph => {
|
||||
return obj === store.activeSubgraph
|
||||
})
|
||||
|
||||
const result = store.nodeLocatorIdToHierarchicalId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe('123:456')
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = store.nodeLocatorIdToHierarchicalId('123')
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null for unknown subgraph UUID', () => {
|
||||
const result = store.nodeLocatorIdToHierarchicalId(
|
||||
'unknown-uuid-1234-5678-90ab-cdef12345678:456'
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for invalid NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToHierarchicalId('invalid:format')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
207
tests-ui/tests/types/nodeIdentification.test.ts
Normal file
207
tests-ui/tests/types/nodeIdentification.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import {
|
||||
type NodeLocatorId,
|
||||
createHierarchicalNodeId,
|
||||
createNodeLocatorId,
|
||||
isHierarchicalNodeId,
|
||||
isNodeLocatorId,
|
||||
parseHierarchicalNodeId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
|
||||
describe('nodeIdentification', () => {
|
||||
describe('NodeLocatorId', () => {
|
||||
const validUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const validNodeId = '123'
|
||||
const validNodeLocatorId = `${validUuid}:${validNodeId}` as NodeLocatorId
|
||||
|
||||
describe('isNodeLocatorId', () => {
|
||||
it('should return true for valid NodeLocatorId', () => {
|
||||
expect(isNodeLocatorId(validNodeLocatorId)).toBe(true)
|
||||
expect(isNodeLocatorId(`${validUuid}:456`)).toBe(true)
|
||||
expect(isNodeLocatorId(`${validUuid}:node_1`)).toBe(true)
|
||||
// Simple node IDs (root graph)
|
||||
expect(isNodeLocatorId('123')).toBe(true)
|
||||
expect(isNodeLocatorId('node_1')).toBe(true)
|
||||
expect(isNodeLocatorId('5')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for invalid formats', () => {
|
||||
expect(isNodeLocatorId('123:456')).toBe(false) // No UUID in first part
|
||||
expect(isNodeLocatorId('not-a-uuid:123')).toBe(false)
|
||||
expect(isNodeLocatorId('')).toBe(false) // Empty string
|
||||
expect(isNodeLocatorId(':123')).toBe(false) // Empty UUID
|
||||
expect(isNodeLocatorId(`${validUuid}:`)).toBe(false) // Empty node ID
|
||||
expect(isNodeLocatorId(`${validUuid}:123:456`)).toBe(false) // Too many parts
|
||||
expect(isNodeLocatorId(123)).toBe(false) // Not a string
|
||||
expect(isNodeLocatorId(null)).toBe(false)
|
||||
expect(isNodeLocatorId(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate UUID format correctly', () => {
|
||||
// Valid UUID formats
|
||||
expect(
|
||||
isNodeLocatorId('00000000-0000-0000-0000-000000000000:123')
|
||||
).toBe(true)
|
||||
expect(
|
||||
isNodeLocatorId('A1B2C3D4-E5F6-7890-ABCD-EF1234567890:123')
|
||||
).toBe(true)
|
||||
|
||||
// Invalid UUID formats
|
||||
expect(isNodeLocatorId('00000000-0000-0000-0000-00000000000:123')).toBe(
|
||||
false
|
||||
) // Too short
|
||||
expect(
|
||||
isNodeLocatorId('00000000-0000-0000-0000-0000000000000:123')
|
||||
).toBe(false) // Too long
|
||||
expect(
|
||||
isNodeLocatorId('00000000_0000_0000_0000_000000000000:123')
|
||||
).toBe(false) // Wrong separator
|
||||
expect(
|
||||
isNodeLocatorId('g0000000-0000-0000-0000-000000000000:123')
|
||||
).toBe(false) // Invalid hex
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseNodeLocatorId', () => {
|
||||
it('should parse valid NodeLocatorId', () => {
|
||||
const result = parseNodeLocatorId(validNodeLocatorId)
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: validUuid,
|
||||
localNodeId: 123
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const stringNodeId = `${validUuid}:node_1`
|
||||
const result = parseNodeLocatorId(stringNodeId)
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: validUuid,
|
||||
localNodeId: 'node_1'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = parseNodeLocatorId('123')
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: null,
|
||||
localNodeId: 123
|
||||
})
|
||||
|
||||
const stringResult = parseNodeLocatorId('node_1')
|
||||
expect(stringResult).toEqual({
|
||||
subgraphUuid: null,
|
||||
localNodeId: 'node_1'
|
||||
})
|
||||
})
|
||||
|
||||
it('should return null for invalid formats', () => {
|
||||
expect(parseNodeLocatorId('123:456')).toBeNull() // No UUID in first part
|
||||
expect(parseNodeLocatorId('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createNodeLocatorId', () => {
|
||||
it('should create NodeLocatorId from components', () => {
|
||||
const result = createNodeLocatorId(validUuid, 123)
|
||||
expect(result).toBe(validNodeLocatorId)
|
||||
expect(isNodeLocatorId(result)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const result = createNodeLocatorId(validUuid, 'node_1')
|
||||
expect(result).toBe(`${validUuid}:node_1`)
|
||||
expect(isNodeLocatorId(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('HierarchicalNodeId', () => {
|
||||
describe('isHierarchicalNodeId', () => {
|
||||
it('should return true for hierarchical IDs', () => {
|
||||
expect(isHierarchicalNodeId('123:456')).toBe(true)
|
||||
expect(isHierarchicalNodeId('123:456:789')).toBe(true)
|
||||
expect(isHierarchicalNodeId('node_1:node_2')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-hierarchical IDs', () => {
|
||||
expect(isHierarchicalNodeId('123')).toBe(false)
|
||||
expect(isHierarchicalNodeId('node_1')).toBe(false)
|
||||
expect(isHierarchicalNodeId('')).toBe(false)
|
||||
expect(isHierarchicalNodeId(123)).toBe(false)
|
||||
expect(isHierarchicalNodeId(null)).toBe(false)
|
||||
expect(isHierarchicalNodeId(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseHierarchicalNodeId', () => {
|
||||
it('should parse hierarchical IDs correctly', () => {
|
||||
expect(parseHierarchicalNodeId('123:456')).toEqual([123, 456])
|
||||
expect(parseHierarchicalNodeId('123:456:789')).toEqual([123, 456, 789])
|
||||
expect(parseHierarchicalNodeId('node_1:node_2')).toEqual([
|
||||
'node_1',
|
||||
'node_2'
|
||||
])
|
||||
expect(parseHierarchicalNodeId('123:node_2:456')).toEqual([
|
||||
123,
|
||||
'node_2',
|
||||
456
|
||||
])
|
||||
})
|
||||
|
||||
it('should return null for non-hierarchical IDs', () => {
|
||||
expect(parseHierarchicalNodeId('123')).toBeNull()
|
||||
expect(parseHierarchicalNodeId('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createHierarchicalNodeId', () => {
|
||||
it('should create hierarchical IDs from node arrays', () => {
|
||||
expect(createHierarchicalNodeId([123, 456])).toBe('123:456')
|
||||
expect(createHierarchicalNodeId([123, 456, 789])).toBe('123:456:789')
|
||||
expect(createHierarchicalNodeId(['node_1', 'node_2'])).toBe(
|
||||
'node_1:node_2'
|
||||
)
|
||||
expect(createHierarchicalNodeId([123, 'node_2', 456])).toBe(
|
||||
'123:node_2:456'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle single node ID', () => {
|
||||
const result = createHierarchicalNodeId([123])
|
||||
expect(result).toBe('123')
|
||||
// Single node IDs are not hierarchical
|
||||
expect(isHierarchicalNodeId(result)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
expect(createHierarchicalNodeId([])).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration tests', () => {
|
||||
it('should round-trip NodeLocatorId correctly', () => {
|
||||
const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const nodeId: NodeId = 123
|
||||
|
||||
const locatorId = createNodeLocatorId(uuid, nodeId)
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
|
||||
expect(parsed).toBeTruthy()
|
||||
expect(parsed!.subgraphUuid).toBe(uuid)
|
||||
expect(parsed!.localNodeId).toBe(nodeId)
|
||||
})
|
||||
|
||||
it('should round-trip HierarchicalNodeId correctly', () => {
|
||||
const nodeIds: NodeId[] = [123, 'node_2', 456]
|
||||
|
||||
const hierarchicalId = createHierarchicalNodeId(nodeIds)
|
||||
const parsed = parseHierarchicalNodeId(hierarchicalId)
|
||||
|
||||
expect(parsed).toEqual(nodeIds)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user