mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
Compare commits
2 Commits
sno-qa-999
...
revert-102
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b223d8f607 | ||
|
|
a75e4ddbc8 |
@@ -23,7 +23,6 @@ import type { AppMode } from '@/composables/useAppMode'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import {
|
||||
@@ -392,9 +391,6 @@ export const useWorkflowService = () => {
|
||||
// Capture thumbnail before loading new graph
|
||||
void workflowThumbnail.storeThumbnail(activeWorkflow)
|
||||
domWidgetStore.clear()
|
||||
|
||||
// Save subgraph viewport before the canvas gets overwritten
|
||||
useSubgraphNavigationStore().saveCurrentViewport()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { findSubgraphPathById } from '@/utils/graphTraversalUtil'
|
||||
import { isNonNullish, isSubgraph } from '@/utils/typeGuardUtil'
|
||||
@@ -35,44 +34,20 @@ export const useSubgraphNavigationStore = defineStore(
|
||||
/** The stack of subgraph IDs from the root graph to the currently opened subgraph. */
|
||||
const idStack = ref<string[]>([])
|
||||
|
||||
/** LRU cache for viewport states. Key: `workflowPath:graphId` */
|
||||
/** LRU cache for viewport states. Key: subgraph ID or 'root' for root graph */
|
||||
const viewportCache = new QuickLRU<string, DragAndScaleState>({
|
||||
maxSize: VIEWPORT_CACHE_MAX_SIZE
|
||||
})
|
||||
|
||||
/** Get the ID of the root graph for the currently active workflow. */
|
||||
/**
|
||||
* Get the ID of the root graph for the currently active workflow.
|
||||
* @returns The ID of the root graph for the currently active workflow.
|
||||
*/
|
||||
const getCurrentRootGraphId = () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
return canvas.graph?.rootGraph?.id ?? 'root'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set by saveCurrentViewport() (called from beforeLoadNewGraph) to
|
||||
* prevent onNavigated from re-saving a stale viewport during the
|
||||
* workflow switch transition. Uses setTimeout instead of rAF so the
|
||||
* flag resets even when the tab is backgrounded.
|
||||
*/
|
||||
let isWorkflowSwitching = false
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/** Build a workflow-scoped cache key. */
|
||||
function buildCacheKey(
|
||||
graphId: string,
|
||||
workflowRef?: { path?: string } | null
|
||||
): string {
|
||||
const wf = workflowRef ?? workflowStore.activeWorkflow
|
||||
const prefix = wf?.path ?? ''
|
||||
return `${prefix}:${graphId}`
|
||||
}
|
||||
|
||||
/** ID of the graph currently shown on the canvas. */
|
||||
function getActiveGraphId(): string {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
return canvas?.subgraph?.id ?? getCurrentRootGraphId()
|
||||
}
|
||||
|
||||
// ── Navigation stack ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A stack representing subgraph navigation history from the root graph to
|
||||
* the current opened subgraph.
|
||||
@@ -85,6 +60,7 @@ export const useSubgraphNavigationStore = defineStore(
|
||||
|
||||
/**
|
||||
* Restore the navigation stack from a list of subgraph IDs.
|
||||
* @param subgraphIds The list of subgraph IDs to restore the navigation stack from.
|
||||
* @see exportState
|
||||
*/
|
||||
const restoreState = (subgraphIds: string[]) => {
|
||||
@@ -94,74 +70,69 @@ export const useSubgraphNavigationStore = defineStore(
|
||||
|
||||
/**
|
||||
* Export the navigation stack as a list of subgraph IDs.
|
||||
* @returns The list of subgraph IDs, ending with the currently active subgraph.
|
||||
* @see restoreState
|
||||
*/
|
||||
const exportState = () => [...idStack.value]
|
||||
|
||||
// ── Viewport save / restore ──────────────────────────────────────
|
||||
|
||||
/** Get the current viewport state, or null if the canvas is not available. */
|
||||
/**
|
||||
* Get the current viewport state.
|
||||
* @returns The current viewport state, or null if the canvas is not available.
|
||||
*/
|
||||
const getCurrentViewport = (): DragAndScaleState | null => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
if (!canvas) return null
|
||||
|
||||
return {
|
||||
scale: canvas.ds.state.scale,
|
||||
offset: [...canvas.ds.state.offset]
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the current viewport state for a graph. */
|
||||
function saveViewport(graphId: string, workflowRef?: object | null): void {
|
||||
/**
|
||||
* Save the current viewport state.
|
||||
* @param graphId The graph ID to save for. Use 'root' for root graph, or omit to use current context.
|
||||
*/
|
||||
const saveViewport = (graphId: string) => {
|
||||
const viewport = getCurrentViewport()
|
||||
if (!viewport) return
|
||||
viewportCache.set(buildCacheKey(graphId, workflowRef), viewport)
|
||||
|
||||
viewportCache.set(graphId, viewport)
|
||||
}
|
||||
|
||||
/** Apply a viewport state to the canvas. */
|
||||
function applyViewport(viewport: DragAndScaleState): void {
|
||||
/**
|
||||
* Restore viewport state for a graph.
|
||||
* @param graphId The graph ID to restore. Use 'root' for root graph, or omit to use current context.
|
||||
*/
|
||||
const restoreViewport = (graphId: string) => {
|
||||
const viewport = viewportCache.get(graphId)
|
||||
if (!viewport) return
|
||||
|
||||
const canvas = app.canvas
|
||||
if (!canvas) return
|
||||
|
||||
canvas.ds.scale = viewport.scale
|
||||
canvas.ds.offset[0] = viewport.offset[0]
|
||||
canvas.ds.offset[1] = viewport.offset[1]
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
function restoreViewport(graphId: string): void {
|
||||
const canvas = app.canvas
|
||||
if (!canvas) return
|
||||
|
||||
const expectedKey = buildCacheKey(graphId)
|
||||
const viewport = viewportCache.get(expectedKey)
|
||||
if (viewport) {
|
||||
applyViewport(viewport)
|
||||
return
|
||||
}
|
||||
|
||||
// Cache miss — fit to content after the canvas has the new graph.
|
||||
// rAF fires after layout + paint, when nodes are positioned.
|
||||
const expectedGraphId = graphId
|
||||
requestAnimationFrame(() => {
|
||||
if (getActiveGraphId() !== expectedGraphId) return
|
||||
useLitegraphService().fitView()
|
||||
})
|
||||
}
|
||||
|
||||
// ── Navigation handler ───────────────────────────────────────────
|
||||
|
||||
function onNavigated(
|
||||
/**
|
||||
* Update the navigation stack when the active subgraph changes.
|
||||
* @param subgraph The new active subgraph.
|
||||
* @param prevSubgraph The previous active subgraph.
|
||||
*/
|
||||
const onNavigated = (
|
||||
subgraph: Subgraph | undefined,
|
||||
prevSubgraph: Subgraph | undefined
|
||||
): void {
|
||||
// During a workflow switch, beforeLoadNewGraph already saved the
|
||||
// outgoing viewport — skip the save here to avoid caching stale
|
||||
// canvas state from the transition.
|
||||
if (!isWorkflowSwitching) {
|
||||
if (prevSubgraph) {
|
||||
saveViewport(prevSubgraph.id)
|
||||
} else if (!prevSubgraph && subgraph) {
|
||||
saveViewport(getCurrentRootGraphId())
|
||||
}
|
||||
) => {
|
||||
// Save viewport state for the graph we're leaving
|
||||
if (prevSubgraph) {
|
||||
// Leaving a subgraph
|
||||
saveViewport(prevSubgraph.id)
|
||||
} else if (!prevSubgraph && subgraph) {
|
||||
// Leaving root graph to enter a subgraph
|
||||
saveViewport(getCurrentRootGraphId())
|
||||
}
|
||||
|
||||
const isInRootGraph = !subgraph
|
||||
@@ -176,22 +147,20 @@ export const useSubgraphNavigationStore = defineStore(
|
||||
if (isInReachableSubgraph) {
|
||||
idStack.value = [...path]
|
||||
} else {
|
||||
// Treat as if opening a new subgraph
|
||||
idStack.value = [subgraph.id]
|
||||
}
|
||||
|
||||
// Always try to restore viewport for the target subgraph
|
||||
restoreViewport(subgraph.id)
|
||||
}
|
||||
|
||||
// ── Watchers ─────────────────────────────────────────────────────
|
||||
|
||||
// Sync flush ensures we capture the outgoing viewport before any other
|
||||
// watchers or DOM updates from the same state change mutate the canvas.
|
||||
// Update navigation stack when opened subgraph changes (also triggers when switching workflows)
|
||||
watch(
|
||||
() => workflowStore.activeSubgraph,
|
||||
(newValue, oldValue) => {
|
||||
onNavigated(newValue, oldValue)
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
}
|
||||
)
|
||||
|
||||
//Allow navigation with forward/back buttons
|
||||
@@ -260,16 +229,6 @@ export const useSubgraphNavigationStore = defineStore(
|
||||
watch(() => canvasStore.currentGraph, updateHash)
|
||||
watch(routeHash, () => navigateToHash(String(routeHash.value)))
|
||||
|
||||
/** Save the current viewport for the active graph/workflow. Called by
|
||||
* workflowService.beforeLoadNewGraph() before the canvas is overwritten. */
|
||||
function saveCurrentViewport(): void {
|
||||
saveViewport(getActiveGraphId())
|
||||
isWorkflowSwitching = true
|
||||
setTimeout(() => {
|
||||
isWorkflowSwitching = false
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return {
|
||||
activeSubgraph,
|
||||
navigationStack,
|
||||
@@ -277,9 +236,7 @@ export const useSubgraphNavigationStore = defineStore(
|
||||
exportState,
|
||||
saveViewport,
|
||||
restoreViewport,
|
||||
saveCurrentViewport,
|
||||
updateHash,
|
||||
/** @internal Exposed for test assertions only. */
|
||||
viewportCache
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,39 +18,32 @@ const { mockSetDirty } = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('@/scripts/app', () => {
|
||||
const mockCanvas = {
|
||||
subgraph: undefined as unknown,
|
||||
graph: undefined as unknown,
|
||||
subgraph: null,
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0],
|
||||
state: { scale: 1, offset: [0, 0] },
|
||||
fitToBounds: vi.fn()
|
||||
state: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
}
|
||||
},
|
||||
setDirty: mockSetDirty,
|
||||
get empty() {
|
||||
return true
|
||||
}
|
||||
setDirty: mockSetDirty
|
||||
}
|
||||
|
||||
const mockGraph = {
|
||||
_nodes: [],
|
||||
nodes: [],
|
||||
subgraphs: new Map(),
|
||||
getNodeById: vi.fn(),
|
||||
id: 'root'
|
||||
}
|
||||
|
||||
mockCanvas.graph = mockGraph
|
||||
|
||||
return {
|
||||
app: {
|
||||
graph: mockGraph,
|
||||
rootGraph: mockGraph,
|
||||
graph: {
|
||||
_nodes: [],
|
||||
nodes: [],
|
||||
subgraphs: new Map(),
|
||||
getNodeById: vi.fn()
|
||||
},
|
||||
canvas: mockCanvas
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Mock canvasStore
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
getCanvas: () => app.canvas
|
||||
@@ -58,165 +51,141 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
}))
|
||||
vi.mock('@vueuse/router', () => ({ useRouteHash: vi.fn() }))
|
||||
|
||||
const { mockFitView } = vi.hoisted(() => ({
|
||||
mockFitView: vi.fn()
|
||||
}))
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: () => ({ fitView: mockFitView })
|
||||
}))
|
||||
|
||||
// Get reference to mock canvas
|
||||
const mockCanvas = app.canvas
|
||||
|
||||
let rafCallbacks: FrameRequestCallback[] = []
|
||||
|
||||
describe('useSubgraphNavigationStore - Viewport Persistence', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
rafCallbacks = []
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
rafCallbacks.push(cb)
|
||||
return rafCallbacks.length
|
||||
})
|
||||
mockCanvas.subgraph = undefined
|
||||
mockCanvas.graph = app.graph
|
||||
// Reset canvas state
|
||||
mockCanvas.ds.scale = 1
|
||||
mockCanvas.ds.offset = [0, 0]
|
||||
mockCanvas.ds.state.scale = 1
|
||||
mockCanvas.ds.state.offset = [0, 0]
|
||||
mockSetDirty.mockClear()
|
||||
mockFitView.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('cache key isolation', () => {
|
||||
it('isolates viewport by workflow — same graphId returns different values', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Save viewport under workflow A
|
||||
workflowStore.activeWorkflow = {
|
||||
path: 'wfA.json'
|
||||
} as typeof workflowStore.activeWorkflow
|
||||
mockCanvas.ds.state.scale = 2
|
||||
mockCanvas.ds.state.offset = [10, 20]
|
||||
store.saveViewport('root')
|
||||
|
||||
// Save different viewport under workflow B
|
||||
workflowStore.activeWorkflow = {
|
||||
path: 'wfB.json'
|
||||
} as typeof workflowStore.activeWorkflow
|
||||
mockCanvas.ds.state.scale = 5
|
||||
mockCanvas.ds.state.offset = [99, 88]
|
||||
store.saveViewport('root')
|
||||
|
||||
// Restore under A — should get A's values
|
||||
workflowStore.activeWorkflow = {
|
||||
path: 'wfA.json'
|
||||
} as typeof workflowStore.activeWorkflow
|
||||
store.restoreViewport('root')
|
||||
|
||||
expect(mockCanvas.ds.scale).toBe(2)
|
||||
expect(mockCanvas.ds.offset).toEqual([10, 20])
|
||||
})
|
||||
})
|
||||
|
||||
describe('saveViewport', () => {
|
||||
it('saves viewport state for root graph', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
it('should save viewport state for root graph', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
|
||||
// Set viewport state
|
||||
mockCanvas.ds.state.scale = 2
|
||||
mockCanvas.ds.state.offset = [100, 200]
|
||||
|
||||
store.saveViewport('root')
|
||||
// Save viewport for root
|
||||
navigationStore.saveViewport('root')
|
||||
|
||||
expect(store.viewportCache.get(':root')).toEqual({
|
||||
// Check it was saved
|
||||
const saved = navigationStore.viewportCache.get('root')
|
||||
expect(saved).toEqual({
|
||||
scale: 2,
|
||||
offset: [100, 200]
|
||||
})
|
||||
})
|
||||
|
||||
it('saves viewport state for subgraph', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
it('should save viewport state for subgraph', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
|
||||
// Set viewport state
|
||||
mockCanvas.ds.state.scale = 1.5
|
||||
mockCanvas.ds.state.offset = [50, 75]
|
||||
|
||||
store.saveViewport('subgraph-123')
|
||||
// Save viewport for subgraph
|
||||
navigationStore.saveViewport('subgraph-123')
|
||||
|
||||
expect(store.viewportCache.get(':subgraph-123')).toEqual({
|
||||
// Check it was saved
|
||||
const saved = navigationStore.viewportCache.get('subgraph-123')
|
||||
expect(saved).toEqual({
|
||||
scale: 1.5,
|
||||
offset: [50, 75]
|
||||
})
|
||||
})
|
||||
|
||||
it('should save viewport for current context when no ID provided', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Mock being in a subgraph
|
||||
const mockSubgraph = { id: 'sub-456' }
|
||||
workflowStore.activeSubgraph = mockSubgraph as Subgraph
|
||||
|
||||
// Set viewport state
|
||||
mockCanvas.ds.state.scale = 3
|
||||
mockCanvas.ds.state.offset = [10, 20]
|
||||
|
||||
// Save viewport without ID (should default to root since activeSubgraph is not tracked by navigation store)
|
||||
navigationStore.saveViewport('sub-456')
|
||||
|
||||
// Should save for the specified subgraph
|
||||
const saved = navigationStore.viewportCache.get('sub-456')
|
||||
expect(saved).toEqual({
|
||||
scale: 3,
|
||||
offset: [10, 20]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreViewport', () => {
|
||||
it('restores cached viewport', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
store.viewportCache.set(':root', { scale: 2.5, offset: [150, 250] })
|
||||
it('should restore viewport state for root graph', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
|
||||
store.restoreViewport('root')
|
||||
// Save a viewport state
|
||||
navigationStore.viewportCache.set('root', {
|
||||
scale: 2.5,
|
||||
offset: [150, 250]
|
||||
})
|
||||
|
||||
// Restore it
|
||||
navigationStore.restoreViewport('root')
|
||||
|
||||
// Check canvas was updated
|
||||
expect(mockCanvas.ds.scale).toBe(2.5)
|
||||
expect(mockCanvas.ds.offset).toEqual([150, 250])
|
||||
expect(mockSetDirty).toHaveBeenCalledWith(true, true)
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('does not mutate canvas synchronously on cache miss', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
it('should restore viewport state for subgraph', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
|
||||
// Save a viewport state
|
||||
navigationStore.viewportCache.set('sub-789', {
|
||||
scale: 0.75,
|
||||
offset: [-50, -100]
|
||||
})
|
||||
|
||||
// Restore it
|
||||
navigationStore.restoreViewport('sub-789')
|
||||
|
||||
// Check canvas was updated
|
||||
expect(mockCanvas.ds.scale).toBe(0.75)
|
||||
expect(mockCanvas.ds.offset).toEqual([-50, -100])
|
||||
})
|
||||
|
||||
it('should do nothing if no saved viewport exists', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
|
||||
// Reset canvas
|
||||
mockCanvas.ds.scale = 1
|
||||
mockCanvas.ds.offset = [0, 0]
|
||||
mockSetDirty.mockClear()
|
||||
|
||||
store.restoreViewport('non-existent')
|
||||
// Try to restore non-existent viewport
|
||||
navigationStore.restoreViewport('non-existent')
|
||||
|
||||
// Should not change canvas synchronously
|
||||
// Canvas should not change
|
||||
expect(mockCanvas.ds.scale).toBe(1)
|
||||
expect(mockCanvas.ds.offset).toEqual([0, 0])
|
||||
expect(mockSetDirty).not.toHaveBeenCalled()
|
||||
// But should have scheduled a rAF
|
||||
expect(rafCallbacks).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('calls fitView on cache miss after rAF fires', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
// Ensure no cached entry
|
||||
store.viewportCache.delete(':root')
|
||||
|
||||
// Use the root graph ID so the stale-guard passes
|
||||
store.restoreViewport('root')
|
||||
|
||||
expect(mockFitView).not.toHaveBeenCalled()
|
||||
expect(rafCallbacks).toHaveLength(1)
|
||||
|
||||
// Simulate rAF firing — active graph still matches
|
||||
rafCallbacks[0](performance.now())
|
||||
|
||||
expect(mockFitView).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('skips fitView if active graph changed before rAF fires', () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
store.viewportCache.delete(':root')
|
||||
|
||||
store.restoreViewport('root')
|
||||
expect(rafCallbacks).toHaveLength(1)
|
||||
|
||||
// Simulate graph switching away before rAF fires
|
||||
mockCanvas.subgraph = { id: 'different-graph' } as never
|
||||
|
||||
rafCallbacks[0](performance.now())
|
||||
|
||||
expect(mockFitView).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation integration', () => {
|
||||
it('saves and restores viewport when navigating between subgraphs', async () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
it('should save and restore viewport when navigating between subgraphs', async () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Create mock subgraph with both _nodes and nodes properties
|
||||
const mockRootGraph = {
|
||||
_nodes: [],
|
||||
nodes: [],
|
||||
@@ -230,72 +199,84 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
|
||||
nodes: []
|
||||
}
|
||||
|
||||
// Start at root with custom viewport
|
||||
mockCanvas.ds.state.scale = 2
|
||||
mockCanvas.ds.state.offset = [100, 100]
|
||||
|
||||
// Enter subgraph
|
||||
// Navigate to subgraph
|
||||
workflowStore.activeSubgraph = subgraph1 as Partial<Subgraph> as Subgraph
|
||||
await nextTick()
|
||||
|
||||
// Root viewport saved
|
||||
expect(store.viewportCache.get(':root')).toEqual({
|
||||
scale: 2,
|
||||
offset: [100, 100]
|
||||
})
|
||||
// Root viewport should have been saved automatically
|
||||
const rootViewport = navigationStore.viewportCache.get('root')
|
||||
expect(rootViewport).toBeDefined()
|
||||
expect(rootViewport?.scale).toBe(2)
|
||||
expect(rootViewport?.offset).toEqual([100, 100])
|
||||
|
||||
// Change viewport in subgraph
|
||||
mockCanvas.ds.state.scale = 0.5
|
||||
mockCanvas.ds.state.offset = [-50, -50]
|
||||
|
||||
// Exit subgraph
|
||||
// Navigate back to root
|
||||
workflowStore.activeSubgraph = undefined
|
||||
await nextTick()
|
||||
|
||||
// Subgraph viewport saved
|
||||
expect(store.viewportCache.get(':sub1')).toEqual({
|
||||
scale: 0.5,
|
||||
offset: [-50, -50]
|
||||
})
|
||||
// Subgraph viewport should have been saved automatically
|
||||
const sub1Viewport = navigationStore.viewportCache.get('sub1')
|
||||
expect(sub1Viewport).toBeDefined()
|
||||
expect(sub1Viewport?.scale).toBe(0.5)
|
||||
expect(sub1Viewport?.offset).toEqual([-50, -50])
|
||||
|
||||
// Root viewport restored
|
||||
// Root viewport should be restored automatically
|
||||
expect(mockCanvas.ds.scale).toBe(2)
|
||||
expect(mockCanvas.ds.offset).toEqual([100, 100])
|
||||
})
|
||||
|
||||
it('preserves pre-existing cache entries across workflow switches', async () => {
|
||||
const store = useSubgraphNavigationStore()
|
||||
it('should preserve viewport cache when switching workflows', async () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
store.viewportCache.set(':root', { scale: 2, offset: [0, 0] })
|
||||
store.viewportCache.set(':sub1', { scale: 1.5, offset: [10, 10] })
|
||||
expect(store.viewportCache.size).toBe(2)
|
||||
// Add some viewport states
|
||||
navigationStore.viewportCache.set('root', { scale: 2, offset: [0, 0] })
|
||||
navigationStore.viewportCache.set('sub1', {
|
||||
scale: 1.5,
|
||||
offset: [10, 10]
|
||||
})
|
||||
|
||||
const wf1 = { path: 'wf1.json' } as ComfyWorkflow
|
||||
const wf2 = { path: 'wf2.json' } as ComfyWorkflow
|
||||
expect(navigationStore.viewportCache.size).toBe(2)
|
||||
|
||||
workflowStore.activeWorkflow = wf1 as typeof workflowStore.activeWorkflow
|
||||
// Switch workflows
|
||||
const workflow1 = { path: 'workflow1.json' } as ComfyWorkflow
|
||||
const workflow2 = { path: 'workflow2.json' } as ComfyWorkflow
|
||||
|
||||
workflowStore.activeWorkflow = workflow1 as ReturnType<
|
||||
typeof useWorkflowStore
|
||||
>['activeWorkflow']
|
||||
await nextTick()
|
||||
|
||||
workflowStore.activeWorkflow = wf2 as typeof workflowStore.activeWorkflow
|
||||
workflowStore.activeWorkflow = workflow2 as ReturnType<
|
||||
typeof useWorkflowStore
|
||||
>['activeWorkflow']
|
||||
await nextTick()
|
||||
|
||||
// Pre-existing entries still in cache
|
||||
expect(store.viewportCache.has(':root')).toBe(true)
|
||||
expect(store.viewportCache.has(':sub1')).toBe(true)
|
||||
// Cache should be preserved (LRU will manage memory)
|
||||
expect(navigationStore.viewportCache.size).toBe(2)
|
||||
expect(navigationStore.viewportCache.has('root')).toBe(true)
|
||||
expect(navigationStore.viewportCache.has('sub1')).toBe(true)
|
||||
})
|
||||
|
||||
it('should save/restore viewports correctly across multiple subgraphs', () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
|
||||
navigationStore.viewportCache.set(':root', {
|
||||
navigationStore.viewportCache.set('root', {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
})
|
||||
navigationStore.viewportCache.set(':sub-1', {
|
||||
navigationStore.viewportCache.set('sub-1', {
|
||||
scale: 2,
|
||||
offset: [100, 200]
|
||||
})
|
||||
navigationStore.viewportCache.set(':sub-2', {
|
||||
navigationStore.viewportCache.set('sub-2', {
|
||||
scale: 0.5,
|
||||
offset: [-50, -75]
|
||||
})
|
||||
@@ -319,18 +300,17 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
|
||||
|
||||
// QuickLRU uses double-buffering: effective capacity is up to 2 * maxSize.
|
||||
// Fill enough entries so the earliest ones are fully evicted.
|
||||
// Keys use the workflow-scoped format (`:graphId`) matching production.
|
||||
for (let i = 0; i < overflowEntryCount; i++) {
|
||||
navigationStore.viewportCache.set(`:sub-${i}`, {
|
||||
navigationStore.viewportCache.set(`sub-${i}`, {
|
||||
scale: i + 1,
|
||||
offset: [i * 10, i * 20]
|
||||
})
|
||||
}
|
||||
|
||||
expect(navigationStore.viewportCache.has(':sub-0')).toBe(false)
|
||||
expect(navigationStore.viewportCache.has('sub-0')).toBe(false)
|
||||
|
||||
expect(
|
||||
navigationStore.viewportCache.has(`:sub-${overflowEntryCount - 1}`)
|
||||
navigationStore.viewportCache.has(`sub-${overflowEntryCount - 1}`)
|
||||
).toBe(true)
|
||||
|
||||
mockCanvas.ds.scale = 99
|
||||
|
||||
Reference in New Issue
Block a user