diff --git a/tests-ui/LGraph.type.test.ts b/tests-ui/LGraph.type.test.ts new file mode 100644 index 0000000000..275f447222 --- /dev/null +++ b/tests-ui/LGraph.type.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect } from 'vitest' +import type { RendererType, LGraphExtra } from '../src/lib/litegraph/src/LGraph' + +/** + * Tests for the RendererType (formerly rendererType) type definition + * + * This tests the type rename from 'rendererType' to 'RendererType' to follow + * TypeScript naming conventions where types should use PascalCase. + */ +describe('RendererType', () => { + describe('type definition', () => { + it('should accept "LG" as a valid value', () => { + const renderer: RendererType = 'LG' + expect(renderer).toBe('LG') + }) + + it('should accept "Vue" as a valid value', () => { + const renderer: RendererType = 'Vue' + expect(renderer).toBe('Vue') + }) + + it('should be assignable to LGraphExtra workflowRendererVersion property', () => { + const extra: LGraphExtra = { + workflowRendererVersion: 'LG' + } + expect(extra.workflowRendererVersion).toBe('LG') + + extra.workflowRendererVersion = 'Vue' + expect(extra.workflowRendererVersion).toBe('Vue') + }) + + it('should allow undefined for workflowRendererVersion', () => { + const extra: LGraphExtra = { + workflowRendererVersion: undefined + } + expect(extra.workflowRendererVersion).toBeUndefined() + }) + + it('should work with optional chaining', () => { + const extra: LGraphExtra = {} + const version = extra.workflowRendererVersion + expect(version).toBeUndefined() + }) + }) + + describe('type safety', () => { + it('should prevent assignment of invalid string values at compile time', () => { + // This test validates TypeScript compiler behavior + // The following would cause a compile error: + // const invalid: RendererType = 'Invalid' + + // We can test runtime validation if needed + const validValues: RendererType[] = ['LG', 'Vue'] + validValues.forEach((value) => { + expect(['LG', 'Vue']).toContain(value) + }) + }) + + it('should work with type guards', () => { + const isValidRendererType = (value: string): value is RendererType => { + return value === 'LG' || value === 'Vue' + } + + expect(isValidRendererType('LG')).toBe(true) + expect(isValidRendererType('Vue')).toBe(true) + expect(isValidRendererType('Invalid')).toBe(false) + }) + + it('should work in switch statements', () => { + const testSwitch = (renderer: RendererType): string => { + switch (renderer) { + case 'LG': + return 'Legacy LiteGraph' + case 'Vue': + return 'Vue Renderer' + default: + // TypeScript should ensure this is unreachable + const _exhaustive: never = renderer + return _exhaustive + } + } + + expect(testSwitch('LG')).toBe('Legacy LiteGraph') + expect(testSwitch('Vue')).toBe('Vue Renderer') + }) + }) + + describe('usage in graph serialization', () => { + it('should serialize and deserialize RendererType correctly', () => { + const extra: LGraphExtra = { + workflowRendererVersion: 'Vue' + } + + const serialized = JSON.stringify(extra) + const deserialized: LGraphExtra = JSON.parse(serialized) + + expect(deserialized.workflowRendererVersion).toBe('Vue') + }) + + it('should handle missing workflowRendererVersion in deserialization', () => { + const json = '{}' + const extra: LGraphExtra = JSON.parse(json) + + expect(extra.workflowRendererVersion).toBeUndefined() + }) + + it('should handle null workflowRendererVersion', () => { + const extra: LGraphExtra = { + workflowRendererVersion: undefined + } + + expect(extra.workflowRendererVersion).toBeUndefined() + }) + }) + + describe('backward compatibility considerations', () => { + it('should work with both old and new renderer formats', () => { + // Even though the type is renamed, the values remain the same + const lgRenderer: RendererType = 'LG' + const vueRenderer: RendererType = 'Vue' + + expect(lgRenderer).toBe('LG') + expect(vueRenderer).toBe('Vue') + }) + + it('should support migration scenarios', () => { + // Simulating reading from an old format + const oldFormat = { workflowRendererVersion: 'LG' as RendererType } + + // Converting to new format (type is already compatible) + const newFormat: LGraphExtra = { + workflowRendererVersion: oldFormat.workflowRendererVersion + } + + expect(newFormat.workflowRendererVersion).toBe('LG') + }) + }) + + describe('default values and initialization', () => { + it('should handle default initialization', () => { + const extra: LGraphExtra = {} + + // workflowRendererVersion should be undefined by default + expect(extra.workflowRendererVersion).toBeUndefined() + }) + + it('should support nullish coalescing', () => { + const extra: LGraphExtra = {} + const renderer = extra.workflowRendererVersion ?? 'LG' + + expect(renderer).toBe('LG') + }) + + it('should support optional chaining with default', () => { + let extra: LGraphExtra | undefined = undefined + const renderer = extra?.workflowRendererVersion ?? 'LG' + + expect(renderer).toBe('LG') + }) + }) +}) \ No newline at end of file diff --git a/tests-ui/ensureCorrectLayoutScale.test.ts b/tests-ui/ensureCorrectLayoutScale.test.ts new file mode 100644 index 0000000000..d2d11cd8be --- /dev/null +++ b/tests-ui/ensureCorrectLayoutScale.test.ts @@ -0,0 +1,1115 @@ +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') + }) + }) +}) \ No newline at end of file