diff --git a/src/lib/litegraph/src/contextMenuCompat.test.ts b/src/lib/litegraph/src/contextMenuCompat.test.ts index 246551dc9..e8afeca9b 100644 --- a/src/lib/litegraph/src/contextMenuCompat.test.ts +++ b/src/lib/litegraph/src/contextMenuCompat.test.ts @@ -195,6 +195,42 @@ describe('contextMenuCompat', () => { expect.any(Error) ) }) + + it('should handle multiple items with undefined content correctly', () => { + // Setup base method with items that have undefined content + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + return [ + { content: undefined, title: 'Separator 1' }, + { content: undefined, title: 'Separator 2' }, + { content: 'Item 1', callback: () => {} } + ] + } + + legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions') + + // Monkey-patch to add an item with undefined content + const original = LGraphCanvas.prototype.getCanvasMenuOptions + LGraphCanvas.prototype.getCanvasMenuOptions = + function (): (IContextMenuValue | null)[] { + const items = original.apply(this) + items.push({ content: undefined, title: 'Separator 3' }) + return items + } + + // Extract legacy items + const legacyItems = legacyMenuCompat.extractLegacyItems( + 'getCanvasMenuOptions', + mockCanvas + ) + + // Should extract only the newly added item with undefined content + // (not collapse with existing undefined content items) + expect(legacyItems).toHaveLength(1) + expect(legacyItems[0]).toMatchObject({ + content: undefined, + title: 'Separator 3' + }) + }) }) describe('integration', () => { diff --git a/src/lib/litegraph/src/contextMenuCompat.ts b/src/lib/litegraph/src/contextMenuCompat.ts index c1f039cfa..ae48aec70 100644 --- a/src/lib/litegraph/src/contextMenuCompat.ts +++ b/src/lib/litegraph/src/contextMenuCompat.ts @@ -152,19 +152,51 @@ class LegacyMenuCompat { const patchedItems = methodToCall.apply(context, args) as | (IContextMenuValue | null)[] | undefined - if (!patchedItems) return [] + if (!patchedItems) { + return [] + } + // Use content-based diff to detect additions (not reference-based) + // Create composite keys from multiple properties to handle undefined content + const createItemKey = (item: IContextMenuValue): string => { + const parts = [ + item.content ?? '', + item.title ?? '', + item.className ?? '', + item.property ?? '', + item.type ?? '' + ] + return parts.join('|') + } - // Use set-based diff to detect additions by reference - const originalSet = new Set(originalItems) - const addedItems = patchedItems.filter((item) => !originalSet.has(item)) + const originalKeys = new Set( + originalItems + .filter( + (item): item is IContextMenuValue => + item !== null && typeof item === 'object' && 'content' in item + ) + .map(createItemKey) + ) + const addedItems = patchedItems.filter((item) => { + if (item === null) return false + if (typeof item !== 'object' || !('content' in item)) return false + return !originalKeys.has(createItemKey(item)) + }) // Warn if items were removed (patched has fewer original items than expected) - const retainedOriginalCount = patchedItems.filter((item) => - originalSet.has(item) + const patchedKeys = new Set( + patchedItems + .filter( + (item): item is IContextMenuValue => + item !== null && typeof item === 'object' && 'content' in item + ) + .map(createItemKey) + ) + const removedCount = [...originalKeys].filter( + (key) => !patchedKeys.has(key) ).length - if (retainedOriginalCount < originalItems.length) { + if (removedCount > 0) { console.warn( - `[Context Menu Compat] Monkey patch for ${methodName} removed ${originalItems.length - retainedOriginalCount} original menu item(s). ` + + `[Context Menu Compat] Monkey patch for ${methodName} removed ${removedCount} original menu item(s). ` + `This may cause unexpected behavior.` ) }