mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
1115 lines
31 KiB
TypeScript
1115 lines
31 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
import { ensureCorrectLayoutScale } from '../src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
|
|
import type { LGraph, RendererType } from '../src/lib/litegraph/src/LGraph'
|
|
import { LiteGraph } from '../src/lib/litegraph/src/litegraph'
|
|
|
|
// Mock dependencies
|
|
vi.mock('../src/composables/useVueFeatureFlags', () => ({
|
|
useVueFeatureFlags: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../src/platform/settings/settingStore', () => ({
|
|
useSettingStore: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../src/stores/layoutStore', () => ({
|
|
layoutStore: {
|
|
batchUpdateNodeBounds: vi.fn()
|
|
}
|
|
}))
|
|
|
|
vi.mock('../src/stores/layoutMutations', () => ({
|
|
useLayoutMutations: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../src/scripts/app', () => ({
|
|
comfyApp: {
|
|
canvas: null
|
|
}
|
|
}))
|
|
|
|
vi.mock('../src/lib/litegraph/src/measure', () => ({
|
|
createBounds: vi.fn()
|
|
}))
|
|
|
|
import { useVueFeatureFlags } from '../src/composables/useVueFeatureFlags'
|
|
import { useSettingStore } from '../src/platform/settings/settingStore'
|
|
import { layoutStore } from '../src/stores/layoutStore'
|
|
import { useLayoutMutations } from '../src/stores/layoutMutations'
|
|
import { comfyApp } from '../src/scripts/app'
|
|
import { createBounds } from '../src/lib/litegraph/src/measure'
|
|
|
|
describe('ensureCorrectLayoutScale', () => {
|
|
let mockGraph: LGraph
|
|
let mockCanvas: any
|
|
let mockSettingStore: any
|
|
let mockLayoutMutations: any
|
|
|
|
beforeEach(() => {
|
|
// Reset all mocks
|
|
vi.clearAllMocks()
|
|
|
|
// Create a complete mock graph with all required properties
|
|
mockGraph = {
|
|
nodes: [],
|
|
reroutes: new Map(),
|
|
groups: [],
|
|
extra: {
|
|
workflowRendererVersion: undefined
|
|
},
|
|
inputNode: null,
|
|
outputNode: null
|
|
} as any
|
|
|
|
// Mock canvas
|
|
mockCanvas = {
|
|
graph: mockGraph,
|
|
ds: {
|
|
scale: 1.0,
|
|
convertOffsetToCanvas: vi.fn((pos) => pos),
|
|
changeScale: vi.fn()
|
|
}
|
|
}
|
|
|
|
// Mock setting store
|
|
mockSettingStore = {
|
|
get: vi.fn().mockReturnValue(true)
|
|
}
|
|
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
|
|
|
|
// Mock layout mutations
|
|
mockLayoutMutations = {
|
|
moveReroute: vi.fn()
|
|
}
|
|
vi.mocked(useLayoutMutations).mockReturnValue(mockLayoutMutations)
|
|
|
|
// Mock Vue feature flags
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
|
|
// Set comfyApp canvas
|
|
;(comfyApp as any).canvas = mockCanvas
|
|
|
|
// Mock createBounds
|
|
vi.mocked(createBounds).mockReturnValue([0, 0, 100, 100])
|
|
})
|
|
|
|
afterEach(() => {
|
|
;(comfyApp as any).canvas = null
|
|
})
|
|
|
|
describe('autoScaleLayoutSetting disabled', () => {
|
|
it('should return early when autoScaleLayoutSetting is false', () => {
|
|
mockSettingStore.get.mockReturnValue(false)
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(createBounds).not.toHaveBeenCalled()
|
|
expect(mockGraph.extra.workflowRendererVersion).toBeUndefined()
|
|
})
|
|
|
|
it('should return early when autoScaleLayoutSetting is undefined', () => {
|
|
mockSettingStore.get.mockReturnValue(undefined)
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(createBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should return early when autoScaleLayoutSetting is null', () => {
|
|
mockSettingStore.get.mockReturnValue(null)
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(createBounds).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('invalid graph scenarios', () => {
|
|
it('should return early when graph is null', () => {
|
|
ensureCorrectLayoutScale('LG', null as any)
|
|
|
|
expect(createBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should return early when graph is undefined', () => {
|
|
ensureCorrectLayoutScale('LG', undefined as any)
|
|
|
|
expect(createBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should return early when graph has no nodes', () => {
|
|
mockGraph.nodes = null as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(createBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should return early when graph has empty nodes array', () => {
|
|
mockGraph.nodes = []
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(createBounds).toHaveBeenCalled()
|
|
expect(createBounds).toHaveReturnedWith(null)
|
|
})
|
|
|
|
it('should return early when createBounds returns null', () => {
|
|
mockGraph.nodes = [
|
|
{ id: 1, pos: [10, 10], size: [100, 50], width: 100, height: 50 }
|
|
] as any
|
|
vi.mocked(createBounds).mockReturnValue(null)
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(layoutStore.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('default renderer parameter', () => {
|
|
it('should default to LG when no renderer is provided', () => {
|
|
mockGraph.nodes = [
|
|
{ id: 1, pos: [10, 10], size: [100, 50], width: 100, height: 50 }
|
|
] as any
|
|
|
|
ensureCorrectLayoutScale(undefined as any, mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('LG')
|
|
})
|
|
|
|
it('should use provided renderer type when specified', () => {
|
|
mockGraph.nodes = [
|
|
{ id: 1, pos: [10, 10], size: [100, 50], width: 100, height: 50 }
|
|
] as any
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('Vue')
|
|
})
|
|
})
|
|
|
|
describe('no scaling needed scenarios', () => {
|
|
it('should set workflowRendererVersion when LG renderer and Vue nodes disabled', () => {
|
|
mockGraph.nodes = [
|
|
{ id: 1, pos: [10, 10], size: [100, 50], width: 100, height: 50 }
|
|
] as any
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('LG')
|
|
expect(layoutStore.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should set workflowRendererVersion when Vue renderer and Vue nodes enabled', () => {
|
|
mockGraph.nodes = [
|
|
{ id: 1, pos: [10, 10], size: [100, 50], width: 100, height: 50 }
|
|
] as any
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('Vue')
|
|
expect(layoutStore.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should not overwrite existing workflowRendererVersion when no scaling needed', () => {
|
|
mockGraph.nodes = [
|
|
{ id: 1, pos: [10, 10], size: [100, 50], width: 100, height: 50 }
|
|
] as any
|
|
mockGraph.extra.workflowRendererVersion = 'Vue'
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('Vue')
|
|
})
|
|
})
|
|
|
|
describe('upscaling from LG to Vue', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
vi.mocked(createBounds).mockReturnValue([0, 0, 100, 100])
|
|
})
|
|
|
|
it('should scale node positions and sizes by SCALE_FACTOR (1.2)', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Original position (100, 100) relative to origin (0, 0)
|
|
// Scaled by 1.2: (120, 120)
|
|
expect(node.pos[0]).toBeCloseTo(120, 5)
|
|
expect(node.pos[1]).toBeCloseTo(120, 5)
|
|
|
|
// Original size (200, 150) scaled by 1.2: (240, 180)
|
|
expect(node.size[0]).toBeCloseTo(240, 5)
|
|
expect(node.size[1]).toBeCloseTo(180, 5)
|
|
})
|
|
|
|
it('should adjust Y position accounting for NODE_TITLE_HEIGHT during upscaling', () => {
|
|
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Y adjustment: (100 - titleHeight) scaled, then no additional adjustment
|
|
const expectedY = (100 - titleHeight) * 1.2
|
|
expect(node.pos[1]).toBeCloseTo(expectedY, 5)
|
|
})
|
|
|
|
it('should handle multiple nodes correctly', () => {
|
|
const nodes = [
|
|
{ id: 1, pos: [0, 0], size: [100, 100], width: 100, height: 100 },
|
|
{ id: 2, pos: [200, 200], size: [150, 100], width: 150, height: 100 },
|
|
{ id: 3, pos: [400, 100], size: [120, 80], width: 120, height: 80 }
|
|
]
|
|
mockGraph.nodes = nodes as any
|
|
vi.mocked(createBounds).mockReturnValue([0, 0, 520, 300])
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Verify all nodes were scaled
|
|
nodes.forEach((node) => {
|
|
expect(node.size[0]).toBeGreaterThan(100)
|
|
expect(node.size[1]).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
it('should update layout store with batched node bounds for active graph', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockCanvas.graph = mockGraph
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(layoutStore.batchUpdateNodeBounds).toHaveBeenCalledTimes(1)
|
|
expect(layoutStore.batchUpdateNodeBounds).toHaveBeenCalledWith(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
nodeId: '1',
|
|
bounds: expect.objectContaining({
|
|
x: expect.any(Number),
|
|
y: expect.any(Number),
|
|
width: expect.any(Number),
|
|
height: expect.any(Number)
|
|
})
|
|
})
|
|
])
|
|
)
|
|
})
|
|
|
|
it('should not update layout store for non-active graphs', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const targetGraph = {
|
|
nodes: [node],
|
|
reroutes: new Map(),
|
|
groups: [],
|
|
extra: {}
|
|
} as any
|
|
|
|
ensureCorrectLayoutScale('LG', targetGraph)
|
|
|
|
expect(layoutStore.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should set workflowRendererVersion to Vue after upscaling', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('Vue')
|
|
})
|
|
})
|
|
|
|
describe('downscaling from Vue to LG', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
vi.mocked(createBounds).mockReturnValue([0, 0, 120, 120])
|
|
})
|
|
|
|
it('should scale node positions and sizes by 1/SCALE_FACTOR (1/1.2)', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
// Scaled by 1/1.2: approximately (100, 100)
|
|
expect(node.pos[0]).toBeCloseTo(100, 5)
|
|
expect(node.pos[1]).toBeCloseTo(100, 5)
|
|
|
|
// Size scaled by 1/1.2 and adjusted for title height
|
|
expect(node.size[0]).toBeCloseTo(200, 5)
|
|
expect(node.size[1]).toBeCloseTo(150 - LiteGraph.NODE_TITLE_HEIGHT, 5)
|
|
})
|
|
|
|
it('should adjust Y position accounting for NODE_TITLE_HEIGHT during downscaling', () => {
|
|
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
// Y should be scaled down and then adjusted by title height
|
|
const expectedY = (120 / 1.2) + titleHeight
|
|
expect(node.pos[1]).toBeCloseTo(expectedY, 5)
|
|
})
|
|
|
|
it('should reduce height by NODE_TITLE_HEIGHT during downscaling', () => {
|
|
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
const expectedHeight = (180 / 1.2) - titleHeight
|
|
expect(node.size[1]).toBeCloseTo(expectedHeight, 5)
|
|
})
|
|
|
|
it('should set workflowRendererVersion to LG after downscaling', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('LG')
|
|
})
|
|
})
|
|
|
|
describe('reroute handling', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
})
|
|
|
|
it('should scale reroute positions during upscaling', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const reroute = { id: 'r1', pos: [150, 150] }
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.reroutes = new Map([['r1', reroute]]) as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Reroute position should be scaled: (150 * 1.2, 150 * 1.2) = (180, 180)
|
|
expect(reroute.pos[0]).toBeCloseTo(180, 5)
|
|
expect(reroute.pos[1]).toBeCloseTo(180, 5)
|
|
})
|
|
|
|
it('should call moveReroute for active graph with Vue nodes enabled', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const reroute = { id: 'r1', pos: [150, 150] }
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.reroutes = new Map([['r1', reroute]]) as any
|
|
mockCanvas.graph = mockGraph
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(mockLayoutMutations.moveReroute).toHaveBeenCalledWith(
|
|
'r1',
|
|
expect.objectContaining({ x: expect.any(Number), y: expect.any(Number) }),
|
|
expect.objectContaining({ x: 150, y: 150 })
|
|
)
|
|
})
|
|
|
|
it('should not call moveReroute for non-active graph', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const reroute = { id: 'r1', pos: [150, 150] }
|
|
const targetGraph = {
|
|
nodes: [node],
|
|
reroutes: new Map([['r1', reroute]]),
|
|
groups: [],
|
|
extra: {}
|
|
} as any
|
|
|
|
ensureCorrectLayoutScale('LG', targetGraph)
|
|
|
|
expect(mockLayoutMutations.moveReroute).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should not call moveReroute when Vue nodes are disabled', () => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
const reroute = { id: 'r1', pos: [180, 180] }
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.reroutes = new Map([['r1', reroute]]) as any
|
|
mockCanvas.graph = mockGraph
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(mockLayoutMutations.moveReroute).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle multiple reroutes correctly', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const reroutes = new Map([
|
|
['r1', { id: 'r1', pos: [150, 150] }],
|
|
['r2', { id: 'r2', pos: [200, 200] }],
|
|
['r3', { id: 'r3', pos: [250, 250] }]
|
|
])
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.reroutes = reroutes as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Verify all reroutes were scaled
|
|
reroutes.forEach((reroute) => {
|
|
expect(reroute.pos[0]).toBeGreaterThan(150)
|
|
expect(reroute.pos[1]).toBeGreaterThan(150)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('subgraph IO node handling', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
})
|
|
|
|
it('should scale input node position and size', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const inputNode = {
|
|
pos: [50, 50],
|
|
size: [100, 80]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.inputNode = inputNode as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(inputNode.pos[0]).toBeCloseTo(60, 5)
|
|
expect(inputNode.pos[1]).toBeCloseTo(60, 5)
|
|
expect(inputNode.size[0]).toBeCloseTo(120, 5)
|
|
expect(inputNode.size[1]).toBeCloseTo(96, 5)
|
|
})
|
|
|
|
it('should scale output node position and size', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const outputNode = {
|
|
pos: [300, 300],
|
|
size: [100, 80]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.outputNode = outputNode as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(outputNode.pos[0]).toBeCloseTo(360, 5)
|
|
expect(outputNode.pos[1]).toBeCloseTo(360, 5)
|
|
expect(outputNode.size[0]).toBeCloseTo(120, 5)
|
|
expect(outputNode.size[1]).toBeCloseTo(96, 5)
|
|
})
|
|
|
|
it('should scale both input and output nodes when present', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const inputNode = {
|
|
pos: [50, 50],
|
|
size: [100, 80]
|
|
}
|
|
const outputNode = {
|
|
pos: [300, 300],
|
|
size: [100, 80]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.inputNode = inputNode as any
|
|
mockGraph.outputNode = outputNode as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(inputNode.pos[0]).toBeCloseTo(60, 5)
|
|
expect(outputNode.pos[0]).toBeCloseTo(360, 5)
|
|
})
|
|
|
|
it('should handle missing input node gracefully', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const outputNode = {
|
|
pos: [300, 300],
|
|
size: [100, 80]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.inputNode = null
|
|
mockGraph.outputNode = outputNode as any
|
|
|
|
expect(() => ensureCorrectLayoutScale('LG', mockGraph)).not.toThrow()
|
|
expect(outputNode.pos[0]).toBeCloseTo(360, 5)
|
|
})
|
|
|
|
it('should handle missing output node gracefully', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const inputNode = {
|
|
pos: [50, 50],
|
|
size: [100, 80]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.inputNode = inputNode as any
|
|
mockGraph.outputNode = null
|
|
|
|
expect(() => ensureCorrectLayoutScale('LG', mockGraph)).not.toThrow()
|
|
expect(inputNode.pos[0]).toBeCloseTo(60, 5)
|
|
})
|
|
})
|
|
|
|
describe('group handling', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
})
|
|
|
|
it('should scale group positions and sizes during upscaling', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const group = {
|
|
pos: [80, 80],
|
|
size: [250, 200]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.groups = [group] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(group.pos[0]).toBeCloseTo(96, 5)
|
|
expect(group.size[0]).toBeCloseTo(300, 5)
|
|
expect(group.size[1]).toBeCloseTo(240, 5)
|
|
})
|
|
|
|
it('should adjust group Y position for NODE_TITLE_HEIGHT during upscaling', () => {
|
|
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const group = {
|
|
pos: [80, 100],
|
|
size: [250, 200]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.groups = [group] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Y adjustment: (100 - titleHeight) scaled, then no additional offset
|
|
const expectedY = (100 - titleHeight) * 1.2
|
|
expect(group.pos[1]).toBeCloseTo(expectedY, 5)
|
|
})
|
|
|
|
it('should scale multiple groups correctly', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const groups = [
|
|
{ pos: [0, 0], size: [300, 300] },
|
|
{ pos: [200, 200], size: [150, 150] },
|
|
{ pos: [400, 100], size: [200, 250] }
|
|
]
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.groups = groups as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Verify all groups were scaled
|
|
groups.forEach((group) => {
|
|
expect(group.size[0]).toBeGreaterThan(150)
|
|
expect(group.size[1]).toBeGreaterThan(150)
|
|
})
|
|
})
|
|
|
|
it('should handle empty groups array', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.groups = []
|
|
|
|
expect(() => ensureCorrectLayoutScale('LG', mockGraph)).not.toThrow()
|
|
})
|
|
|
|
it('should adjust group positions during downscaling', () => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
const group = {
|
|
pos: [96, 96],
|
|
size: [300, 240]
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockGraph.groups = [group] as any
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(group.pos[0]).toBeCloseTo(80, 5)
|
|
expect(group.size[0]).toBeCloseTo(250, 5)
|
|
})
|
|
})
|
|
|
|
describe('canvas scale adjustment', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
})
|
|
|
|
it('should call changeScale on canvas during upscaling', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockCanvas.graph = mockGraph
|
|
mockCanvas.ds.scale = 1.0
|
|
mockCanvas.ds.convertOffsetToCanvas.mockReturnValue([0, 0])
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(mockCanvas.ds.changeScale).toHaveBeenCalledWith(
|
|
1.0 / 1.2,
|
|
[0, 0]
|
|
)
|
|
})
|
|
|
|
it('should call changeScale on canvas during downscaling', () => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
|
|
const node = {
|
|
id: 1,
|
|
pos: [120, 120],
|
|
size: [240, 180],
|
|
width: 240,
|
|
height: 180
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockCanvas.graph = mockGraph
|
|
mockCanvas.ds.scale = 0.833333
|
|
mockCanvas.ds.convertOffsetToCanvas.mockReturnValue([0, 0])
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
expect(mockCanvas.ds.changeScale).toHaveBeenCalledWith(
|
|
expect.any(Number),
|
|
[0, 0]
|
|
)
|
|
})
|
|
|
|
it('should not call changeScale for non-active graph', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
const targetGraph = {
|
|
nodes: [node],
|
|
reroutes: new Map(),
|
|
groups: [],
|
|
extra: {}
|
|
} as any
|
|
|
|
ensureCorrectLayoutScale('LG', targetGraph)
|
|
|
|
expect(mockCanvas.ds.changeScale).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should not call changeScale when canvas is null', () => {
|
|
;(comfyApp as any).canvas = null
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
expect(() => ensureCorrectLayoutScale('LG', mockGraph)).not.toThrow()
|
|
})
|
|
|
|
it('should use correct origin screen position for scale adjustment', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
mockCanvas.graph = mockGraph
|
|
vi.mocked(createBounds).mockReturnValue([50, 60, 300, 250])
|
|
mockCanvas.ds.convertOffsetToCanvas.mockReturnValue([100, 120])
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(mockCanvas.ds.convertOffsetToCanvas).toHaveBeenCalledWith([50, 60])
|
|
expect(mockCanvas.ds.changeScale).toHaveBeenCalledWith(
|
|
expect.any(Number),
|
|
[100, 120]
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('edge cases and boundary conditions', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
})
|
|
|
|
it('should handle nodes at origin (0, 0)', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [0, 0],
|
|
size: [100, 100],
|
|
width: 100,
|
|
height: 100
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
vi.mocked(createBounds).mockReturnValue([0, 0, 100, 100])
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(node.pos[0]).toBeCloseTo(0, 5)
|
|
expect(node.size[0]).toBeCloseTo(120, 5)
|
|
})
|
|
|
|
it('should handle negative positions', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [-100, -100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
vi.mocked(createBounds).mockReturnValue([-100, -100, 100, 50])
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(node.pos[0]).toBeLessThan(0)
|
|
expect(node.pos[1]).toBeLessThan(0)
|
|
})
|
|
|
|
it('should handle very small node sizes', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [1, 1],
|
|
width: 1,
|
|
height: 1
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(node.size[0]).toBeCloseTo(1.2, 5)
|
|
expect(node.size[1]).toBeCloseTo(1.2, 5)
|
|
})
|
|
|
|
it('should handle very large node sizes', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [10000, 5000],
|
|
width: 10000,
|
|
height: 5000
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(node.size[0]).toBeCloseTo(12000, 5)
|
|
expect(node.size[1]).toBeCloseTo(6000, 5)
|
|
})
|
|
|
|
it('should handle nodes with fractional positions', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100.5, 100.7],
|
|
size: [200.3, 150.9],
|
|
width: 200.3,
|
|
height: 150.9
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
expect(node.pos[0]).toBeCloseTo(120.6, 5)
|
|
expect(node.size[0]).toBeCloseTo(240.36, 5)
|
|
})
|
|
|
|
it('should handle node with id of 0', () => {
|
|
const node = {
|
|
id: 0,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
expect(() => ensureCorrectLayoutScale('LG', mockGraph)).not.toThrow()
|
|
expect(layoutStore.batchUpdateNodeBounds).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle string node IDs', () => {
|
|
const node = {
|
|
id: 'node-123',
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
expect(() => ensureCorrectLayoutScale('LG', mockGraph)).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('RendererType type usage', () => {
|
|
it('should accept valid RendererType values', () => {
|
|
const node = {
|
|
id: 1,
|
|
pos: [100, 100],
|
|
size: [200, 150],
|
|
width: 200,
|
|
height: 150
|
|
}
|
|
mockGraph.nodes = [node] as any
|
|
|
|
const lgRenderer: RendererType = 'LG'
|
|
const vueRenderer: RendererType = 'Vue'
|
|
|
|
expect(() => ensureCorrectLayoutScale(lgRenderer, mockGraph)).not.toThrow()
|
|
expect(() => ensureCorrectLayoutScale(vueRenderer, mockGraph)).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('integration scenarios', () => {
|
|
it('should handle a complete workflow upscaling', () => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: true }
|
|
} as any)
|
|
|
|
const nodes = [
|
|
{ id: 1, pos: [100, 100], size: [200, 150], width: 200, height: 150 },
|
|
{ id: 2, pos: [400, 100], size: [150, 100], width: 150, height: 100 }
|
|
]
|
|
const reroutes = new Map([
|
|
['r1', { id: 'r1', pos: [300, 150] }]
|
|
])
|
|
const groups = [{ pos: [50, 50], size: [500, 300] }]
|
|
const inputNode = { pos: [0, 100], size: [100, 80] }
|
|
|
|
mockGraph.nodes = nodes as any
|
|
mockGraph.reroutes = reroutes as any
|
|
mockGraph.groups = groups as any
|
|
mockGraph.inputNode = inputNode as any
|
|
mockCanvas.graph = mockGraph
|
|
|
|
ensureCorrectLayoutScale('LG', mockGraph)
|
|
|
|
// Verify all elements were scaled
|
|
expect(nodes[0].size[0]).toBeCloseTo(240, 5)
|
|
expect(nodes[1].size[0]).toBeCloseTo(180, 5)
|
|
expect(reroutes.get('r1')?.pos[0]).toBeCloseTo(360, 5)
|
|
expect(groups[0].size[0]).toBeCloseTo(600, 5)
|
|
expect(inputNode.size[0]).toBeCloseTo(120, 5)
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('Vue')
|
|
expect(layoutStore.batchUpdateNodeBounds).toHaveBeenCalled()
|
|
expect(mockCanvas.ds.changeScale).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle a complete workflow downscaling', () => {
|
|
vi.mocked(useVueFeatureFlags).mockReturnValue({
|
|
shouldRenderVueNodes: { value: false }
|
|
} as any)
|
|
|
|
const nodes = [
|
|
{ id: 1, pos: [120, 120], size: [240, 180], width: 240, height: 180 },
|
|
{ id: 2, pos: [480, 120], size: [180, 120], width: 180, height: 120 }
|
|
]
|
|
const reroutes = new Map([
|
|
['r1', { id: 'r1', pos: [360, 180] }]
|
|
])
|
|
const groups = [{ pos: [60, 60], size: [600, 360] }]
|
|
|
|
mockGraph.nodes = nodes as any
|
|
mockGraph.reroutes = reroutes as any
|
|
mockGraph.groups = groups as any
|
|
mockCanvas.graph = mockGraph
|
|
|
|
ensureCorrectLayoutScale('Vue', mockGraph)
|
|
|
|
// Verify all elements were scaled down
|
|
expect(nodes[0].size[0]).toBeCloseTo(200, 5)
|
|
expect(nodes[1].size[0]).toBeCloseTo(150, 5)
|
|
expect(reroutes.get('r1')?.pos[0]).toBeCloseTo(300, 5)
|
|
expect(groups[0].size[0]).toBeCloseTo(500, 5)
|
|
expect(mockGraph.extra.workflowRendererVersion).toBe('LG')
|
|
})
|
|
})
|
|
}) |