mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: LGraphCanvas: add type guard for widget.getContextMenuOptions() and validate returned value (#9390)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,7 @@ import type { IBaseWidget, TWidgetValue } from './types/widgets'
|
||||
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
|
||||
import { findFirstNode, getAllNestedItems } from './utils/collections'
|
||||
import { resolveConnectingLinkColor } from './utils/linkColors'
|
||||
import { hasWidgetContextMenuOptions } from './utils/type'
|
||||
import { createUuidv4 } from './utils/uuid'
|
||||
import type { UUID } from './utils/uuid'
|
||||
import { BaseWidget } from './widgets/BaseWidget'
|
||||
@@ -8493,6 +8494,18 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
} else {
|
||||
// on node
|
||||
menu_info = this.getNodeMenuOptions(node)
|
||||
|
||||
const widget = node.getWidgetOnPos(event.canvasX, event.canvasY)
|
||||
if (widget && hasWidgetContextMenuOptions(widget)) {
|
||||
const widgetMenuItems = widget.getContextMenuOptions({
|
||||
e: event,
|
||||
node,
|
||||
canvas: this
|
||||
})
|
||||
if (Array.isArray(widgetMenuItems) && widgetMenuItems.length) {
|
||||
menu_info.unshift(...widgetMenuItems, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu_info = this.getCanvasMenuOptions()
|
||||
|
||||
99
src/lib/litegraph/src/LGraphCanvas.widgetContextMenu.test.ts
Normal file
99
src/lib/litegraph/src/LGraphCanvas.widgetContextMenu.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { hasWidgetContextMenuOptions } from '@/lib/litegraph/src/utils/type'
|
||||
|
||||
function createMockWidget(
|
||||
overrides: Partial<IBaseWidget> & Record<string, unknown> = {}
|
||||
): IBaseWidget {
|
||||
return {
|
||||
name: 'test',
|
||||
type: 'number',
|
||||
y: 0,
|
||||
options: {},
|
||||
...overrides
|
||||
} as IBaseWidget
|
||||
}
|
||||
|
||||
describe('hasWidgetContextMenuOptions', () => {
|
||||
it('returns true for a widget with a callable getContextMenuOptions', () => {
|
||||
const widget = createMockWidget({
|
||||
getContextMenuOptions: vi.fn().mockReturnValue([])
|
||||
})
|
||||
expect(hasWidgetContextMenuOptions(widget)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for a widget without getContextMenuOptions', () => {
|
||||
const widget = createMockWidget()
|
||||
expect(hasWidgetContextMenuOptions(widget)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for a widget where getContextMenuOptions is not a function', () => {
|
||||
const widget = createMockWidget({
|
||||
getContextMenuOptions: 'not-a-function' as unknown
|
||||
})
|
||||
expect(hasWidgetContextMenuOptions(widget)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('widget context menu options Array.isArray guard', () => {
|
||||
it('accepts a valid non-empty array', () => {
|
||||
const getContextMenuOptions = vi
|
||||
.fn()
|
||||
.mockReturnValue([{ content: 'Test Option' }])
|
||||
const widget = createMockWidget({ getContextMenuOptions })
|
||||
|
||||
if (hasWidgetContextMenuOptions(widget)) {
|
||||
const result = widget.getContextMenuOptions({
|
||||
e: {} as never,
|
||||
node: {} as never,
|
||||
canvas: {} as never
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts an empty array', () => {
|
||||
const getContextMenuOptions = vi.fn().mockReturnValue([])
|
||||
const widget = createMockWidget({ getContextMenuOptions })
|
||||
|
||||
if (hasWidgetContextMenuOptions(widget)) {
|
||||
const result = widget.getContextMenuOptions({
|
||||
e: {} as never,
|
||||
node: {} as never,
|
||||
canvas: {} as never
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('safely handles undefined return value', () => {
|
||||
const getContextMenuOptions = vi.fn().mockReturnValue(undefined)
|
||||
const widget = createMockWidget({ getContextMenuOptions })
|
||||
|
||||
if (hasWidgetContextMenuOptions(widget)) {
|
||||
const result = widget.getContextMenuOptions({
|
||||
e: {} as never,
|
||||
node: {} as never,
|
||||
canvas: {} as never
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('safely handles non-array return value', () => {
|
||||
const getContextMenuOptions = vi.fn().mockReturnValue('not an array')
|
||||
const widget = createMockWidget({ getContextMenuOptions })
|
||||
|
||||
if (hasWidgetContextMenuOptions(widget)) {
|
||||
const result = widget.getContextMenuOptions({
|
||||
e: {} as never,
|
||||
node: {} as never,
|
||||
canvas: {} as never
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,35 @@
|
||||
import { without } from 'es-toolkit'
|
||||
|
||||
import type { IColorable, ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeBindable } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
IColorable,
|
||||
IContextMenuValue,
|
||||
ISlotType
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
NodeBindable
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import type { CanvasPointerEvent } from '../types/events'
|
||||
import type { LGraphCanvas } from '../LGraphCanvas'
|
||||
import type { LGraphNode } from '../LGraphNode'
|
||||
|
||||
export interface IWidgetWithContextMenu extends IBaseWidget {
|
||||
getContextMenuOptions(opts: {
|
||||
e: CanvasPointerEvent
|
||||
node: LGraphNode
|
||||
canvas: LGraphCanvas
|
||||
}): IContextMenuValue[]
|
||||
}
|
||||
|
||||
export function hasWidgetContextMenuOptions(
|
||||
widget: IBaseWidget
|
||||
): widget is IWidgetWithContextMenu {
|
||||
return (
|
||||
'getContextMenuOptions' in widget &&
|
||||
typeof widget.getContextMenuOptions === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain object to a class instance if it is not already an instance of the class.
|
||||
|
||||
Reference in New Issue
Block a user