Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions src/composables/graph/contextMenuConverter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { describe, it, expect } from 'vitest'

import type { MenuOption } from './useMoreOptionsMenu'
import {
buildStructuredMenu,
convertContextMenuToOptions
} from './contextMenuConverter'

describe('contextMenuConverter', () => {
describe('buildStructuredMenu', () => {
it('should order core items before extension items', () => {
const options: MenuOption[] = [
{ label: 'Custom Extension Item', source: 'litegraph' },
{ label: 'Copy', source: 'vue' },
{ label: 'Rename', source: 'vue' }
]

const result = buildStructuredMenu(options)

// Core items (Rename, Copy) should come before extension items
const renameIndex = result.findIndex((opt) => opt.label === 'Rename')
const copyIndex = result.findIndex((opt) => opt.label === 'Copy')
const extensionIndex = result.findIndex(
(opt) => opt.label === 'Custom Extension Item'
)

expect(renameIndex).toBeLessThan(extensionIndex)
expect(copyIndex).toBeLessThan(extensionIndex)
})

it('should add Extensions category label before extension items', () => {
const options: MenuOption[] = [
{ label: 'Copy', source: 'vue' },
{ label: 'My Custom Extension', source: 'litegraph' }
]

const result = buildStructuredMenu(options)

const extensionsLabel = result.find(
(opt) => opt.label === 'Extensions' && opt.type === 'category'
)
expect(extensionsLabel).toBeDefined()
expect(extensionsLabel?.disabled).toBe(true)
})

it('should place Delete at the very end', () => {
const options: MenuOption[] = [
{ label: 'Delete', action: () => {}, source: 'vue' },
{ label: 'Copy', source: 'vue' },
{ label: 'Rename', source: 'vue' }
]

const result = buildStructuredMenu(options)

const lastNonDivider = [...result]
.reverse()
.find((opt) => opt.type !== 'divider')
expect(lastNonDivider?.label).toBe('Delete')
})

it('should deduplicate items with same label, preferring vue source', () => {
const options: MenuOption[] = [
{ label: 'Copy', action: () => {}, source: 'litegraph' },
{ label: 'Copy', action: () => {}, source: 'vue' }
]

const result = buildStructuredMenu(options)

const copyItems = result.filter((opt) => opt.label === 'Copy')
expect(copyItems).toHaveLength(1)
expect(copyItems[0].source).toBe('vue')
})

it('should preserve dividers between sections', () => {
const options: MenuOption[] = [
{ label: 'Rename', source: 'vue' },
{ label: 'Copy', source: 'vue' },
{ label: 'Pin', source: 'vue' }
]

const result = buildStructuredMenu(options)

const dividers = result.filter((opt) => opt.type === 'divider')
expect(dividers.length).toBeGreaterThan(0)
})

it('should handle empty input', () => {
const result = buildStructuredMenu([])
expect(result).toEqual([])
})

it('should handle only dividers', () => {
const options: MenuOption[] = [{ type: 'divider' }, { type: 'divider' }]

const result = buildStructuredMenu(options)

// Should be empty since dividers are filtered initially
expect(result).toEqual([])
})

it('should recognize Remove as equivalent to Delete', () => {
const options: MenuOption[] = [
{ label: 'Remove', action: () => {}, source: 'vue' },
{ label: 'Copy', source: 'vue' }
]

const result = buildStructuredMenu(options)

// Remove should be placed at the end like Delete
const lastNonDivider = [...result]
.reverse()
.find((opt) => opt.type !== 'divider')
expect(lastNonDivider?.label).toBe('Remove')
})

it('should group core items in correct section order', () => {
const options: MenuOption[] = [
{ label: 'Color', source: 'vue' },
{ label: 'Node Info', source: 'vue' },
{ label: 'Pin', source: 'vue' },
{ label: 'Rename', source: 'vue' }
]

const result = buildStructuredMenu(options)

// Get indices of items (excluding dividers and categories)
const getIndex = (label: string) =>
result.findIndex((opt) => opt.label === label)

// Rename (section 1) should come before Pin (section 2)
expect(getIndex('Rename')).toBeLessThan(getIndex('Pin'))
// Pin (section 2) should come before Node Info (section 4)
expect(getIndex('Pin')).toBeLessThan(getIndex('Node Info'))
// Node Info (section 4) should come before or with Color (section 4)
expect(getIndex('Node Info')).toBeLessThanOrEqual(getIndex('Color'))
})
})

describe('convertContextMenuToOptions', () => {
it('should convert empty array to empty result', () => {
const result = convertContextMenuToOptions([])
expect(result).toEqual([])
})

it('should convert null items to dividers', () => {
const result = convertContextMenuToOptions([null], undefined, false)
expect(result).toHaveLength(1)
expect(result[0].type).toBe('divider')
})

it('should skip blacklisted items like Properties', () => {
const items = [{ content: 'Properties', callback: () => {} }]
const result = convertContextMenuToOptions(items, undefined, false)
expect(result.find((opt) => opt.label === 'Properties')).toBeUndefined()
})

it('should convert basic menu items with content', () => {
const items = [{ content: 'Test Item', callback: () => {} }]
const result = convertContextMenuToOptions(items, undefined, false)
expect(result).toHaveLength(1)
expect(result[0].label).toBe('Test Item')
})

it('should mark items as litegraph source', () => {
const items = [{ content: 'Test Item', callback: () => {} }]
const result = convertContextMenuToOptions(items, undefined, false)
expect(result[0].source).toBe('litegraph')
})

it('should pass through disabled state', () => {
const items = [{ content: 'Disabled Item', disabled: true }]
const result = convertContextMenuToOptions(items, undefined, false)
expect(result[0].disabled).toBe(true)
})

it('should apply structuring by default', () => {
const items = [
{ content: 'Copy', callback: () => {} },
{ content: 'Custom Extension', callback: () => {} }
]
const result = convertContextMenuToOptions(items)

// With structuring, there should be Extensions category
const hasExtensionsCategory = result.some(
(opt) => opt.label === 'Extensions' && opt.type === 'category'
)
expect(hasExtensionsCategory).toBe(true)
})
})
})
Loading
Loading