Merge remote-tracking branch 'origin/main' into contextmenu-legacy-compat

This commit is contained in:
Johnpaul
2025-10-13 23:41:38 +01:00
520 changed files with 22964 additions and 12046 deletions

View File

@@ -209,11 +209,13 @@ describe('NodeConflictDialogContent', () => {
const wrapper = createWrapper()
// Find import failed panel header (first one)
const importFailedHeader = wrapper.find('.w-full.h-8.flex.items-center')
const importFailedHeader = wrapper.find(
'[data-testid="conflict-dialog-panel-toggle"]'
)
// Initially collapsed
expect(
wrapper.find('[class*="py-2 px-4 flex flex-col gap-2.5"]').exists()
wrapper.find('[data-testid="conflict-dialog-panel-expanded"]').exists()
).toBe(false)
// Click to expand import failed panel
@@ -221,7 +223,7 @@ describe('NodeConflictDialogContent', () => {
// Should be expanded now and show package name
const expandedContent = wrapper.find(
'[class*="py-2 px-4 flex flex-col gap-2.5"]'
'[data-testid="conflict-dialog-panel-expanded"]'
)
expect(expandedContent.exists()).toBe(true)
expect(expandedContent.text()).toContain('Test Package 3')
@@ -236,7 +238,7 @@ describe('NodeConflictDialogContent', () => {
// Find conflicts panel header (second one)
const conflictsHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[1]
// Click to expand conflicts panel
@@ -252,7 +254,7 @@ describe('NodeConflictDialogContent', () => {
// Find extensions panel header (third one)
const extensionsHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[2]
// Click to expand extensions panel
@@ -260,7 +262,7 @@ describe('NodeConflictDialogContent', () => {
// Should be expanded now and show all package names
const expandedContent = wrapper.findAll(
'[class*="py-2 px-4 flex flex-col gap-2.5"]'
'[data-testid="conflict-dialog-panel-expanded"]'
)[0]
expect(expandedContent.exists()).toBe(true)
expect(expandedContent.text()).toContain('Test Package 1')
@@ -272,13 +274,13 @@ describe('NodeConflictDialogContent', () => {
const wrapper = createWrapper()
const importFailedHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[0]
const conflictsHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[1]
const extensionsHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[2]
// Open import failed panel first
@@ -317,7 +319,7 @@ describe('NodeConflictDialogContent', () => {
// Expand conflicts panel (second header)
const conflictsHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[1]
await conflictsHeader.trigger('click')
@@ -331,7 +333,7 @@ describe('NodeConflictDialogContent', () => {
// Expand import failed panel (first header)
const importFailedHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[0]
await importFailedHeader.trigger('click')
@@ -346,7 +348,7 @@ describe('NodeConflictDialogContent', () => {
// Expand extensions panel (third header)
const extensionsHeader = wrapper.findAll(
'.w-full.h-8.flex.items-center'
'[data-testid="conflict-dialog-panel-toggle"]'
)[2]
await extensionsHeader.trigger('click')
@@ -387,7 +389,9 @@ describe('NodeConflictDialogContent', () => {
const wrapper = createWrapper()
// Test all three panels
const headers = wrapper.findAll('.w-full.h-8.flex.items-center')
const headers = wrapper.findAll(
'[data-testid="conflict-dialog-panel-toggle"]'
)
for (let i = 0; i < headers.length; i++) {
await headers[i].trigger('click')
@@ -423,7 +427,9 @@ describe('NodeConflictDialogContent', () => {
mockConflictData.value = mockConflictResults
const wrapper = createWrapper()
const headers = wrapper.findAll('.w-full.h-8.flex.items-center')
const headers = wrapper.findAll(
'[data-testid="conflict-dialog-panel-toggle"]'
)
expect(headers).toHaveLength(3) // import failed, conflicts and extensions headers
headers.forEach((header) => {

View File

@@ -1,4 +1,8 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import ZoomControlsModal from '@/components/graph/modals/ZoomControlsModal.vue'
// Mock functions
const mockExecute = vi.fn()
@@ -13,7 +17,14 @@ const mockFormatKeySequence = vi.fn().mockReturnValue('Ctrl+')
const mockSetAppZoom = vi.fn()
const mockSettingGet = vi.fn().mockReturnValue(true)
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: {} }
})
// Mock dependencies
vi.mock('@/renderer/extensions/minimap/composables/useMinimap', () => ({
useMinimap: () => ({
containerStyles: { value: { backgroundColor: '#fff', borderRadius: '8px' } }
@@ -41,128 +52,152 @@ vi.mock('@/platform/settings/settingStore', () => ({
})
}))
const createWrapper = (props = {}) => {
return mount(ZoomControlsModal, {
props: {
visible: true,
...props
},
global: {
plugins: [i18n],
stubs: {
Button: false,
InputNumber: false
}
}
})
}
describe('ZoomControlsModal', () => {
it('should have proper props interface', () => {
// Test that the component file structure and basic exports work
expect(mockExecute).toBeDefined()
expect(mockGetCommand).toBeDefined()
expect(mockFormatKeySequence).toBeDefined()
expect(mockSetAppZoom).toBeDefined()
expect(mockSettingGet).toBeDefined()
beforeEach(() => {
vi.restoreAllMocks()
})
it('should call command store execute when executeCommand is invoked', () => {
mockExecute.mockClear()
it('should execute zoom in command when zoom in button is clicked', async () => {
const wrapper = createWrapper()
// Simulate the executeCommand function behavior
const executeCommand = (command: string) => {
mockExecute(command)
}
const buttons = wrapper.findAll('button')
const zoomInButton = buttons.find((btn) =>
btn.text().includes('graphCanvasMenu.zoomIn')
)
expect(zoomInButton).toBeDefined()
await zoomInButton!.trigger('mousedown')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.ZoomIn')
})
it('should execute zoom out command when zoom out button is clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const zoomOutButton = buttons.find((btn) =>
btn.text().includes('graphCanvasMenu.zoomOut')
)
expect(zoomOutButton).toBeDefined()
await zoomOutButton!.trigger('mousedown')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.ZoomOut')
})
it('should execute fit view command when fit view button is clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const fitViewButton = buttons.find((btn) =>
btn.text().includes('zoomControls.zoomToFit')
)
expect(fitViewButton).toBeDefined()
await fitViewButton!.trigger('click')
executeCommand('Comfy.Canvas.FitView')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.FitView')
})
it('should validate zoom input ranges correctly', () => {
mockSetAppZoom.mockClear()
it('should emit close when minimap toggle button is clicked', async () => {
const wrapper = createWrapper()
// Simulate the applyZoom function behavior
const applyZoom = (val: { value: number }) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
mockSetAppZoom(inputValue)
}
const minimapButton = wrapper.find('[data-testid="toggle-minimap-button"]')
expect(minimapButton.exists()).toBe(true)
// Test invalid values
applyZoom({ value: 0 })
applyZoom({ value: 1010 })
applyZoom({ value: NaN })
await minimapButton.trigger('click')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.ToggleMinimap')
expect(wrapper.emitted('close')).toBeTruthy()
expect(wrapper.emitted('close')).toHaveLength(1)
})
it('should not emit close when other command buttons are clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const fitViewButton = buttons.find((btn) =>
btn.text().includes('zoomControls.zoomToFit')
)
expect(fitViewButton).toBeDefined()
await fitViewButton!.trigger('click')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.FitView')
expect(wrapper.emitted('close')).toBeFalsy()
})
it('should call setAppZoomFromPercentage with valid zoom input values', async () => {
const wrapper = createWrapper()
const inputNumber = wrapper.findComponent({ name: 'InputNumber' })
expect(inputNumber.exists()).toBe(true)
// Emit the input event with PrimeVue's InputNumberInputEvent structure
await inputNumber.vm.$emit('input', { value: 150 })
expect(mockSetAppZoom).toHaveBeenCalledWith(150)
})
it('should not call setAppZoomFromPercentage with invalid zoom input values', async () => {
const wrapper = createWrapper()
const inputNumber = wrapper.findComponent({ name: 'InputNumber' })
expect(inputNumber.exists()).toBe(true)
// Test out of range values
await inputNumber.vm.$emit('input', { value: 0 })
expect(mockSetAppZoom).not.toHaveBeenCalled()
// Test valid value
applyZoom({ value: 50 })
expect(mockSetAppZoom).toHaveBeenCalledWith(50)
await inputNumber.vm.$emit('input', { value: 1001 })
expect(mockSetAppZoom).not.toHaveBeenCalled()
})
it('should return correct minimap toggle text based on setting', () => {
const t = (key: string) => {
const translations: Record<string, string> = {
'zoomControls.showMinimap': 'Show Minimap',
'zoomControls.hideMinimap': 'Hide Minimap'
}
return translations[key] || key
}
// Simulate the minimapToggleText computed property
const minimapToggleText = () =>
mockSettingGet('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
// Test when minimap is visible
it('should display "Hide Minimap" when minimap is visible', () => {
mockSettingGet.mockReturnValue(true)
expect(minimapToggleText()).toBe('Hide Minimap')
const wrapper = createWrapper()
// Test when minimap is hidden
const minimapButton = wrapper.find('[data-testid="toggle-minimap-button"]')
expect(minimapButton.text()).toContain('zoomControls.hideMinimap')
})
it('should display "Show Minimap" when minimap is hidden', () => {
mockSettingGet.mockReturnValue(false)
expect(minimapToggleText()).toBe('Show Minimap')
const wrapper = createWrapper()
const minimapButton = wrapper.find('[data-testid="toggle-minimap-button"]')
expect(minimapButton.text()).toContain('zoomControls.showMinimap')
})
it('should format keyboard shortcuts correctly', () => {
mockFormatKeySequence.mockReturnValue('Ctrl+')
it('should display keyboard shortcuts for commands', () => {
const wrapper = createWrapper()
expect(mockFormatKeySequence()).toBe('Ctrl+')
expect(mockGetCommand).toBeDefined()
const buttons = wrapper.findAll('button')
expect(buttons.length).toBeGreaterThan(0)
// Each command button should show the keyboard shortcut
expect(mockFormatKeySequence).toHaveBeenCalled()
})
it('should handle repeat command functionality', () => {
mockExecute.mockClear()
let interval: number | null = null
it('should not be visible when visible prop is false', () => {
const wrapper = createWrapper({ visible: false })
// Simulate the repeat functionality
const startRepeat = (command: string) => {
if (interval) return
const cmd = () => mockExecute(command)
cmd() // Execute immediately
interval = 1 // Mock interval ID
}
const stopRepeat = () => {
if (interval) {
interval = null
}
}
startRepeat('Comfy.Canvas.ZoomIn')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.ZoomIn')
stopRepeat()
expect(interval).toBeNull()
})
it('should have proper filteredMinimapStyles computed property', () => {
const mockContainerStyles = {
backgroundColor: '#fff',
borderRadius: '8px',
height: '100px',
width: '200px'
}
// Simulate the filteredMinimapStyles computed property
const filteredMinimapStyles = () => {
return {
...mockContainerStyles,
height: undefined,
width: undefined
}
}
const result = filteredMinimapStyles()
expect(result.backgroundColor).toBe('#fff')
expect(result.borderRadius).toBe('8px')
expect(result.height).toBeUndefined()
expect(result.width).toBeUndefined()
expect(wrapper.find('.absolute').exists()).toBe(false)
})
})

View File

@@ -301,7 +301,115 @@ describe('useNodePricing', () => {
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
})
// ============================== OpenAIVideoSora2 ==============================
describe('dynamic pricing - OpenAIVideoSora2', () => {
it('should require model, duration & size when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should require duration when duration is invalid or zero', () => {
const { getNodeDisplayPrice } = useNodePricing()
const nodeNaN = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 'oops' },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set model, duration & size')
const nodeZero = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 0 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeZero)).toBe('Set model, duration & size')
})
it('should require size when size is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 }
])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should compute pricing for sora-2-pro with 1024x1792', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
})
it('should compute pricing for sora-2-pro with 720x1280', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
it('should reject unsupported size for sora-2-pro', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '640x640' }
])
expect(getNodeDisplayPrice(node)).toBe(
'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
)
})
it('should compute pricing for sora-2 (720x1280 only)', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 10 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
})
it('should reject non-720 sizes for sora-2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe(
'sora-2 supports only 720x1280 or 1280x720'
)
})
it('should accept duration_s alias for duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration_s', value: 4 },
{ name: 'size', value: '1792x1024' }
])
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
})
it('should be case-insensitive for model and size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'SoRa-2-PrO' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '1280x720' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
})
// ============================== MinimaxHailuoVideoNode ==============================
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
it('should return $0.28 for 6s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()

View File

@@ -1,303 +1,200 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
import { useExtensionStore } from '@/stores/extensionStore'
import type { ComfyExtension } from '@/types/comfy'
describe('Context Menu Extension API', () => {
let mockCanvas: LGraphCanvas
let mockNode: LGraphNode
let extensionStore: ReturnType<typeof useExtensionStore>
let extensionService: ReturnType<typeof useExtensionService>
// Mock menu items
const canvasMenuItem1: IContextMenuValue = {
content: 'Canvas Item 1',
callback: () => {}
}
const canvasMenuItem2: IContextMenuValue = {
content: 'Canvas Item 2',
callback: () => {}
}
const nodeMenuItem1: IContextMenuValue = {
content: 'Node Item 1',
callback: () => {}
}
const nodeMenuItem2: IContextMenuValue = {
content: 'Node Item 2',
callback: () => {}
}
// Mock extensions
const createCanvasMenuExtension = (
name: string,
items: IContextMenuValue[]
): ComfyExtension => ({
name,
getCanvasMenuItems: () => items
})
const createNodeMenuExtension = (
name: string,
items: IContextMenuValue[]
): ComfyExtension => ({
name,
getNodeMenuItems: () => items
})
beforeEach(() => {
// Create mock canvas
setActivePinia(createTestingPinia({ stubActions: false }))
extensionStore = useExtensionStore()
extensionService = useExtensionService()
mockCanvas = {
graph_mouse: [100, 100],
selectedItems: new Set()
} as unknown as LGraphCanvas
// Create mock node
mockNode = {
id: 1,
type: 'TestNode',
pos: [0, 0]
} as unknown as LGraphNode
// Clear console
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('collectCanvasMenuItems', () => {
it('should collect items from extensions with getCanvasMenuItems', () => {
const mockExtension1: ComfyExtension = {
name: 'Test Extension 1',
getCanvasMenuItems: (_canvas: LGraphCanvas) => {
return [
{ content: 'Extension 1 Item', callback: () => {} }
] as IContextMenuValue[]
}
}
const mockExtension2: ComfyExtension = {
name: 'Test Extension 2',
getCanvasMenuItems: (_canvas: LGraphCanvas) => {
return [
{ content: 'Extension 2 Item', callback: () => {} }
] as IContextMenuValue[]
}
}
// Mock extensions array
vi.spyOn(app, 'extensions', 'get').mockReturnValue([
mockExtension1,
mockExtension2
it('should call getCanvasMenuItems and collect into flat array', () => {
const ext1 = createCanvasMenuExtension('Extension 1', [canvasMenuItem1])
const ext2 = createCanvasMenuExtension('Extension 2', [
canvasMenuItem2,
{ content: 'Item 3', callback: () => {} }
])
const items = app.collectCanvasMenuItems(mockCanvas)
extensionStore.registerExtension(ext1)
extensionStore.registerExtension(ext2)
expect(items).toHaveLength(2)
expect(items[0]).toMatchObject({ content: 'Extension 1 Item' })
expect(items[1]).toMatchObject({ content: 'Extension 2 Item' })
const items = extensionService
.invokeExtensions('getCanvasMenuItems', mockCanvas)
.flat() as IContextMenuValue[]
expect(items).toHaveLength(3)
expect(items[0]).toMatchObject({ content: 'Canvas Item 1' })
expect(items[1]).toMatchObject({ content: 'Canvas Item 2' })
expect(items[2]).toMatchObject({ content: 'Item 3' })
})
it('should support submenus and separators', () => {
const extension = createCanvasMenuExtension('Test Extension', [
{
content: 'Menu with Submenu',
has_submenu: true,
submenu: {
options: [
{ content: 'Submenu Item 1', callback: () => {} },
{ content: 'Submenu Item 2', callback: () => {} }
]
}
},
null as unknown as IContextMenuValue,
{ content: 'After Separator', callback: () => {} }
])
extensionStore.registerExtension(extension)
const items = extensionService
.invokeExtensions('getCanvasMenuItems', mockCanvas)
.flat() as IContextMenuValue[]
expect(items).toHaveLength(3)
expect(items[0].content).toBe('Menu with Submenu')
expect(items[0].submenu?.options).toHaveLength(2)
expect(items[1]).toBeNull()
expect(items[2].content).toBe('After Separator')
})
it('should skip extensions without getCanvasMenuItems', () => {
const mockExtension1: ComfyExtension = {
name: 'Test Extension 1',
getCanvasMenuItems: () => {
return [{ content: 'Item 1', callback: () => {} }]
}
}
const mockExtension2: ComfyExtension = {
name: 'Test Extension 2'
// No getCanvasMenuItems
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([
mockExtension1,
mockExtension2
const canvasExtension = createCanvasMenuExtension('Canvas Ext', [
canvasMenuItem1
])
const extensionWithoutCanvasMenu: ComfyExtension = {
name: 'No Canvas Menu'
}
const items = app.collectCanvasMenuItems(mockCanvas)
extensionStore.registerExtension(canvasExtension)
extensionStore.registerExtension(extensionWithoutCanvasMenu)
const items = extensionService
.invokeExtensions('getCanvasMenuItems', mockCanvas)
.flat() as IContextMenuValue[]
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({ content: 'Item 1' })
})
it('should handle errors in extension gracefully', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const mockExtension1: ComfyExtension = {
name: 'Failing Extension',
getCanvasMenuItems: () => {
throw new Error('Extension error')
}
}
const mockExtension2: ComfyExtension = {
name: 'Working Extension',
getCanvasMenuItems: () => {
return [{ content: 'Working Item', callback: () => {} }]
}
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([
mockExtension1,
mockExtension2
])
const items = app.collectCanvasMenuItems(mockCanvas)
// Should have logged error with extension name
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('[Context Menu]'),
expect.any(Error)
)
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('"Failing Extension"'),
expect.any(Error)
)
// Should still return items from working extension
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({ content: 'Working Item' })
})
it('should pass canvas to extension method', () => {
const canvasCapture = vi.fn()
const mockExtension: ComfyExtension = {
name: 'Test Extension',
getCanvasMenuItems: (canvas: LGraphCanvas) => {
canvasCapture(canvas)
return []
}
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([mockExtension])
app.collectCanvasMenuItems(mockCanvas)
expect(canvasCapture).toHaveBeenCalledWith(mockCanvas)
})
it('should return empty array when no extensions', () => {
vi.spyOn(app, 'extensions', 'get').mockReturnValue([])
const items = app.collectCanvasMenuItems(mockCanvas)
expect(items).toHaveLength(0)
expect(items[0].content).toBe('Canvas Item 1')
})
})
describe('collectNodeMenuItems', () => {
it('should collect items from extensions with getNodeMenuItems', () => {
const mockExtension1: ComfyExtension = {
name: 'Test Extension 1',
getNodeMenuItems: (_node: LGraphNode) => {
return [
{ content: 'Node Item 1', callback: () => {} }
] as IContextMenuValue[]
}
}
const mockExtension2: ComfyExtension = {
name: 'Test Extension 2',
getNodeMenuItems: (_node: LGraphNode) => {
return [
{ content: 'Node Item 2', callback: () => {} }
] as IContextMenuValue[]
}
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([
mockExtension1,
mockExtension2
it('should call getNodeMenuItems and collect into flat array', () => {
const ext1 = createNodeMenuExtension('Extension 1', [nodeMenuItem1])
const ext2 = createNodeMenuExtension('Extension 2', [
nodeMenuItem2,
{ content: 'Item 3', callback: () => {} }
])
const items = app.collectNodeMenuItems(mockNode)
extensionStore.registerExtension(ext1)
extensionStore.registerExtension(ext2)
expect(items).toHaveLength(2)
const items = extensionService
.invokeExtensions('getNodeMenuItems', mockNode)
.flat() as IContextMenuValue[]
expect(items).toHaveLength(3)
expect(items[0]).toMatchObject({ content: 'Node Item 1' })
expect(items[1]).toMatchObject({ content: 'Node Item 2' })
})
it('should support submenus', () => {
const extension = createNodeMenuExtension('Submenu Extension', [
{
content: 'Node Menu with Submenu',
has_submenu: true,
submenu: {
options: [
{ content: 'Node Submenu 1', callback: () => {} },
{ content: 'Node Submenu 2', callback: () => {} }
]
}
}
])
extensionStore.registerExtension(extension)
const items = extensionService
.invokeExtensions('getNodeMenuItems', mockNode)
.flat() as IContextMenuValue[]
expect(items[0].content).toBe('Node Menu with Submenu')
expect(items[0].submenu?.options).toHaveLength(2)
})
it('should skip extensions without getNodeMenuItems', () => {
const mockExtension1: ComfyExtension = {
name: 'Test Extension 1',
getNodeMenuItems: () => {
return [{ content: 'Node Item', callback: () => {} }]
}
const nodeExtension = createNodeMenuExtension('Node Ext', [nodeMenuItem1])
const extensionWithoutNodeMenu: ComfyExtension = {
name: 'No Node Menu'
}
const mockExtension2: ComfyExtension = {
name: 'Test Extension 2'
// No getNodeMenuItems
}
extensionStore.registerExtension(nodeExtension)
extensionStore.registerExtension(extensionWithoutNodeMenu)
vi.spyOn(app, 'extensions', 'get').mockReturnValue([
mockExtension1,
mockExtension2
])
const items = app.collectNodeMenuItems(mockNode)
const items = extensionService
.invokeExtensions('getNodeMenuItems', mockNode)
.flat() as IContextMenuValue[]
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({ content: 'Node Item' })
})
it('should handle errors in extension gracefully', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const mockExtension1: ComfyExtension = {
name: 'Failing Extension',
getNodeMenuItems: () => {
throw new Error('Extension error')
}
}
const mockExtension2: ComfyExtension = {
name: 'Working Extension',
getNodeMenuItems: () => {
return [{ content: 'Working Node Item', callback: () => {} }]
}
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([
mockExtension1,
mockExtension2
])
const items = app.collectNodeMenuItems(mockNode)
// Should have logged error with extension name
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('[Context Menu]'),
expect.any(Error)
)
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('"Failing Extension"'),
expect.any(Error)
)
// Should still return items from working extension
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({ content: 'Working Node Item' })
})
it('should pass node to extension method', () => {
const nodeCapture = vi.fn()
const mockExtension: ComfyExtension = {
name: 'Test Extension',
getNodeMenuItems: (node: LGraphNode) => {
nodeCapture(node)
return []
}
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([mockExtension])
app.collectNodeMenuItems(mockNode)
expect(nodeCapture).toHaveBeenCalledWith(mockNode)
})
it('should return empty array when no extensions', () => {
vi.spyOn(app, 'extensions', 'get').mockReturnValue([])
const items = app.collectNodeMenuItems(mockNode)
expect(items).toHaveLength(0)
})
})
describe('integration with both methods', () => {
it('should work when extension provides both methods', () => {
const mockExtension: ComfyExtension = {
name: 'Full Extension',
getCanvasMenuItems: () => {
return [{ content: 'Canvas Item', callback: () => {} }]
},
getNodeMenuItems: () => {
return [{ content: 'Node Item', callback: () => {} }]
}
}
vi.spyOn(app, 'extensions', 'get').mockReturnValue([mockExtension])
const canvasItems = app.collectCanvasMenuItems(mockCanvas)
const nodeItems = app.collectNodeMenuItems(mockNode)
expect(canvasItems).toHaveLength(1)
expect(canvasItems[0]).toMatchObject({ content: 'Canvas Item' })
expect(nodeItems).toHaveLength(1)
expect(nodeItems[0]).toMatchObject({ content: 'Node Item' })
expect(items[0].content).toBe('Node Item 1')
})
})
})

View File

@@ -46,12 +46,12 @@ describe('LGraphNodeProperties', () => {
})
})
it("should not emit events when value doesn't change", () => {
it('should emit event when value is set to the same value', () => {
new LGraphNodeProperties(mockNode)
mockNode.title = 'Test Node' // Same value as original
expect(mockGraph.trigger).toHaveBeenCalledTimes(0)
expect(mockGraph.trigger).toHaveBeenCalledTimes(1)
})
it('should not emit events when node has no graph', () => {

View File

@@ -283,6 +283,7 @@ LGraph {
"nodes_actioning": [],
"nodes_executedAction": [],
"nodes_executing": [],
"onTrigger": undefined,
"revision": 0,
"runningtime": 0,
"starttime": 0,

View File

@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import {
@@ -165,10 +165,11 @@ describe('layoutStore CRDT operations', () => {
actor: layoutStore.getCurrentActor()
})
// Wait for async notification
await new Promise((resolve) => setTimeout(resolve, 50))
// Wait for onChange callback to be called (uses setTimeout internally)
await vi.waitFor(() => {
expect(changes.length).toBeGreaterThanOrEqual(1)
})
expect(changes.length).toBeGreaterThanOrEqual(1)
const lastChange = changes[changes.length - 1]
expect(lastChange.source).toBe('vue')
expect(lastChange.operation.actor).toBe('user-123')
@@ -176,6 +177,48 @@ describe('layoutStore CRDT operations', () => {
unsubscribe()
})
it('should emit change when batch updating node bounds', async () => {
const nodeId = 'test-node-6'
const layout = createTestNode(nodeId)
layoutStore.applyOperation({
type: 'createNode',
entity: 'node',
nodeId,
layout,
timestamp: Date.now(),
source: LayoutSource.External,
actor: 'test'
})
const changes: LayoutChange[] = []
const unsubscribe = layoutStore.onChange((change) => {
changes.push(change)
})
const newBounds = { x: 40, y: 60, width: 220, height: 120 }
layoutStore.batchUpdateNodeBounds([{ nodeId, bounds: newBounds }])
// Wait for onChange callback to be called (uses setTimeout internally)
await vi.waitFor(() => {
expect(changes.length).toBeGreaterThan(0)
const lastChange = changes[changes.length - 1]
expect(lastChange.operation.type).toBe('batchUpdateBounds')
})
const lastChange = changes[changes.length - 1]
if (lastChange.operation.type === 'batchUpdateBounds') {
expect(lastChange.nodeIds).toContain(nodeId)
expect(lastChange.operation.bounds[nodeId]?.bounds).toEqual(newBounds)
}
const nodeRef = layoutStore.getNodeLayoutRef(nodeId)
expect(nodeRef.value?.position).toEqual({ x: 40, y: 60 })
expect(nodeRef.value?.size).toEqual({ width: 220, height: 120 })
unsubscribe()
})
it('should query nodes by spatial bounds', () => {
const nodes = [
{ id: 'node-a', position: { x: 0, y: 0 } },

View File

@@ -6,8 +6,8 @@ import type { ComponentProps } from 'vue-component-type-helpers'
import { createI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
const mockData = vi.hoisted(() => ({
@@ -77,6 +77,13 @@ vi.mock('@/renderer/extensions/vueNodes/preview/useNodePreviewState', () => ({
}))
}))
vi.mock('../composables/useNodeResize', () => ({
useNodeResize: vi.fn(() => ({
startResize: vi.fn(),
isResizing: computed(() => false)
}))
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
@@ -96,6 +103,14 @@ function mountLGraphNode(props: ComponentProps<typeof LGraphNode>) {
}),
i18n
],
provide: {
[TransformStateKey as symbol]: {
screenToCanvas: vi.fn(),
canvasToScreen: vi.fn(),
camera: { z: 1 },
isNodeInViewport: vi.fn()
}
},
stubs: {
NodeHeader: true,
NodeSlots: true,
@@ -155,6 +170,14 @@ describe('LGraphNode', () => {
}),
i18n
],
provide: {
[TransformStateKey as symbol]: {
screenToCanvas: vi.fn(),
canvasToScreen: vi.fn(),
camera: { z: 1 },
isNodeInViewport: vi.fn()
}
},
stubs: {
NodeSlots: true,
NodeWidgets: true,
@@ -181,18 +204,4 @@ describe('LGraphNode', () => {
expect(wrapper.classes()).toContain('animate-pulse')
})
it('should emit node-click event on pointer up', async () => {
const { handleNodeSelect } = useNodeEventHandlers()
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
await wrapper.trigger('pointerup')
expect(handleNodeSelect).toHaveBeenCalledOnce()
expect(handleNodeSelect).toHaveBeenCalledWith(
expect.any(PointerEvent),
mockNodeData,
expect.any(Boolean)
)
})
})

View File

@@ -0,0 +1,134 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import LivePreview from '@/renderer/extensions/vueNodes/components/LivePreview.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: {
liveSamplingPreview: 'Live sampling preview',
imageFailedToLoad: 'Image failed to load',
errorLoadingImage: 'Error loading image',
calculatingDimensions: 'Calculating dimensions'
}
}
}
})
describe('LivePreview', () => {
const defaultProps = {
imageUrl: '/api/view?filename=test_sample.png&type=temp'
}
const mountLivePreview = (props = {}) => {
return mount(LivePreview, {
props: { ...defaultProps, ...props },
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn
}),
i18n
],
stubs: {
'i-lucide:image-off': true
}
}
})
}
it('renders preview when imageUrl provided', () => {
const wrapper = mountLivePreview()
expect(wrapper.find('img').exists()).toBe(true)
expect(wrapper.find('img').attributes('src')).toBe(defaultProps.imageUrl)
})
it('does not render when no imageUrl provided', () => {
const wrapper = mountLivePreview({ imageUrl: null })
expect(wrapper.find('img').exists()).toBe(false)
expect(wrapper.text()).toBe('')
})
it('displays calculating dimensions text initially', () => {
const wrapper = mountLivePreview()
expect(wrapper.text()).toContain('Calculating dimensions')
})
it('has proper accessibility attributes', () => {
const wrapper = mountLivePreview()
const img = wrapper.find('img')
expect(img.attributes('alt')).toBe('Live sampling preview')
})
it('handles image load event', async () => {
const wrapper = mountLivePreview()
const img = wrapper.find('img')
// Mock the naturalWidth and naturalHeight properties on the img element
Object.defineProperty(img.element, 'naturalWidth', {
writable: false,
value: 512
})
Object.defineProperty(img.element, 'naturalHeight', {
writable: false,
value: 512
})
// Trigger the load event
await img.trigger('load')
expect(wrapper.text()).toContain('512 x 512')
})
it('handles image error state', async () => {
const wrapper = mountLivePreview()
const img = wrapper.find('img')
// Trigger the error event
await img.trigger('error')
// Check that the image is hidden and error content is shown
expect(wrapper.find('img').exists()).toBe(false)
expect(wrapper.text()).toContain('Image failed to load')
})
it('resets state when imageUrl changes', async () => {
const wrapper = mountLivePreview()
const img = wrapper.find('img')
// Set error state via event
await img.trigger('error')
expect(wrapper.text()).toContain('Error loading image')
// Change imageUrl prop
await wrapper.setProps({ imageUrl: '/new-image.png' })
await nextTick()
// State should be reset - dimensions text should show calculating
expect(wrapper.text()).toContain('Calculating dimensions')
expect(wrapper.text()).not.toContain('Error loading image')
})
it('shows error state when image fails to load', async () => {
const wrapper = mountLivePreview()
const img = wrapper.find('img')
// Trigger error event
await img.trigger('error')
// Should show error state instead of image
expect(wrapper.find('img').exists()).toBe(false)
expect(wrapper.text()).toContain('Image failed to load')
expect(wrapper.text()).toContain('Error loading image')
})
})

View File

@@ -102,7 +102,7 @@ describe('useNodeEventHandlers', () => {
metaKey: false
})
handleNodeSelect(event, testNodeData, false)
handleNodeSelect(event, testNodeData)
expect(canvas?.deselectAll).toHaveBeenCalledOnce()
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
@@ -122,7 +122,7 @@ describe('useNodeEventHandlers', () => {
metaKey: false
})
handleNodeSelect(ctrlClickEvent, testNodeData, false)
handleNodeSelect(ctrlClickEvent, testNodeData)
expect(canvas?.deselectAll).not.toHaveBeenCalled()
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
@@ -141,7 +141,7 @@ describe('useNodeEventHandlers', () => {
metaKey: false
})
handleNodeSelect(ctrlClickEvent, testNodeData, false)
handleNodeSelect(ctrlClickEvent, testNodeData)
expect(canvas?.deselect).toHaveBeenCalledWith(mockNode)
expect(canvas?.select).not.toHaveBeenCalled()
@@ -159,7 +159,7 @@ describe('useNodeEventHandlers', () => {
metaKey: true
})
handleNodeSelect(metaClickEvent, testNodeData, false)
handleNodeSelect(metaClickEvent, testNodeData)
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
expect(canvas?.deselectAll).not.toHaveBeenCalled()
@@ -171,7 +171,7 @@ describe('useNodeEventHandlers', () => {
mockNode!.flags.pinned = false
const event = new PointerEvent('pointerdown')
handleNodeSelect(event, testNodeData, false)
handleNodeSelect(event, testNodeData)
expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith(
'node-1'
@@ -184,7 +184,7 @@ describe('useNodeEventHandlers', () => {
mockNode!.flags.pinned = true
const event = new PointerEvent('pointerdown')
handleNodeSelect(event, testNodeData, false)
handleNodeSelect(event, testNodeData)
expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled()
})

View File

@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import WidgetAudioUI from '@/renderer/extensions/vueNodes/widgets/components/WidgetAudioUI.vue'
import WidgetButton from '@/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue'
import WidgetColorPicker from '@/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue'
import WidgetFileUpload from '@/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue'
@@ -26,81 +27,81 @@ describe('widgetRegistry', () => {
// Test number type mappings
describe('number types', () => {
it('should map int types to slider widget', () => {
expect(getComponent('int')).toBe(WidgetInputNumber)
expect(getComponent('INT')).toBe(WidgetInputNumber)
expect(getComponent('int', 'bar')).toBe(WidgetInputNumber)
expect(getComponent('INT', 'bar')).toBe(WidgetInputNumber)
})
it('should map float types to slider widget', () => {
expect(getComponent('float')).toBe(WidgetInputNumber)
expect(getComponent('FLOAT')).toBe(WidgetInputNumber)
expect(getComponent('number')).toBe(WidgetInputNumber)
expect(getComponent('slider')).toBe(WidgetInputNumber)
expect(getComponent('float', 'cfg')).toBe(WidgetInputNumber)
expect(getComponent('FLOAT', 'cfg')).toBe(WidgetInputNumber)
expect(getComponent('number', 'cfg')).toBe(WidgetInputNumber)
expect(getComponent('slider', 'cfg')).toBe(WidgetInputNumber)
})
})
// Test text type mappings
describe('text types', () => {
it('should map text variations to input text widget', () => {
expect(getComponent('text')).toBe(WidgetInputText)
expect(getComponent('string')).toBe(WidgetInputText)
expect(getComponent('STRING')).toBe(WidgetInputText)
expect(getComponent('text', 'text')).toBe(WidgetInputText)
expect(getComponent('string', 'text')).toBe(WidgetInputText)
expect(getComponent('STRING', 'text')).toBe(WidgetInputText)
})
it('should map multiline text types to textarea widget', () => {
expect(getComponent('multiline')).toBe(WidgetTextarea)
expect(getComponent('textarea')).toBe(WidgetTextarea)
expect(getComponent('TEXTAREA')).toBe(WidgetTextarea)
expect(getComponent('customtext')).toBe(WidgetTextarea)
expect(getComponent('multiline', 'text')).toBe(WidgetTextarea)
expect(getComponent('textarea', 'text')).toBe(WidgetTextarea)
expect(getComponent('TEXTAREA', 'text')).toBe(WidgetTextarea)
expect(getComponent('customtext', 'text')).toBe(WidgetTextarea)
})
it('should map markdown to markdown widget', () => {
expect(getComponent('MARKDOWN')).toBe(WidgetMarkdown)
expect(getComponent('markdown')).toBe(WidgetMarkdown)
expect(getComponent('MARKDOWN', 'text')).toBe(WidgetMarkdown)
expect(getComponent('markdown', 'text')).toBe(WidgetMarkdown)
})
})
// Test selection type mappings
describe('selection types', () => {
it('should map combo types to select widget', () => {
expect(getComponent('combo')).toBe(WidgetSelect)
expect(getComponent('COMBO')).toBe(WidgetSelect)
expect(getComponent('combo', 'image')).toBe(WidgetSelect)
expect(getComponent('COMBO', 'video')).toBe(WidgetSelect)
})
})
// Test boolean type mappings
describe('boolean types', () => {
it('should map boolean types to toggle switch widget', () => {
expect(getComponent('toggle')).toBe(WidgetToggleSwitch)
expect(getComponent('boolean')).toBe(WidgetToggleSwitch)
expect(getComponent('BOOLEAN')).toBe(WidgetToggleSwitch)
expect(getComponent('toggle', 'image')).toBe(WidgetToggleSwitch)
expect(getComponent('boolean', 'image')).toBe(WidgetToggleSwitch)
expect(getComponent('BOOLEAN', 'image')).toBe(WidgetToggleSwitch)
})
})
// Test advanced widget mappings
describe('advanced widgets', () => {
it('should map color types to color picker widget', () => {
expect(getComponent('color')).toBe(WidgetColorPicker)
expect(getComponent('COLOR')).toBe(WidgetColorPicker)
expect(getComponent('color', 'color')).toBe(WidgetColorPicker)
expect(getComponent('COLOR', 'color')).toBe(WidgetColorPicker)
})
it('should map file types to file upload widget', () => {
expect(getComponent('file')).toBe(WidgetFileUpload)
expect(getComponent('fileupload')).toBe(WidgetFileUpload)
expect(getComponent('FILEUPLOAD')).toBe(WidgetFileUpload)
expect(getComponent('file', 'file')).toBe(WidgetFileUpload)
expect(getComponent('fileupload', 'file')).toBe(WidgetFileUpload)
expect(getComponent('FILEUPLOAD', 'file')).toBe(WidgetFileUpload)
})
it('should map button types to button widget', () => {
expect(getComponent('button')).toBe(WidgetButton)
expect(getComponent('BUTTON')).toBe(WidgetButton)
expect(getComponent('button', '')).toBe(WidgetButton)
expect(getComponent('BUTTON', '')).toBe(WidgetButton)
})
})
// Test fallback behavior
describe('fallback behavior', () => {
it('should return null for unknown types', () => {
expect(getComponent('unknown')).toBe(null)
expect(getComponent('custom_widget')).toBe(null)
expect(getComponent('')).toBe(null)
expect(getComponent('unknown', 'unknown')).toBe(null)
expect(getComponent('custom_widget', 'custom_widget')).toBe(null)
expect(getComponent('', '')).toBe(null)
})
})
})
@@ -165,10 +166,16 @@ describe('widgetRegistry', () => {
it('should handle case sensitivity correctly through aliases', () => {
// Test that both lowercase and uppercase work
expect(getComponent('string')).toBe(WidgetInputText)
expect(getComponent('STRING')).toBe(WidgetInputText)
expect(getComponent('combo')).toBe(WidgetSelect)
expect(getComponent('COMBO')).toBe(WidgetSelect)
expect(getComponent('string', '')).toBe(WidgetInputText)
expect(getComponent('STRING', '')).toBe(WidgetInputText)
expect(getComponent('combo', '')).toBe(WidgetSelect)
expect(getComponent('COMBO', '')).toBe(WidgetSelect)
})
it('should handle combo additional widgets', () => {
// Test that both lowercase and uppercase work
expect(getComponent('combo', 'audio')).toBe(WidgetAudioUI)
expect(getComponent('combo', 'image')).toBe(WidgetSelect)
})
})
})

View File

@@ -145,18 +145,24 @@ describe('useReleaseStore', () => {
it('should show toast for medium/high attention releases', () => {
vi.mocked(semverCompare).mockReturnValue(1)
// Need multiple releases for hasMediumOrHighAttention to work
const mediumRelease = {
...mockRelease,
id: 2,
attention: 'medium' as const
}
store.releases = [mockRelease, mediumRelease]
store.releases = [mockRelease]
expect(store.shouldShowToast).toBe(true)
})
it('should not show toast for low attention releases', () => {
vi.mocked(semverCompare).mockReturnValue(1)
const lowAttentionRelease = {
...mockRelease,
attention: 'low' as const
}
store.releases = [lowAttentionRelease]
expect(store.shouldShowToast).toBe(false)
})
it('should show red dot for new versions', () => {
vi.mocked(semverCompare).mockReturnValue(1)
@@ -490,12 +496,7 @@ describe('useReleaseStore', () => {
vi.mocked(semverCompare).mockReturnValue(1)
const mediumRelease = { ...mockRelease, attention: 'medium' as const }
store.releases = [
mockRelease,
mediumRelease,
{ ...mockRelease, attention: 'low' as const }
]
store.releases = [mockRelease]
expect(store.shouldShowToast).toBe(true)
})
@@ -578,14 +579,7 @@ describe('useReleaseStore', () => {
it('should show toast when conditions are met', () => {
vi.mocked(semverCompare).mockReturnValue(1)
// Need multiple releases for hasMediumOrHighAttention
const mediumRelease = {
...mockRelease,
id: 2,
attention: 'medium' as const
}
store.releases = [mockRelease, mediumRelease]
store.releases = [mockRelease]
expect(store.shouldShowToast).toBe(true)
})
@@ -615,12 +609,7 @@ describe('useReleaseStore', () => {
vi.mocked(semverCompare).mockReturnValue(1)
// Set up all conditions that would normally show toast
const mediumRelease = {
...mockRelease,
id: 2,
attention: 'medium' as const
}
store.releases = [mockRelease, mediumRelease]
store.releases = [mockRelease]
expect(store.shouldShowToast).toBe(false)
})

View File

@@ -1,11 +1,9 @@
import { describe, expect, test, vi } from 'vitest'
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
import {
type LGraphCanvas,
LGraphNode,
type SubgraphNode
} from '@/lib/litegraph/src/litegraph'
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas, SubgraphNode } from '@/lib/litegraph/src/litegraph'
import {
createTestSubgraph,
@@ -66,14 +64,19 @@ describe('Subgraph proxyWidgets', () => {
subgraphNode.widgets[1].name
)
})
test('Will not modify existing widgets', () => {
test('Will serialize existing widgets', () => {
const [subgraphNode, innerNodes] = setupSubgraph(1)
innerNodes[0].addWidget('text', 'stringWidget', 'value', () => {})
innerNodes[0].addWidget('text', 'istringWidget', 'value', () => {})
subgraphNode.addWidget('text', 'stringWidget', 'value', () => {})
subgraphNode.properties.proxyWidgets = [['1', 'stringWidget']]
const proxyWidgets = parseProxyWidgets(subgraphNode.properties.proxyWidgets)
proxyWidgets.push(['1', 'istringWidget'])
subgraphNode.properties.proxyWidgets = proxyWidgets
expect(subgraphNode.widgets.length).toBe(2)
subgraphNode.properties.proxyWidgets = []
expect(subgraphNode.widgets.length).toBe(1)
expect(subgraphNode.widgets[0].name).toBe('stringWidget')
subgraphNode.properties.proxyWidgets = [proxyWidgets[1], proxyWidgets[0]]
expect(subgraphNode.widgets[0].name).toBe('1: istringWidget')
})
test('Will mirror changes to value', () => {
const [subgraphNode, innerNodes] = setupSubgraph(1)