diff --git a/src/composables/graph/contextMenuConverter.test.ts b/src/composables/graph/contextMenuConverter.test.ts index cc157f220a..c2bf5de6e6 100644 --- a/src/composables/graph/contextMenuConverter.test.ts +++ b/src/composables/graph/contextMenuConverter.test.ts @@ -134,6 +134,27 @@ describe('contextMenuConverter', () => { // Node Info (section 4) should come before or with Color (section 4) expect(getIndex('Node Info')).toBeLessThanOrEqual(getIndex('Color')) }) + + it('should recognize Frame Nodes as a core menu item', () => { + const options: MenuOption[] = [ + { label: 'Rename', source: 'vue' }, + { label: 'Frame Nodes', source: 'vue' }, + { label: 'Custom Extension', source: 'vue' } + ] + + const result = buildStructuredMenu(options) + + // Frame Nodes should appear in the core items section (before Extensions) + const frameNodesIndex = result.findIndex( + (opt) => opt.label === 'Frame Nodes' + ) + const extensionsCategoryIndex = result.findIndex( + (opt) => opt.label === 'Extensions' && opt.type === 'category' + ) + + // Frame Nodes should come before Extensions category + expect(frameNodesIndex).toBeLessThan(extensionsCategoryIndex) + }) }) describe('convertContextMenuToOptions', () => { diff --git a/src/composables/graph/contextMenuConverter.ts b/src/composables/graph/contextMenuConverter.ts index 3395e18be8..905f2934d7 100644 --- a/src/composables/graph/contextMenuConverter.ts +++ b/src/composables/graph/contextMenuConverter.ts @@ -44,6 +44,7 @@ const CORE_MENU_ITEMS = new Set([ // Structure operations 'Convert to Subgraph', 'Frame selection', + 'Frame Nodes', 'Minimize Node', 'Expand', 'Collapse', @@ -103,7 +104,8 @@ function isDuplicateItem(label: string, existingItems: MenuOption[]): boolean { shape: ['shape', 'shapes'], pin: ['pin', 'unpin'], delete: ['remove', 'delete'], - duplicate: ['clone', 'duplicate'] + duplicate: ['clone', 'duplicate'], + frame: ['frame selection', 'frame nodes'] } return existingItems.some((item) => { @@ -226,6 +228,7 @@ const MENU_ORDER: string[] = [ // Section 3: Structure operations 'Convert to Subgraph', 'Frame selection', + 'Frame Nodes', 'Minimize Node', 'Expand', 'Collapse', diff --git a/src/composables/graph/useSelectionMenuOptions.test.ts b/src/composables/graph/useSelectionMenuOptions.test.ts index 8600e85eec..8d1870cebe 100644 --- a/src/composables/graph/useSelectionMenuOptions.test.ts +++ b/src/composables/graph/useSelectionMenuOptions.test.ts @@ -2,10 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { useSelectionMenuOptions } from '@/composables/graph/useSelectionMenuOptions' -const subgraphMocks = vi.hoisted(() => ({ +const mocks = vi.hoisted(() => ({ convertToSubgraph: vi.fn(), unpackSubgraph: vi.fn(), addSubgraphToLibrary: vi.fn(), + frameNodes: vi.fn(), createI18nMock: vi.fn(() => ({ global: { t: vi.fn(), @@ -19,7 +20,7 @@ vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }), - createI18n: subgraphMocks.createI18nMock + createI18n: mocks.createI18nMock })) vi.mock('@/composables/graph/useSelectionOperations', () => ({ @@ -42,18 +43,46 @@ vi.mock('@/composables/graph/useNodeArrangement', () => ({ vi.mock('@/composables/graph/useSubgraphOperations', () => ({ useSubgraphOperations: () => ({ - convertToSubgraph: subgraphMocks.convertToSubgraph, - unpackSubgraph: subgraphMocks.unpackSubgraph, - addSubgraphToLibrary: subgraphMocks.addSubgraphToLibrary + convertToSubgraph: mocks.convertToSubgraph, + unpackSubgraph: mocks.unpackSubgraph, + addSubgraphToLibrary: mocks.addSubgraphToLibrary }) })) vi.mock('@/composables/graph/useFrameNodes', () => ({ useFrameNodes: () => ({ - frameNodes: vi.fn() + frameNodes: mocks.frameNodes }) })) +describe('useSelectionMenuOptions - multiple nodes options', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns Frame Nodes option that invokes frameNodes when called', () => { + const { getMultipleNodesOptions } = useSelectionMenuOptions() + const options = getMultipleNodesOptions() + + const frameOption = options.find((opt) => opt.label === 'g.frameNodes') + expect(frameOption).toBeDefined() + expect(frameOption?.action).toBeDefined() + + frameOption?.action?.() + expect(mocks.frameNodes).toHaveBeenCalledOnce() + }) + + it('returns Convert to Group Node option from getMultipleNodesOptions', () => { + const { getMultipleNodesOptions } = useSelectionMenuOptions() + const options = getMultipleNodesOptions() + + const groupNodeOption = options.find( + (opt) => opt.label === 'contextMenu.Convert to Group Node' + ) + expect(groupNodeOption).toBeDefined() + }) +}) + describe('useSelectionMenuOptions - subgraph options', () => { beforeEach(() => { vi.clearAllMocks() @@ -68,7 +97,7 @@ describe('useSelectionMenuOptions - subgraph options', () => { expect(options).toHaveLength(1) expect(options[0]?.label).toBe('contextMenu.Convert to Subgraph') - expect(options[0]?.action).toBe(subgraphMocks.convertToSubgraph) + expect(options[0]?.action).toBe(mocks.convertToSubgraph) }) it('includes convert, add to library, and unpack when subgraphs are selected', () => { @@ -86,7 +115,7 @@ describe('useSelectionMenuOptions - subgraph options', () => { const convertOption = options.find( (option) => option.label === 'contextMenu.Convert to Subgraph' ) - expect(convertOption?.action).toBe(subgraphMocks.convertToSubgraph) + expect(convertOption?.action).toBe(mocks.convertToSubgraph) }) it('hides convert option when only a single subgraph is selected', () => { diff --git a/src/composables/graph/useSelectionState.test.ts b/src/composables/graph/useSelectionState.test.ts index cd4af9422d..278365435e 100644 --- a/src/composables/graph/useSelectionState.test.ts +++ b/src/composables/graph/useSelectionState.test.ts @@ -87,6 +87,25 @@ describe('useSelectionState', () => { const { hasAnySelection } = useSelectionState() expect(hasAnySelection.value).toBe(true) }) + + test('hasMultipleSelection should be true when 2+ items selected', () => { + const canvasStore = useCanvasStore() + const node1 = createMockLGraphNode({ id: 1 }) + const node2 = createMockLGraphNode({ id: 2 }) + canvasStore.$state.selectedItems = [node1, node2] + + const { hasMultipleSelection } = useSelectionState() + expect(hasMultipleSelection.value).toBe(true) + }) + + test('hasMultipleSelection should be false when only 1 item selected', () => { + const canvasStore = useCanvasStore() + const node1 = createMockLGraphNode({ id: 1 }) + canvasStore.$state.selectedItems = [node1] + + const { hasMultipleSelection } = useSelectionState() + expect(hasMultipleSelection.value).toBe(false) + }) }) describe('Node Type Filtering', () => {