This commit is contained in:
@@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* Workflow Panel Width Persistence Tests
|
||||||
|
* Tests for GitHub issue #22745: Panel width persistence bug fix
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
|
type PanelWidthSource = 'user' | 'system'
|
||||||
|
|
||||||
|
// Mock localStorage for testing
|
||||||
|
const createMockLocalStorage = () => {
|
||||||
|
const storage: Record<string, string> = {}
|
||||||
|
return {
|
||||||
|
getItem: jest.fn((key: string) => storage[key] || null),
|
||||||
|
setItem: jest.fn((key: string, value: string) => {
|
||||||
|
storage[key] = value
|
||||||
|
}),
|
||||||
|
removeItem: jest.fn((key: string) => {
|
||||||
|
delete storage[key]
|
||||||
|
}),
|
||||||
|
clear: jest.fn(() => {
|
||||||
|
Object.keys(storage).forEach(key => delete storage[key])
|
||||||
|
}),
|
||||||
|
get storage() { return { ...storage } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core panel width logic extracted from the component
|
||||||
|
const createPanelWidthManager = (storageKey: string) => {
|
||||||
|
return {
|
||||||
|
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
||||||
|
const newValue = Math.max(400, Math.min(width, 800))
|
||||||
|
if (source === 'user')
|
||||||
|
localStorage.setItem(storageKey, `${newValue}`)
|
||||||
|
|
||||||
|
return newValue
|
||||||
|
},
|
||||||
|
getStoredWidth: () => {
|
||||||
|
const stored = localStorage.getItem(storageKey)
|
||||||
|
return stored ? Number.parseFloat(stored) : 400
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Workflow Panel Width Persistence', () => {
|
||||||
|
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockLocalStorage = createMockLocalStorage()
|
||||||
|
Object.defineProperty(globalThis, 'localStorage', {
|
||||||
|
value: mockLocalStorage,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Node Panel Width Management', () => {
|
||||||
|
const storageKey = 'workflow-node-panel-width'
|
||||||
|
|
||||||
|
it('should save user resize to localStorage', () => {
|
||||||
|
const manager = createPanelWidthManager(storageKey)
|
||||||
|
|
||||||
|
const result = manager.updateWidth(500, 'user')
|
||||||
|
|
||||||
|
expect(result).toBe(500)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, '500')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not save system compression to localStorage', () => {
|
||||||
|
const manager = createPanelWidthManager(storageKey)
|
||||||
|
|
||||||
|
const result = manager.updateWidth(200, 'system')
|
||||||
|
|
||||||
|
expect(result).toBe(400) // Respects minimum width
|
||||||
|
expect(localStorage.setItem).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should enforce minimum width of 400px', () => {
|
||||||
|
const manager = createPanelWidthManager(storageKey)
|
||||||
|
|
||||||
|
// User tries to set below minimum
|
||||||
|
const userResult = manager.updateWidth(300, 'user')
|
||||||
|
expect(userResult).toBe(400)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, '400')
|
||||||
|
|
||||||
|
// System compression below minimum
|
||||||
|
const systemResult = manager.updateWidth(150, 'system')
|
||||||
|
expect(systemResult).toBe(400)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledTimes(1) // Only user call
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve user preferences during system compression', () => {
|
||||||
|
localStorage.setItem(storageKey, '600')
|
||||||
|
const manager = createPanelWidthManager(storageKey)
|
||||||
|
|
||||||
|
// System compresses panel
|
||||||
|
manager.updateWidth(200, 'system')
|
||||||
|
|
||||||
|
// User preference should remain unchanged
|
||||||
|
expect(localStorage.getItem(storageKey)).toBe('600')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Bug Scenario Reproduction', () => {
|
||||||
|
it('should reproduce original bug behavior (for comparison)', () => {
|
||||||
|
const storageKey = 'workflow-node-panel-width'
|
||||||
|
|
||||||
|
// Original buggy behavior - always saves regardless of source
|
||||||
|
const buggyUpdate = (width: number) => {
|
||||||
|
localStorage.setItem(storageKey, `${width}`)
|
||||||
|
return Math.max(400, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(storageKey, '500') // User preference
|
||||||
|
buggyUpdate(200) // System compression pollutes localStorage
|
||||||
|
|
||||||
|
expect(localStorage.getItem(storageKey)).toBe('200') // Bug: corrupted state
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should verify fix prevents localStorage pollution', () => {
|
||||||
|
const storageKey = 'workflow-node-panel-width'
|
||||||
|
const manager = createPanelWidthManager(storageKey)
|
||||||
|
|
||||||
|
localStorage.setItem(storageKey, '500') // User preference
|
||||||
|
manager.updateWidth(200, 'system') // System compression
|
||||||
|
|
||||||
|
expect(localStorage.getItem(storageKey)).toBe('500') // Fix: preserved state
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle multiple rapid operations correctly', () => {
|
||||||
|
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||||
|
|
||||||
|
// Rapid system adjustments
|
||||||
|
manager.updateWidth(300, 'system')
|
||||||
|
manager.updateWidth(250, 'system')
|
||||||
|
manager.updateWidth(180, 'system')
|
||||||
|
|
||||||
|
// Single user adjustment
|
||||||
|
manager.updateWidth(550, 'user')
|
||||||
|
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledTimes(1)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith('workflow-node-panel-width', '550')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle corrupted localStorage gracefully', () => {
|
||||||
|
localStorage.setItem('workflow-node-panel-width', '150') // Below minimum
|
||||||
|
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||||
|
|
||||||
|
const storedWidth = manager.getStoredWidth()
|
||||||
|
expect(storedWidth).toBe(150) // Returns raw value
|
||||||
|
|
||||||
|
// User can correct the preference
|
||||||
|
const correctedWidth = manager.updateWidth(500, 'user')
|
||||||
|
expect(correctedWidth).toBe(500)
|
||||||
|
expect(localStorage.getItem('workflow-node-panel-width')).toBe('500')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('TypeScript Type Safety', () => {
|
||||||
|
it('should enforce source parameter type', () => {
|
||||||
|
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||||
|
|
||||||
|
// Valid source values
|
||||||
|
manager.updateWidth(500, 'user')
|
||||||
|
manager.updateWidth(500, 'system')
|
||||||
|
|
||||||
|
// Default to 'user'
|
||||||
|
manager.updateWidth(500)
|
||||||
|
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledTimes(2) // user + default
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@@ -99,15 +99,18 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
return Math.max(available, 400)
|
return Math.max(available, 400)
|
||||||
}, [workflowCanvasWidth, otherPanelWidth])
|
}, [workflowCanvasWidth, otherPanelWidth])
|
||||||
|
|
||||||
const updateNodePanelWidth = useCallback((width: number) => {
|
const updateNodePanelWidth = useCallback((width: number, source: 'user' | 'system' = 'user') => {
|
||||||
// Ensure the width is within the min and max range
|
// Ensure the width is within the min and max range
|
||||||
const newValue = Math.max(400, Math.min(width, maxNodePanelWidth))
|
const newValue = Math.max(400, Math.min(width, maxNodePanelWidth))
|
||||||
localStorage.setItem('workflow-node-panel-width', `${newValue}`)
|
|
||||||
|
if (source === 'user')
|
||||||
|
localStorage.setItem('workflow-node-panel-width', `${newValue}`)
|
||||||
|
|
||||||
setNodePanelWidth(newValue)
|
setNodePanelWidth(newValue)
|
||||||
}, [maxNodePanelWidth, setNodePanelWidth])
|
}, [maxNodePanelWidth, setNodePanelWidth])
|
||||||
|
|
||||||
const handleResize = useCallback((width: number) => {
|
const handleResize = useCallback((width: number) => {
|
||||||
updateNodePanelWidth(width)
|
updateNodePanelWidth(width, 'user')
|
||||||
}, [updateNodePanelWidth])
|
}, [updateNodePanelWidth])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -121,7 +124,10 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
onResize: debounce(handleResize),
|
onResize: debounce(handleResize),
|
||||||
})
|
})
|
||||||
|
|
||||||
const debounceUpdate = debounce(updateNodePanelWidth)
|
const debounceUpdate = debounce((width: number) => {
|
||||||
|
updateNodePanelWidth(width, 'system')
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workflowCanvasWidth)
|
if (!workflowCanvasWidth)
|
||||||
return
|
return
|
||||||
@@ -132,7 +138,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
const target = Math.max(workflowCanvasWidth - otherPanelWidth - reservedCanvasWidth, 400)
|
const target = Math.max(workflowCanvasWidth - otherPanelWidth - reservedCanvasWidth, 400)
|
||||||
debounceUpdate(target)
|
debounceUpdate(target)
|
||||||
}
|
}
|
||||||
}, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, updateNodePanelWidth])
|
}, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, debounceUpdate])
|
||||||
|
|
||||||
const { handleNodeSelect } = useNodesInteractions()
|
const { handleNodeSelect } = useNodesInteractions()
|
||||||
const { nodesReadOnly } = useNodesReadOnly()
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Debug and Preview Panel Width Persistence Tests
|
||||||
|
* Tests for GitHub issue #22745: Panel width persistence bug fix
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
|
type PanelWidthSource = 'user' | 'system'
|
||||||
|
|
||||||
|
// Mock localStorage for testing
|
||||||
|
const createMockLocalStorage = () => {
|
||||||
|
const storage: Record<string, string> = {}
|
||||||
|
return {
|
||||||
|
getItem: jest.fn((key: string) => storage[key] || null),
|
||||||
|
setItem: jest.fn((key: string, value: string) => {
|
||||||
|
storage[key] = value
|
||||||
|
}),
|
||||||
|
removeItem: jest.fn((key: string) => {
|
||||||
|
delete storage[key]
|
||||||
|
}),
|
||||||
|
clear: jest.fn(() => {
|
||||||
|
Object.keys(storage).forEach(key => delete storage[key])
|
||||||
|
}),
|
||||||
|
get storage() { return { ...storage } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview panel width logic
|
||||||
|
const createPreviewPanelManager = () => {
|
||||||
|
const storageKey = 'debug-and-preview-panel-width'
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
||||||
|
const newValue = Math.max(400, Math.min(width, 800))
|
||||||
|
if (source === 'user')
|
||||||
|
localStorage.setItem(storageKey, `${newValue}`)
|
||||||
|
|
||||||
|
return newValue
|
||||||
|
},
|
||||||
|
getStoredWidth: () => {
|
||||||
|
const stored = localStorage.getItem(storageKey)
|
||||||
|
return stored ? Number.parseFloat(stored) : 400
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Debug and Preview Panel Width Persistence', () => {
|
||||||
|
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockLocalStorage = createMockLocalStorage()
|
||||||
|
Object.defineProperty(globalThis, 'localStorage', {
|
||||||
|
value: mockLocalStorage,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Preview Panel Width Management', () => {
|
||||||
|
it('should save user resize to localStorage', () => {
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
const result = manager.updateWidth(450, 'user')
|
||||||
|
|
||||||
|
expect(result).toBe(450)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '450')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not save system compression to localStorage', () => {
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
const result = manager.updateWidth(300, 'system')
|
||||||
|
|
||||||
|
expect(result).toBe(400) // Respects minimum width
|
||||||
|
expect(localStorage.setItem).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should behave identically to Node Panel', () => {
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
// Both user and system operations should behave consistently
|
||||||
|
manager.updateWidth(500, 'user')
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500')
|
||||||
|
|
||||||
|
manager.updateWidth(200, 'system')
|
||||||
|
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Dual Panel Scenario', () => {
|
||||||
|
it('should maintain independence from Node Panel', () => {
|
||||||
|
localStorage.setItem('workflow-node-panel-width', '600')
|
||||||
|
localStorage.setItem('debug-and-preview-panel-width', '450')
|
||||||
|
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
// System compresses preview panel
|
||||||
|
manager.updateWidth(200, 'system')
|
||||||
|
|
||||||
|
// Only preview panel storage key should be unaffected
|
||||||
|
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('450')
|
||||||
|
expect(localStorage.getItem('workflow-node-panel-width')).toBe('600')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle F12 scenario consistently', () => {
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
// User sets preference
|
||||||
|
manager.updateWidth(500, 'user')
|
||||||
|
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
||||||
|
|
||||||
|
// F12 opens causing viewport compression
|
||||||
|
manager.updateWidth(180, 'system')
|
||||||
|
|
||||||
|
// User preference preserved
|
||||||
|
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Consistency with Node Panel', () => {
|
||||||
|
it('should enforce same minimum width rules', () => {
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
// Same 400px minimum as Node Panel
|
||||||
|
const result = manager.updateWidth(300, 'user')
|
||||||
|
expect(result).toBe(400)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '400')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use same source parameter pattern', () => {
|
||||||
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
|
// Default to 'user' when source not specified
|
||||||
|
manager.updateWidth(500)
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500')
|
||||||
|
|
||||||
|
// Explicit 'system' source
|
||||||
|
manager.updateWidth(300, 'system')
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledTimes(1) // Only user call
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@@ -53,8 +53,9 @@ const DebugAndPreview = () => {
|
|||||||
const nodePanelWidth = useStore(s => s.nodePanelWidth)
|
const nodePanelWidth = useStore(s => s.nodePanelWidth)
|
||||||
const panelWidth = useStore(s => s.previewPanelWidth)
|
const panelWidth = useStore(s => s.previewPanelWidth)
|
||||||
const setPanelWidth = useStore(s => s.setPreviewPanelWidth)
|
const setPanelWidth = useStore(s => s.setPreviewPanelWidth)
|
||||||
const handleResize = useCallback((width: number) => {
|
const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => {
|
||||||
localStorage.setItem('debug-and-preview-panel-width', `${width}`)
|
if (source === 'user')
|
||||||
|
localStorage.setItem('debug-and-preview-panel-width', `${width}`)
|
||||||
setPanelWidth(width)
|
setPanelWidth(width)
|
||||||
}, [setPanelWidth])
|
}, [setPanelWidth])
|
||||||
const maxPanelWidth = useMemo(() => {
|
const maxPanelWidth = useMemo(() => {
|
||||||
@@ -74,7 +75,9 @@ const DebugAndPreview = () => {
|
|||||||
triggerDirection: 'left',
|
triggerDirection: 'left',
|
||||||
minWidth: 400,
|
minWidth: 400,
|
||||||
maxWidth: maxPanelWidth,
|
maxWidth: maxPanelWidth,
|
||||||
onResize: debounce(handleResize),
|
onResize: debounce((width: number) => {
|
||||||
|
handleResize(width, 'user')
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Reference in New Issue
Block a user