|
| 1 | +import { createPinia, setActivePinia } from 'pinia' |
| 2 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 3 | +import { nextTick } from 'vue' |
| 4 | + |
| 5 | +import { app } from '@/scripts/app' |
| 6 | +import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' |
| 7 | +import { useWorkflowStore } from '@/stores/workflowStore' |
| 8 | +import type { ComfyWorkflow } from '@/stores/workflowStore' |
| 9 | + |
| 10 | +vi.mock('@/scripts/app', () => { |
| 11 | + const mockCanvas = { |
| 12 | + subgraph: null, |
| 13 | + ds: { |
| 14 | + scale: 1, |
| 15 | + offset: [0, 0], |
| 16 | + state: { |
| 17 | + scale: 1, |
| 18 | + offset: [0, 0] |
| 19 | + } |
| 20 | + }, |
| 21 | + setDirty: vi.fn() |
| 22 | + } |
| 23 | + |
| 24 | + return { |
| 25 | + app: { |
| 26 | + graph: { |
| 27 | + subgraphs: new Map(), |
| 28 | + getNodeById: vi.fn() |
| 29 | + }, |
| 30 | + canvas: mockCanvas |
| 31 | + } |
| 32 | + } |
| 33 | +}) |
| 34 | + |
| 35 | +// Mock canvasStore |
| 36 | +vi.mock('@/stores/graphStore', () => ({ |
| 37 | + useCanvasStore: () => ({ |
| 38 | + getCanvas: () => (app as any).canvas |
| 39 | + }) |
| 40 | +})) |
| 41 | + |
| 42 | +// Get reference to mock canvas |
| 43 | +const mockCanvas = app.canvas as any |
| 44 | + |
| 45 | +describe('useSubgraphNavigationStore - Viewport Persistence', () => { |
| 46 | + beforeEach(() => { |
| 47 | + setActivePinia(createPinia()) |
| 48 | + // Reset canvas state |
| 49 | + mockCanvas.ds.scale = 1 |
| 50 | + mockCanvas.ds.offset = [0, 0] |
| 51 | + mockCanvas.ds.state.scale = 1 |
| 52 | + mockCanvas.ds.state.offset = [0, 0] |
| 53 | + mockCanvas.setDirty.mockClear() |
| 54 | + }) |
| 55 | + |
| 56 | + describe('saveViewport', () => { |
| 57 | + it('should save viewport state for root graph', () => { |
| 58 | + const navigationStore = useSubgraphNavigationStore() |
| 59 | + |
| 60 | + // Set viewport state |
| 61 | + mockCanvas.ds.state.scale = 2 |
| 62 | + mockCanvas.ds.state.offset = [100, 200] |
| 63 | + |
| 64 | + // Save viewport for root |
| 65 | + navigationStore.saveViewport('root') |
| 66 | + |
| 67 | + // Check it was saved |
| 68 | + const saved = navigationStore.viewportCache.get('root') |
| 69 | + expect(saved).toEqual({ |
| 70 | + scale: 2, |
| 71 | + offset: [100, 200] |
| 72 | + }) |
| 73 | + }) |
| 74 | + |
| 75 | + it('should save viewport state for subgraph', () => { |
| 76 | + const navigationStore = useSubgraphNavigationStore() |
| 77 | + |
| 78 | + // Set viewport state |
| 79 | + mockCanvas.ds.state.scale = 1.5 |
| 80 | + mockCanvas.ds.state.offset = [50, 75] |
| 81 | + |
| 82 | + // Save viewport for subgraph |
| 83 | + navigationStore.saveViewport('subgraph-123') |
| 84 | + |
| 85 | + // Check it was saved |
| 86 | + const saved = navigationStore.viewportCache.get('subgraph-123') |
| 87 | + expect(saved).toEqual({ |
| 88 | + scale: 1.5, |
| 89 | + offset: [50, 75] |
| 90 | + }) |
| 91 | + }) |
| 92 | + |
| 93 | + it('should save viewport for current context when no ID provided', () => { |
| 94 | + const navigationStore = useSubgraphNavigationStore() |
| 95 | + const workflowStore = useWorkflowStore() |
| 96 | + |
| 97 | + // Mock being in a subgraph |
| 98 | + const mockSubgraph = { id: 'sub-456' } |
| 99 | + workflowStore.activeSubgraph = mockSubgraph as any |
| 100 | + |
| 101 | + // Set viewport state |
| 102 | + mockCanvas.ds.state.scale = 3 |
| 103 | + mockCanvas.ds.state.offset = [10, 20] |
| 104 | + |
| 105 | + // Save viewport without ID (should default to root since activeSubgraph is not tracked by navigation store) |
| 106 | + navigationStore.saveViewport('sub-456') |
| 107 | + |
| 108 | + // Should save for the specified subgraph |
| 109 | + const saved = navigationStore.viewportCache.get('sub-456') |
| 110 | + expect(saved).toEqual({ |
| 111 | + scale: 3, |
| 112 | + offset: [10, 20] |
| 113 | + }) |
| 114 | + }) |
| 115 | + }) |
| 116 | + |
| 117 | + describe('restoreViewport', () => { |
| 118 | + it('should restore viewport state for root graph', () => { |
| 119 | + const navigationStore = useSubgraphNavigationStore() |
| 120 | + |
| 121 | + // Save a viewport state |
| 122 | + navigationStore.viewportCache.set('root', { |
| 123 | + scale: 2.5, |
| 124 | + offset: [150, 250] |
| 125 | + }) |
| 126 | + |
| 127 | + // Restore it |
| 128 | + navigationStore.restoreViewport('root') |
| 129 | + |
| 130 | + // Check canvas was updated |
| 131 | + expect(mockCanvas.ds.scale).toBe(2.5) |
| 132 | + expect(mockCanvas.ds.offset).toEqual([150, 250]) |
| 133 | + expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true) |
| 134 | + }) |
| 135 | + |
| 136 | + it('should restore viewport state for subgraph', () => { |
| 137 | + const navigationStore = useSubgraphNavigationStore() |
| 138 | + |
| 139 | + // Save a viewport state |
| 140 | + navigationStore.viewportCache.set('sub-789', { |
| 141 | + scale: 0.75, |
| 142 | + offset: [-50, -100] |
| 143 | + }) |
| 144 | + |
| 145 | + // Restore it |
| 146 | + navigationStore.restoreViewport('sub-789') |
| 147 | + |
| 148 | + // Check canvas was updated |
| 149 | + expect(mockCanvas.ds.scale).toBe(0.75) |
| 150 | + expect(mockCanvas.ds.offset).toEqual([-50, -100]) |
| 151 | + }) |
| 152 | + |
| 153 | + it('should do nothing if no saved viewport exists', () => { |
| 154 | + const navigationStore = useSubgraphNavigationStore() |
| 155 | + |
| 156 | + // Reset canvas |
| 157 | + mockCanvas.ds.scale = 1 |
| 158 | + mockCanvas.ds.offset = [0, 0] |
| 159 | + mockCanvas.setDirty.mockClear() |
| 160 | + |
| 161 | + // Try to restore non-existent viewport |
| 162 | + navigationStore.restoreViewport('non-existent') |
| 163 | + |
| 164 | + // Canvas should not change |
| 165 | + expect(mockCanvas.ds.scale).toBe(1) |
| 166 | + expect(mockCanvas.ds.offset).toEqual([0, 0]) |
| 167 | + expect(mockCanvas.setDirty).not.toHaveBeenCalled() |
| 168 | + }) |
| 169 | + }) |
| 170 | + |
| 171 | + describe('navigation integration', () => { |
| 172 | + it('should save and restore viewport when navigating between subgraphs', async () => { |
| 173 | + const navigationStore = useSubgraphNavigationStore() |
| 174 | + const workflowStore = useWorkflowStore() |
| 175 | + |
| 176 | + // Start at root with custom viewport |
| 177 | + mockCanvas.ds.state.scale = 2 |
| 178 | + mockCanvas.ds.state.offset = [100, 100] |
| 179 | + |
| 180 | + // Navigate to subgraph |
| 181 | + const subgraph1 = { id: 'sub1' } |
| 182 | + workflowStore.activeSubgraph = subgraph1 as any |
| 183 | + await nextTick() |
| 184 | + |
| 185 | + // Root viewport should have been saved |
| 186 | + const rootViewport = navigationStore.viewportCache.get('root') |
| 187 | + expect(rootViewport).toBeDefined() |
| 188 | + expect(rootViewport?.scale).toBe(2) |
| 189 | + expect(rootViewport?.offset).toEqual([100, 100]) |
| 190 | + |
| 191 | + // Change viewport in subgraph |
| 192 | + mockCanvas.ds.state.scale = 0.5 |
| 193 | + mockCanvas.ds.state.offset = [-50, -50] |
| 194 | + |
| 195 | + // Navigate back to root |
| 196 | + workflowStore.activeSubgraph = undefined |
| 197 | + await nextTick() |
| 198 | + |
| 199 | + // Subgraph viewport should have been saved |
| 200 | + const sub1Viewport = navigationStore.viewportCache.get('sub1') |
| 201 | + expect(sub1Viewport).toBeDefined() |
| 202 | + expect(sub1Viewport?.scale).toBe(0.5) |
| 203 | + expect(sub1Viewport?.offset).toEqual([-50, -50]) |
| 204 | + |
| 205 | + // Root viewport should be restored |
| 206 | + expect(mockCanvas.ds.scale).toBe(2) |
| 207 | + expect(mockCanvas.ds.offset).toEqual([100, 100]) |
| 208 | + }) |
| 209 | + |
| 210 | + it('should preserve viewport cache when switching workflows', async () => { |
| 211 | + const navigationStore = useSubgraphNavigationStore() |
| 212 | + const workflowStore = useWorkflowStore() |
| 213 | + |
| 214 | + // Add some viewport states |
| 215 | + navigationStore.viewportCache.set('root', { scale: 2, offset: [0, 0] }) |
| 216 | + navigationStore.viewportCache.set('sub1', { |
| 217 | + scale: 1.5, |
| 218 | + offset: [10, 10] |
| 219 | + }) |
| 220 | + |
| 221 | + expect(navigationStore.viewportCache.size).toBe(2) |
| 222 | + |
| 223 | + // Switch workflows |
| 224 | + const workflow1 = { path: 'workflow1.json' } as ComfyWorkflow |
| 225 | + const workflow2 = { path: 'workflow2.json' } as ComfyWorkflow |
| 226 | + |
| 227 | + workflowStore.activeWorkflow = workflow1 as any |
| 228 | + await nextTick() |
| 229 | + |
| 230 | + workflowStore.activeWorkflow = workflow2 as any |
| 231 | + await nextTick() |
| 232 | + |
| 233 | + // Cache should be preserved (LRU will manage memory) |
| 234 | + expect(navigationStore.viewportCache.size).toBe(2) |
| 235 | + expect(navigationStore.viewportCache.has('root')).toBe(true) |
| 236 | + expect(navigationStore.viewportCache.has('sub1')).toBe(true) |
| 237 | + }) |
| 238 | + }) |
| 239 | +}) |
0 commit comments