Address remaining PR feedback: type safety and robustness improvements

This commit is contained in:
DrJKL
2026-01-12 18:17:35 -08:00
parent 4c46f5786b
commit 4a12f4bc7e
28 changed files with 251 additions and 160 deletions

View File

@@ -82,7 +82,7 @@ watch(
: inputValue.value
const start = 0
const end = fileName.length
el.setSelectionRange?.(start, end)
el.setSelectionRange(start, end)
})
}
},

View File

@@ -75,7 +75,11 @@ function getFormAttrs(item: FormItem) {
: item.options
attrs['options'] = options
if (options && typeof options[0] !== 'string') {
if (
Array.isArray(options) &&
options.length > 0 &&
typeof options[0] !== 'string'
) {
attrs['optionLabel'] = 'text'
attrs['optionValue'] = 'value'
}

View File

@@ -61,7 +61,7 @@ describe('TreeExplorerTreeNode', () => {
expect(wrapper.findComponent(EditableText).props('modelValue')).toBe(
'Test Node'
)
const badgeValue = wrapper.findComponent(Badge).props()['value']
const badgeValue = wrapper.findComponent(Badge).props('value')
expect(String(badgeValue)).toBe('3')
})

View File

@@ -76,7 +76,9 @@ describe('MissingCoreNodesMessage', () => {
vi.clearAllMocks()
mockSystemStatsStore = createMockSystemStatsStore()
vi.mocked(useSystemStatsStore).mockReturnValue(
mockSystemStatsStore as unknown as ReturnType<typeof useSystemStatsStore>
mockSystemStatsStore as Partial<
ReturnType<typeof useSystemStatsStore>
> as ReturnType<typeof useSystemStatsStore>
)
})

View File

@@ -428,7 +428,12 @@ onMounted(async () => {
await newUserService().initializeIfNewUser(settingStore)
if (!canvasRef.value) return
if (!canvasRef.value) {
console.error(
'[GraphCanvas] canvasRef.value is null during onMounted - comfyApp.setup was skipped'
)
return
}
await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false

View File

@@ -63,5 +63,13 @@ const closePopover = () => {
popover.value?.hide()
}
defineExpose({ popover, closePopover })
const openPopover = (event: Event) => {
popover.value?.show(event)
}
const togglePopover = (event: Event) => {
popover.value?.toggle(event)
}
defineExpose({ closePopover, openPopover, togglePopover })
</script>

View File

@@ -36,54 +36,76 @@ interface MockStore {
maskOpacity: number
}
function getImgCanvas(): MockCanvas {
if (!mockStore.imgCanvas) throw new Error('imgCanvas not initialized')
return mockStore.imgCanvas
}
const {
mockStore,
getImgCanvas,
getMaskCanvas,
getRgbCanvas,
getImgCtx,
getMaskCtx,
getRgbCtx,
getCanvasBackground
} = vi.hoisted(() => {
const mockStore: MockStore = {
imgCanvas: null,
maskCanvas: null,
rgbCanvas: null,
imgCtx: null,
maskCtx: null,
rgbCtx: null,
canvasBackground: null,
maskColor: { r: 0, g: 0, b: 0 },
maskBlendMode: 'black' as MaskBlendMode,
maskOpacity: 0.8
}
function getMaskCanvas(): MockCanvas {
if (!mockStore.maskCanvas) throw new Error('maskCanvas not initialized')
return mockStore.maskCanvas
}
function getImgCanvas(): MockCanvas {
if (!mockStore.imgCanvas) throw new Error('imgCanvas not initialized')
return mockStore.imgCanvas
}
function getRgbCanvas(): MockCanvas {
if (!mockStore.rgbCanvas) throw new Error('rgbCanvas not initialized')
return mockStore.rgbCanvas
}
function getMaskCanvas(): MockCanvas {
if (!mockStore.maskCanvas) throw new Error('maskCanvas not initialized')
return mockStore.maskCanvas
}
function getImgCtx(): MockContext {
if (!mockStore.imgCtx) throw new Error('imgCtx not initialized')
return mockStore.imgCtx
}
function getRgbCanvas(): MockCanvas {
if (!mockStore.rgbCanvas) throw new Error('rgbCanvas not initialized')
return mockStore.rgbCanvas
}
function getMaskCtx(): MockContext {
if (!mockStore.maskCtx) throw new Error('maskCtx not initialized')
return mockStore.maskCtx
}
function getImgCtx(): MockContext {
if (!mockStore.imgCtx) throw new Error('imgCtx not initialized')
return mockStore.imgCtx
}
function getRgbCtx(): MockContext {
if (!mockStore.rgbCtx) throw new Error('rgbCtx not initialized')
return mockStore.rgbCtx
}
function getMaskCtx(): MockContext {
if (!mockStore.maskCtx) throw new Error('maskCtx not initialized')
return mockStore.maskCtx
}
function getCanvasBackground(): { style: Partial<MockCanvasStyle> } {
if (!mockStore.canvasBackground)
throw new Error('canvasBackground not initialized')
return mockStore.canvasBackground
}
function getRgbCtx(): MockContext {
if (!mockStore.rgbCtx) throw new Error('rgbCtx not initialized')
return mockStore.rgbCtx
}
const mockStore: MockStore = {
imgCanvas: null,
maskCanvas: null,
rgbCanvas: null,
imgCtx: null,
maskCtx: null,
rgbCtx: null,
canvasBackground: null,
maskColor: { r: 0, g: 0, b: 0 },
maskBlendMode: MaskBlendMode.Black,
maskOpacity: 0.8
}
function getCanvasBackground(): { style: Partial<MockCanvasStyle> } {
if (!mockStore.canvasBackground)
throw new Error('canvasBackground not initialized')
return mockStore.canvasBackground
}
return {
mockStore,
getImgCanvas,
getMaskCanvas,
getRgbCanvas,
getImgCtx,
getMaskCtx,
getRgbCtx,
getCanvasBackground
}
})
vi.mock('@/stores/maskEditorStore', () => ({
useMaskEditorStore: vi.fn(() => mockStore)

View File

@@ -36,7 +36,10 @@ class FilteredContextMenu<TValue = unknown> extends ContextMenu<TValue> {
// We must request an animation frame for the current node of the active canvas to update.
requestAnimationFrame(() => {
const currentNode = LGraphCanvas.active_canvas.current_node
const activeCanvas = LGraphCanvas.active_canvas
if (!activeCanvas) return
const currentNode = activeCanvas.current_node
const clickedComboValue = currentNode?.widgets
?.filter(
(w) =>

View File

@@ -666,9 +666,12 @@ export class GroupNodeConfig {
unknown,
Record<string, unknown>
]
// Intentional casts: widget config types are dynamic at runtime and the
// compiler cannot infer the union shapes expected by mergeIfValid.
const output = { widget: primitiveConfig } as unknown as Parameters<
typeof mergeIfValid
>[0]
// Intentional casts for targetWidget and primitiveConfig — see mergeIfValid.
const config = mergeIfValid(
output,
targetWidget as Parameters<typeof mergeIfValid>[1],

View File

@@ -51,7 +51,16 @@ export interface GroupNodeInputsSpec {
export type GroupNodeOutputType = string | (string | number)[]
/**
* Partial link info used internally by group node getInputLink override.
* Extends ILinkRouting to be compatible with the base getInputLink return type.
* Represents a partial or synthetic link used internally by group node's
* `getInputLink` override when resolving connections through collapsed group nodes.
*
* Unlike a full `ILinkRouting`, this represents a computed/virtual link that may not
* correspond to an actual link in the graph's link registry. It's constructed on-the-fly
* to represent the logical connection path through group node boundaries.
*
* This type aliases `ILinkRouting` (rather than narrowing it) because the consuming code
* expects the same shape for both real and synthetic links. The distinction is purely
* semantic: callers should be aware that these links are transient and may not have
* valid `link_id` references in the global link map.
*/
export interface PartialLinkInfo extends ILinkRouting {}

View File

@@ -37,12 +37,15 @@ app.registerExtension({
})
container.replaceChildren(video)
setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener(
'loadedmetadata',
() => resolveVideo(video),
false
)
let resolved = false
const resolve = () => {
if (resolved) return
resolved = true
clearTimeout(fallbackTimeout)
resolveVideo(video)
}
const fallbackTimeout = setTimeout(resolve, 500)
video.addEventListener('loadedmetadata', resolve, { once: true })
video.srcObject = stream
video.play()
} catch (error) {

View File

@@ -112,8 +112,12 @@ export class PrimitiveNode extends LGraphNode {
const newValues = rawValues.map(String)
comboWidget.options.values = newValues
if (!newValues.includes(String(comboWidget.value))) {
comboWidget.value = newValues[0]
comboWidget.callback?.(comboWidget.value)
if (newValues.length > 0) {
comboWidget.value = newValues[0]
comboWidget.callback?.(comboWidget.value)
} else {
comboWidget.value = ''
}
}
}
}

View File

@@ -705,15 +705,17 @@ export class LGraph
const ctorA = A.constructor
const ctorB = B.constructor
const priorityA =
('priority' in ctorA && typeof ctorA.priority === 'number'
'priority' in ctorA && typeof ctorA.priority === 'number'
? ctorA.priority
: 0) ??
('priority' in A && typeof A.priority === 'number' ? A.priority : 0)
: 'priority' in A && typeof A.priority === 'number'
? A.priority
: 0
const priorityB =
('priority' in ctorB && typeof ctorB.priority === 'number'
'priority' in ctorB && typeof ctorB.priority === 'number'
? ctorB.priority
: 0) ??
('priority' in B && typeof B.priority === 'number' ? B.priority : 0)
: 'priority' in B && typeof B.priority === 'number'
? B.priority
: 0
// if same priority, sort by order
return priorityA == priorityB ? A.order - B.order : priorityA - priorityB
})

View File

@@ -104,6 +104,14 @@ import type { UUID } from './utils/uuid'
import { BaseWidget } from './widgets/BaseWidget'
import { toConcreteWidget } from './widgets/widgetMap'
function isContentEditable(el: HTMLElement | null): boolean {
while (el) {
if (el.isContentEditable) return true
el = el.parentElement
}
return false
}
interface IShowSearchOptions {
node_to?: SubgraphOutputNode | LGraphNode | null
node_from?: SubgraphInputNode | LGraphNode | null
@@ -3681,6 +3689,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
let block_default = false
const targetEl = e.target
if (targetEl instanceof HTMLInputElement) return
if (targetEl instanceof HTMLTextAreaElement) return
if (targetEl instanceof HTMLElement && isContentEditable(targetEl)) return
if (e.type == 'keydown') {
// TODO: Switch
@@ -3717,18 +3727,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.pasteFromClipboard({ connectInputs: e.shiftKey })
} else if (e.key === 'Delete' || e.key === 'Backspace') {
// delete or backspace
if (
!(targetEl instanceof HTMLInputElement) &&
!(targetEl instanceof HTMLTextAreaElement)
) {
if (this.selectedItems.size === 0) {
this.#noItemsSelected()
return
}
this.deleteSelected()
block_default = true
if (this.selectedItems.size === 0) {
this.#noItemsSelected()
return
}
this.deleteSelected()
block_default = true
}
// TODO
@@ -7839,6 +7844,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (typeof root.onOpen == 'function') root.onOpen()
root.graph = this.graph
return root
}

View File

@@ -856,22 +856,17 @@ export class LiteGraphGlobal {
capture
)
}
}
if (
pointerAndMouseEvents.includes(sEvent) ||
pointerOnlyEvents.includes(sEvent)
) {
} else if (pointerOnlyEvents.includes(sEvent)) {
if (this.pointerevents_method == 'pointer') {
return oDOM.removeEventListener(
oDOM.removeEventListener(
this.pointerevents_method + sEvent,
fCall,
capture
)
}
} else {
oDOM.removeEventListener(sEvent, fCall, capture)
}
return oDOM.removeEventListener(sEvent, fCall, capture)
}
getTime(): number {

View File

@@ -25,9 +25,10 @@ describe('NodeSlot', () => {
const serialized = outputAsSerialisable(slot)
expect(serialized).not.toHaveProperty('_data')
})
})
describe('inputAsSerialisable', () => {
it('removes pos from widget input slots', () => {
// Minimal slot for serialization test - boundingRect is calculated at runtime, not serialized
const widgetInputSlot: INodeInputSlot = {
name: 'test-id',
pos: [10, 20],
@@ -54,7 +55,6 @@ describe('NodeSlot', () => {
})
it('preserves only widget name during serialization', () => {
// Extra widget properties simulate real data that should be stripped during serialization
const widgetInputSlot: INodeInputSlot = {
name: 'test-id',
type: 'STRING',

View File

@@ -289,10 +289,13 @@ describe.skip('SubgraphSerialization - Version Compatibility', () => {
id: 'future-id',
name: 'Future Subgraph'
})
// Test with unknown future fields - simulating a hypothetical future version
// Test with unknown future fields - simulating a hypothetical future version.
// NOTE: The "as unknown as ExportedSubgraph" assertion below is an intentional
// exception to normal type-safety rules. It simulates a future schema/version
// to validate deserialization resilience against forward-compatible data.
const extendedFormat = {
...futureFormat,
version: 2 as const, // Type assertion for test purposes
version: 2 as const,
futureFeature: 'unknown_data'
} as unknown as ExportedSubgraph

View File

@@ -23,8 +23,11 @@ type WritableComboWidget = Omit<IComboWidget, 'value'> & { value: ExposedValue }
const isImageFile = (file: File) => file.type.startsWith('image/')
const isVideoFile = (file: File) => file.type.startsWith('video/')
const findFileComboWidget = (node: LGraphNode, inputName: string) =>
node.widgets!.find((w) => w.name === inputName) as IComboWidget
const findFileComboWidget = (
node: LGraphNode,
inputName: string
): IComboWidget | undefined =>
node.widgets?.find((w): w is IComboWidget => w.name === inputName)
export const useImageUploadWidget = () => {
const widgetConstructor: ComfyWidgetConstructor = (
@@ -50,6 +53,9 @@ export const useImageUploadWidget = () => {
const fileFilter = isVideo ? isVideoFile : isImageFile
const fileComboWidget = findFileComboWidget(node, imageInputName)
if (!fileComboWidget) {
throw new Error(`Widget "${imageInputName}" not found on node`)
}
const initialFile = `${fileComboWidget.value}`
const formatPath = (value: InternalFile) =>
createAnnotatedPath(value, { rootFolder: image_folder })

View File

@@ -190,7 +190,7 @@ function findBox(
start: number,
end: number,
type: string
): IsobmffBoxContentRange | null {
): IsobmffBoxContentRange {
let offset = start
while (offset < end) {
if (offset + 8 > end) break

View File

@@ -39,6 +39,7 @@ export function getFromFlacFile(file: File): Promise<Record<string, string>> {
if (event.target?.result instanceof ArrayBuffer) {
r(getFromFlacBuffer(event.target.result))
} else {
console.error('FileReader returned a non-ArrayBuffer result')
r({})
}
}

View File

@@ -28,15 +28,15 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
contentElement = $el('span')
popup: ComfyPopup | null = null
element: HTMLElement
overIcon!: string
iconSize!: number
content!: string | HTMLElement
icon!: string
tooltip!: string
classList!: ClassList
hidden!: boolean
enabled!: boolean
action!: (e: Event, btn: ComfyButton) => void
overIcon: string | undefined
iconSize: number | undefined
content: string | HTMLElement | undefined
icon: string | undefined
tooltip: string | undefined
classList: ClassList | undefined
hidden: boolean | undefined
enabled: boolean | undefined
action: ((e: Event, btn: ComfyButton) => void) | undefined
constructor({
icon,
@@ -74,13 +74,13 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
'icon',
icon,
toggleElement(this.iconElement, { onShow: this.updateIcon })
)!
)
this.overIcon = prop(this, 'overIcon', overIcon, () => {
if (this.isOver) {
this.updateIcon()
}
})!
this.iconSize = prop(this, 'iconSize', iconSize, this.updateIcon)!
})
this.iconSize = prop(this, 'iconSize', iconSize, this.updateIcon)
this.content = prop(
this,
'content',
@@ -94,7 +94,7 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
}
}
})
)!
)
this.tooltip = prop(this, 'tooltip', tooltip, (v) => {
if (v) {
@@ -102,17 +102,17 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
} else {
this.element.removeAttribute('title')
}
})!
})
if (tooltip !== undefined) {
this.element.setAttribute('aria-label', tooltip)
}
this.classList = prop(this, 'classList', classList, this.updateClasses)!
this.hidden = prop(this, 'hidden', false, this.updateClasses)!
this.classList = prop(this, 'classList', classList, this.updateClasses)
this.hidden = prop(this, 'hidden', false, this.updateClasses)
this.enabled = prop(this, 'enabled', enabled, () => {
this.updateClasses()
;(this.element as HTMLButtonElement).disabled = !this.enabled
})!
this.action = prop(this, 'action', action)!
})
this.action = prop(this, 'action', action)
this.element.addEventListener('click', (e) => {
if (this.popup) {
// we are either a touch device or triggered by click not hover
@@ -154,7 +154,9 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
internalClasses.push('popup-closed')
}
}
applyClasses(this.element, this.classList, ...internalClasses)
if (this.classList !== undefined) {
applyClasses(this.element, this.classList, ...internalClasses)
}
}
withPopup(popup: ComfyPopup, mode: 'click' | 'hover' = 'click') {

View File

@@ -114,6 +114,17 @@ export class DraggableList extends EventTarget {
return () => source.removeEventListener(event, boundListener)
}
getPointerCoordinates(
e: MouseEvent | TouchEvent
): { clientX: number; clientY: number } | null {
if ('clientX' in e) {
return { clientX: e.clientX, clientY: e.clientY }
}
const touch = e.touches?.[0] ?? e.changedTouches?.[0]
if (!touch) return null
return { clientX: touch.clientX, clientY: touch.clientY }
}
dragStart(e: MouseEvent | TouchEvent) {
const target = e.target
if (!(target instanceof HTMLElement)) return
@@ -123,8 +134,10 @@ export class DraggableList extends EventTarget {
if (!this.draggableItem) return
const clientX = 'clientX' in e ? e.clientX : e.touches[0].clientX
const clientY = 'clientY' in e ? e.clientY : e.touches[0].clientY
const coords = this.getPointerCoordinates(e)
if (!coords) return
const { clientX, clientY } = coords
this.pointerStartX = clientX
this.pointerStartY = clientY
@@ -184,10 +197,12 @@ export class DraggableList extends EventTarget {
drag(e: MouseEvent | TouchEvent) {
if (!this.draggableItem) return
const coords = this.getPointerCoordinates(e)
if (!coords) return
e.preventDefault()
const clientX = 'clientX' in e ? e.clientX : e.touches[0].clientX
const clientY = 'clientY' in e ? e.clientY : e.touches[0].clientY
const { clientX, clientY } = coords
const listRect = this.listContainer.getBoundingClientRect()

View File

@@ -1,6 +1,6 @@
import { $el } from '../ui'
interface ToggleSwitchItem {
export interface ToggleSwitchItem {
text: string
value?: string
tooltip?: string
@@ -19,6 +19,13 @@ export function toggleSwitch(
) {
const onChange = e?.onChange
const normalizedItems: ToggleSwitchItem[] = items.map((item) => {
if (typeof item === 'string') {
return { text: item, value: item }
}
return { ...item, value: item.value ?? item.text }
})
let selectedIndex: number | null = null
let elements: HTMLLabelElement[]
@@ -27,20 +34,14 @@ export function toggleSwitch(
elements[selectedIndex].classList.remove('comfy-toggle-selected')
}
onChange?.({
item: items[index] as ToggleSwitchItem,
prev:
selectedIndex == null
? undefined
: (items[selectedIndex] as ToggleSwitchItem)
item: normalizedItems[index],
prev: selectedIndex == null ? undefined : normalizedItems[selectedIndex]
})
selectedIndex = index
elements[selectedIndex].classList.add('comfy-toggle-selected')
}
elements = items.map((item, i) => {
if (typeof item === 'string') item = { text: item }
if (!item.value) item.value = item.text
elements = normalizedItems.map((item, i) => {
const toggle = $el(
'label',
{

View File

@@ -7,24 +7,6 @@ import type {
IStringWidget
} from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
type ComboValuesType = IComboWidget['options']['values']
/**
* Normalizes combo widget values to an array.
* Handles the case where values may be a dictionary (Record<string, string>)
* or a legacy function type.
*/
function getComboValuesArray(
values: ComboValuesType | undefined,
widget?: IComboWidget,
node?: LGraphNode
): string[] {
if (!values) return []
if (typeof values === 'function') return values(widget, node)
if (Array.isArray(values)) return values
return Object.keys(values)
}
import { dynamicWidgets } from '@/core/graph/widgets/dynamicWidgets'
import { useBooleanWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBooleanWidget'
import { useChartWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChartWidget'
@@ -46,6 +28,24 @@ import type { ComfyApp } from './app'
import './domWidget'
import './errorNodeWidgets'
type ComboValuesType = IComboWidget['options']['values']
/**
* Normalizes combo widget values to an array.
* Handles the case where values may be a dictionary (Record<string, string>)
* or a legacy function type.
*/
function getComboValuesArray(
values: ComboValuesType | undefined,
widget?: IComboWidget,
node?: LGraphNode
): string[] {
if (!values) return []
if (typeof values === 'function') return values(widget, node)
if (Array.isArray(values)) return values
return Object.keys(values)
}
export type ComfyWidgetConstructorV2 = (
node: LGraphNode,
inputSpec: InputSpecV2

View File

@@ -118,6 +118,11 @@ declare module '@/lib/litegraph/src/litegraph' {
* This method is a no-op stub for backward compatibility with extensions.
*/
convertWidgetToInput?(): boolean
/**
* Recreates this node, typically used to refresh node state after definition changes.
* Callers should await the result and handle the null case (recreation failed or was cancelled).
* @returns A promise resolving to the new node instance, or null if recreation failed.
*/
recreate?(): Promise<LGraphNode | null>
refreshComboInNode?(defs: Record<string, ComfyNodeDef>)
/** @deprecated groupNode */

View File

@@ -92,7 +92,7 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
try {
graph.beforeChange()
const deserialised = JSON.parse(data)
const deserialised = JSON.parse(data) as VintageSerialisable
// Find the top left point of the boundary of all pasted nodes
const topLeft = [Infinity, Infinity]
@@ -128,7 +128,8 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
const relativeId = info[0]
const outNode = relativeId != null ? nodes[relativeId] : undefined
const inNode = nodes[info[2]]
const inNodeId = info[2]
const inNode = inNodeId != null ? nodes[inNodeId] : undefined
if (outNode && inNode) outNode.connect(info[1], inNode, info[3])
else console.warn('Warning, nodes missing on pasting')
}

View File

@@ -9,22 +9,17 @@ import { useMissingNodes } from '@/workbench/extensions/manager/composables/node
import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type PartialNodeDefStore = Pick<
ReturnType<typeof useNodeDefStore>,
'nodeDefsByName'
>
type NodeDefStore = ReturnType<typeof useNodeDefStore>
type PartialManagerStore = Pick<
ReturnType<typeof useComfyManagerStore>,
'isPackInstalled'
>
function createMockNodeDefStore(
names: string[]
): ReturnType<typeof useNodeDefStore> {
const nodeDefsByName = Object.fromEntries(
function createMockNodeDefStore(names: string[]): NodeDefStore {
const nodeDefsByName: Record<string, ComfyNodeDefImpl> = Object.fromEntries(
names.map((name) => [name, { name } as ComfyNodeDefImpl])
)
return { nodeDefsByName } as ReturnType<typeof useNodeDefStore>
return { nodeDefsByName } as unknown as NodeDefStore
}
vi.mock('@vueuse/core', async () => {
@@ -117,12 +112,7 @@ describe('useMissingNodes', () => {
})
// Reset node def store mock
const partialNodeDefStore: PartialNodeDefStore = {
nodeDefsByName: {}
}
mockUseNodeDefStore.mockReturnValue(
partialNodeDefStore as ReturnType<typeof useNodeDefStore>
)
mockUseNodeDefStore.mockReturnValue(createMockNodeDefStore([]))
// Reset app.rootGraph.nodes
mockApp.rootGraph = { nodes: [] }

View File

@@ -2,7 +2,7 @@ import type { TWidgetValue } from '@/lib/litegraph/src/litegraph'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
interface HasInputOrder {
input_order?: Record<string, string[]>
input_order?: { required?: string[]; optional?: string[] }
}
/**