mirror of
https://github.com/langgenius/dify.git
synced 2026-03-29 01:49:57 +08:00
Merge remote-tracking branch 'origin/main' into feat/trigger
This commit is contained in:
235
web/__tests__/goto-anything/match-action.test.ts
Normal file
235
web/__tests__/goto-anything/match-action.test.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
|
||||
|
||||
// Mock the entire actions module to avoid import issues
|
||||
jest.mock('../../app/components/goto-anything/actions', () => ({
|
||||
matchAction: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('../../app/components/goto-anything/actions/commands/registry')
|
||||
|
||||
// Import after mocking to get mocked version
|
||||
import { matchAction } from '../../app/components/goto-anything/actions'
|
||||
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
|
||||
|
||||
// Implement the actual matchAction logic for testing
|
||||
const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => {
|
||||
const result = Object.values(actions).find((action) => {
|
||||
// Special handling for slash commands
|
||||
if (action.key === '/') {
|
||||
// Get all registered commands from the registry
|
||||
const allCommands = slashCommandRegistry.getAllCommands()
|
||||
|
||||
// Check if query matches any registered command
|
||||
return allCommands.some((cmd) => {
|
||||
const cmdPattern = `/${cmd.name}`
|
||||
|
||||
// For direct mode commands, don't match (keep in command selector)
|
||||
if (cmd.mode === 'direct')
|
||||
return false
|
||||
|
||||
// For submenu mode commands, match when complete command is entered
|
||||
return query === cmdPattern || query.startsWith(`${cmdPattern} `)
|
||||
})
|
||||
}
|
||||
|
||||
const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
|
||||
return reg.test(query)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Replace mock with actual implementation
|
||||
;(matchAction as jest.Mock).mockImplementation(actualMatchAction)
|
||||
|
||||
describe('matchAction Logic', () => {
|
||||
const mockActions: Record<string, ActionItem> = {
|
||||
app: {
|
||||
key: '@app',
|
||||
shortcut: '@a',
|
||||
title: 'Search Applications',
|
||||
description: 'Search apps',
|
||||
search: jest.fn(),
|
||||
},
|
||||
knowledge: {
|
||||
key: '@knowledge',
|
||||
shortcut: '@kb',
|
||||
title: 'Search Knowledge',
|
||||
description: 'Search knowledge bases',
|
||||
search: jest.fn(),
|
||||
},
|
||||
slash: {
|
||||
key: '/',
|
||||
shortcut: '/',
|
||||
title: 'Commands',
|
||||
description: 'Execute commands',
|
||||
search: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
{ name: 'docs', mode: 'direct' },
|
||||
{ name: 'community', mode: 'direct' },
|
||||
{ name: 'feedback', mode: 'direct' },
|
||||
{ name: 'account', mode: 'direct' },
|
||||
{ name: 'theme', mode: 'submenu' },
|
||||
{ name: 'language', mode: 'submenu' },
|
||||
])
|
||||
})
|
||||
|
||||
describe('@ Actions Matching', () => {
|
||||
it('should match @app with key', () => {
|
||||
const result = matchAction('@app', mockActions)
|
||||
expect(result).toBe(mockActions.app)
|
||||
})
|
||||
|
||||
it('should match @app with shortcut', () => {
|
||||
const result = matchAction('@a', mockActions)
|
||||
expect(result).toBe(mockActions.app)
|
||||
})
|
||||
|
||||
it('should match @knowledge with key', () => {
|
||||
const result = matchAction('@knowledge', mockActions)
|
||||
expect(result).toBe(mockActions.knowledge)
|
||||
})
|
||||
|
||||
it('should match @knowledge with shortcut @kb', () => {
|
||||
const result = matchAction('@kb', mockActions)
|
||||
expect(result).toBe(mockActions.knowledge)
|
||||
})
|
||||
|
||||
it('should match with text after action', () => {
|
||||
const result = matchAction('@app search term', mockActions)
|
||||
expect(result).toBe(mockActions.app)
|
||||
})
|
||||
|
||||
it('should not match partial @ actions', () => {
|
||||
const result = matchAction('@ap', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Slash Commands Matching', () => {
|
||||
describe('Direct Mode Commands', () => {
|
||||
it('should not match direct mode commands', () => {
|
||||
const result = matchAction('/docs', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not match direct mode with arguments', () => {
|
||||
const result = matchAction('/docs something', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not match any direct mode command', () => {
|
||||
expect(matchAction('/community', mockActions)).toBeUndefined()
|
||||
expect(matchAction('/feedback', mockActions)).toBeUndefined()
|
||||
expect(matchAction('/account', mockActions)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Submenu Mode Commands', () => {
|
||||
it('should match submenu mode commands exactly', () => {
|
||||
const result = matchAction('/theme', mockActions)
|
||||
expect(result).toBe(mockActions.slash)
|
||||
})
|
||||
|
||||
it('should match submenu mode with arguments', () => {
|
||||
const result = matchAction('/theme dark', mockActions)
|
||||
expect(result).toBe(mockActions.slash)
|
||||
})
|
||||
|
||||
it('should match all submenu commands', () => {
|
||||
expect(matchAction('/language', mockActions)).toBe(mockActions.slash)
|
||||
expect(matchAction('/language en', mockActions)).toBe(mockActions.slash)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Slash Without Command', () => {
|
||||
it('should not match single slash', () => {
|
||||
const result = matchAction('/', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not match unregistered commands', () => {
|
||||
const result = matchAction('/unknown', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty query', () => {
|
||||
const result = matchAction('', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle whitespace only', () => {
|
||||
const result = matchAction(' ', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle regular text without actions', () => {
|
||||
const result = matchAction('search something', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle special characters', () => {
|
||||
const result = matchAction('#tag', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle multiple @ or /', () => {
|
||||
expect(matchAction('@@app', mockActions)).toBeUndefined()
|
||||
expect(matchAction('//theme', mockActions)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mode-based Filtering', () => {
|
||||
it('should filter direct mode commands from matching', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
{ name: 'test', mode: 'direct' },
|
||||
])
|
||||
|
||||
const result = matchAction('/test', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow submenu mode commands to match', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
{ name: 'test', mode: 'submenu' },
|
||||
])
|
||||
|
||||
const result = matchAction('/test', mockActions)
|
||||
expect(result).toBe(mockActions.slash)
|
||||
})
|
||||
|
||||
it('should treat undefined mode as submenu', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
{ name: 'test' }, // No mode specified
|
||||
])
|
||||
|
||||
const result = matchAction('/test', mockActions)
|
||||
expect(result).toBe(mockActions.slash)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Registry Integration', () => {
|
||||
it('should call getAllCommands when matching slash', () => {
|
||||
matchAction('/theme', mockActions)
|
||||
expect(slashCommandRegistry.getAllCommands).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call getAllCommands for @ actions', () => {
|
||||
matchAction('@app', mockActions)
|
||||
expect(slashCommandRegistry.getAllCommands).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty command list', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([])
|
||||
const result = matchAction('/anything', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
134
web/__tests__/goto-anything/scope-command-tags.test.tsx
Normal file
134
web/__tests__/goto-anything/scope-command-tags.test.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
// Type alias for search mode
|
||||
type SearchMode = 'scopes' | 'commands' | null
|
||||
|
||||
// Mock component to test tag display logic
|
||||
const TagDisplay: React.FC<{ searchMode: SearchMode }> = ({ searchMode }) => {
|
||||
if (!searchMode) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs text-text-tertiary">
|
||||
<span>{searchMode === 'scopes' ? 'SCOPES' : 'COMMANDS'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Scope and Command Tags', () => {
|
||||
describe('Tag Display Logic', () => {
|
||||
it('should display SCOPES for @ actions', () => {
|
||||
render(<TagDisplay searchMode="scopes" />)
|
||||
expect(screen.getByText('SCOPES')).toBeInTheDocument()
|
||||
expect(screen.queryByText('COMMANDS')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display COMMANDS for / actions', () => {
|
||||
render(<TagDisplay searchMode="commands" />)
|
||||
expect(screen.getByText('COMMANDS')).toBeInTheDocument()
|
||||
expect(screen.queryByText('SCOPES')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not display any tag when searchMode is null', () => {
|
||||
const { container } = render(<TagDisplay searchMode={null} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search Mode Detection', () => {
|
||||
const getSearchMode = (query: string): SearchMode => {
|
||||
if (query.startsWith('@')) return 'scopes'
|
||||
if (query.startsWith('/')) return 'commands'
|
||||
return null
|
||||
}
|
||||
|
||||
it('should detect scopes mode for @ queries', () => {
|
||||
expect(getSearchMode('@app')).toBe('scopes')
|
||||
expect(getSearchMode('@knowledge')).toBe('scopes')
|
||||
expect(getSearchMode('@plugin')).toBe('scopes')
|
||||
expect(getSearchMode('@node')).toBe('scopes')
|
||||
})
|
||||
|
||||
it('should detect commands mode for / queries', () => {
|
||||
expect(getSearchMode('/theme')).toBe('commands')
|
||||
expect(getSearchMode('/language')).toBe('commands')
|
||||
expect(getSearchMode('/docs')).toBe('commands')
|
||||
})
|
||||
|
||||
it('should return null for regular queries', () => {
|
||||
expect(getSearchMode('')).toBe(null)
|
||||
expect(getSearchMode('search term')).toBe(null)
|
||||
expect(getSearchMode('app')).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle queries with spaces', () => {
|
||||
expect(getSearchMode('@app search')).toBe('scopes')
|
||||
expect(getSearchMode('/theme dark')).toBe('commands')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tag Styling', () => {
|
||||
it('should apply correct styling classes', () => {
|
||||
const { container } = render(<TagDisplay searchMode="scopes" />)
|
||||
const tagContainer = container.querySelector('.flex.items-center.gap-1.text-xs.text-text-tertiary')
|
||||
expect(tagContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use hardcoded English text', () => {
|
||||
// Verify that tags are hardcoded and not using i18n
|
||||
render(<TagDisplay searchMode="scopes" />)
|
||||
const scopesText = screen.getByText('SCOPES')
|
||||
expect(scopesText.textContent).toBe('SCOPES')
|
||||
|
||||
render(<TagDisplay searchMode="commands" />)
|
||||
const commandsText = screen.getByText('COMMANDS')
|
||||
expect(commandsText.textContent).toBe('COMMANDS')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with Search States', () => {
|
||||
const SearchComponent: React.FC<{ query: string }> = ({ query }) => {
|
||||
let searchMode: SearchMode = null
|
||||
|
||||
if (query.startsWith('@')) searchMode = 'scopes'
|
||||
else if (query.startsWith('/')) searchMode = 'commands'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={query} readOnly />
|
||||
<TagDisplay searchMode={searchMode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
it('should update tag when switching between @ and /', () => {
|
||||
const { rerender } = render(<SearchComponent query="@app" />)
|
||||
expect(screen.getByText('SCOPES')).toBeInTheDocument()
|
||||
|
||||
rerender(<SearchComponent query="/theme" />)
|
||||
expect(screen.queryByText('SCOPES')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('COMMANDS')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide tag when clearing search', () => {
|
||||
const { rerender } = render(<SearchComponent query="@app" />)
|
||||
expect(screen.getByText('SCOPES')).toBeInTheDocument()
|
||||
|
||||
rerender(<SearchComponent query="" />)
|
||||
expect(screen.queryByText('SCOPES')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('COMMANDS')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain correct tag during search refinement', () => {
|
||||
const { rerender } = render(<SearchComponent query="@" />)
|
||||
expect(screen.getByText('SCOPES')).toBeInTheDocument()
|
||||
|
||||
rerender(<SearchComponent query="@app" />)
|
||||
expect(screen.getByText('SCOPES')).toBeInTheDocument()
|
||||
|
||||
rerender(<SearchComponent query="@app test" />)
|
||||
expect(screen.getByText('SCOPES')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
212
web/__tests__/goto-anything/slash-command-modes.test.tsx
Normal file
212
web/__tests__/goto-anything/slash-command-modes.test.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import '@testing-library/jest-dom'
|
||||
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
|
||||
import type { SlashCommandHandler } from '../../app/components/goto-anything/actions/commands/types'
|
||||
|
||||
// Mock the registry
|
||||
jest.mock('../../app/components/goto-anything/actions/commands/registry')
|
||||
|
||||
describe('Slash Command Dual-Mode System', () => {
|
||||
const mockDirectCommand: SlashCommandHandler = {
|
||||
name: 'docs',
|
||||
description: 'Open documentation',
|
||||
mode: 'direct',
|
||||
execute: jest.fn(),
|
||||
search: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'docs',
|
||||
title: 'Documentation',
|
||||
description: 'Open documentation',
|
||||
type: 'command' as const,
|
||||
data: { command: 'navigation.docs', args: {} },
|
||||
},
|
||||
]),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
}
|
||||
|
||||
const mockSubmenuCommand: SlashCommandHandler = {
|
||||
name: 'theme',
|
||||
description: 'Change theme',
|
||||
mode: 'submenu',
|
||||
search: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'theme-light',
|
||||
title: 'Light Theme',
|
||||
description: 'Switch to light theme',
|
||||
type: 'command' as const,
|
||||
data: { command: 'theme.set', args: { theme: 'light' } },
|
||||
},
|
||||
{
|
||||
id: 'theme-dark',
|
||||
title: 'Dark Theme',
|
||||
description: 'Switch to dark theme',
|
||||
type: 'command' as const,
|
||||
data: { command: 'theme.set', args: { theme: 'dark' } },
|
||||
},
|
||||
]),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(slashCommandRegistry as any).findCommand = jest.fn((name: string) => {
|
||||
if (name === 'docs') return mockDirectCommand
|
||||
if (name === 'theme') return mockSubmenuCommand
|
||||
return null
|
||||
})
|
||||
;(slashCommandRegistry as any).getAllCommands = jest.fn(() => [
|
||||
mockDirectCommand,
|
||||
mockSubmenuCommand,
|
||||
])
|
||||
})
|
||||
|
||||
describe('Direct Mode Commands', () => {
|
||||
it('should execute immediately when selected', () => {
|
||||
const mockSetShow = jest.fn()
|
||||
const mockSetSearchQuery = jest.fn()
|
||||
|
||||
// Simulate command selection
|
||||
const handler = slashCommandRegistry.findCommand('docs')
|
||||
expect(handler?.mode).toBe('direct')
|
||||
|
||||
if (handler?.mode === 'direct' && handler.execute) {
|
||||
handler.execute()
|
||||
mockSetShow(false)
|
||||
mockSetSearchQuery('')
|
||||
}
|
||||
|
||||
expect(mockDirectCommand.execute).toHaveBeenCalled()
|
||||
expect(mockSetShow).toHaveBeenCalledWith(false)
|
||||
expect(mockSetSearchQuery).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should not enter submenu for direct mode commands', () => {
|
||||
const handler = slashCommandRegistry.findCommand('docs')
|
||||
expect(handler?.mode).toBe('direct')
|
||||
expect(handler?.execute).toBeDefined()
|
||||
})
|
||||
|
||||
it('should close modal after execution', () => {
|
||||
const mockModalClose = jest.fn()
|
||||
|
||||
const handler = slashCommandRegistry.findCommand('docs')
|
||||
if (handler?.mode === 'direct' && handler.execute) {
|
||||
handler.execute()
|
||||
mockModalClose()
|
||||
}
|
||||
|
||||
expect(mockModalClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Submenu Mode Commands', () => {
|
||||
it('should show options instead of executing immediately', async () => {
|
||||
const handler = slashCommandRegistry.findCommand('theme')
|
||||
expect(handler?.mode).toBe('submenu')
|
||||
|
||||
const results = await handler?.search('', 'en')
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results?.[0].title).toBe('Light Theme')
|
||||
expect(results?.[1].title).toBe('Dark Theme')
|
||||
})
|
||||
|
||||
it('should not have execute function for submenu mode', () => {
|
||||
const handler = slashCommandRegistry.findCommand('theme')
|
||||
expect(handler?.mode).toBe('submenu')
|
||||
expect(handler?.execute).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should keep modal open for selection', () => {
|
||||
const mockModalClose = jest.fn()
|
||||
|
||||
const handler = slashCommandRegistry.findCommand('theme')
|
||||
// For submenu mode, modal should not close immediately
|
||||
expect(handler?.mode).toBe('submenu')
|
||||
expect(mockModalClose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mode Detection and Routing', () => {
|
||||
it('should correctly identify direct mode commands', () => {
|
||||
const commands = slashCommandRegistry.getAllCommands()
|
||||
const directCommands = commands.filter(cmd => cmd.mode === 'direct')
|
||||
const submenuCommands = commands.filter(cmd => cmd.mode === 'submenu')
|
||||
|
||||
expect(directCommands).toContainEqual(expect.objectContaining({ name: 'docs' }))
|
||||
expect(submenuCommands).toContainEqual(expect.objectContaining({ name: 'theme' }))
|
||||
})
|
||||
|
||||
it('should handle missing mode property gracefully', () => {
|
||||
const commandWithoutMode: SlashCommandHandler = {
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
search: jest.fn(),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
}
|
||||
|
||||
;(slashCommandRegistry as any).findCommand = jest.fn(() => commandWithoutMode)
|
||||
|
||||
const handler = slashCommandRegistry.findCommand('test')
|
||||
// Default behavior should be submenu when mode is not specified
|
||||
expect(handler?.mode).toBeUndefined()
|
||||
expect(handler?.execute).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Enter Key Handling', () => {
|
||||
// Helper function to simulate key handler behavior
|
||||
const createKeyHandler = () => {
|
||||
return (commandKey: string) => {
|
||||
if (commandKey.startsWith('/')) {
|
||||
const commandName = commandKey.substring(1)
|
||||
const handler = slashCommandRegistry.findCommand(commandName)
|
||||
if (handler?.mode === 'direct' && handler.execute) {
|
||||
handler.execute()
|
||||
return true // Indicates handled
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
it('should trigger direct execution on Enter for direct mode', () => {
|
||||
const keyHandler = createKeyHandler()
|
||||
const handled = keyHandler('/docs')
|
||||
expect(handled).toBe(true)
|
||||
expect(mockDirectCommand.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not trigger direct execution for submenu mode', () => {
|
||||
const keyHandler = createKeyHandler()
|
||||
const handled = keyHandler('/theme')
|
||||
expect(handled).toBe(false)
|
||||
expect(mockSubmenuCommand.search).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Command Registration', () => {
|
||||
it('should register both direct and submenu commands', () => {
|
||||
mockDirectCommand.register?.({})
|
||||
mockSubmenuCommand.register?.({ setTheme: jest.fn() })
|
||||
|
||||
expect(mockDirectCommand.register).toHaveBeenCalled()
|
||||
expect(mockSubmenuCommand.register).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle unregistration for both command types', () => {
|
||||
// Test unregister for direct command
|
||||
mockDirectCommand.unregister?.()
|
||||
expect(mockDirectCommand.unregister).toHaveBeenCalled()
|
||||
|
||||
// Test unregister for submenu command
|
||||
mockSubmenuCommand.unregister?.()
|
||||
expect(mockSubmenuCommand.unregister).toHaveBeenCalled()
|
||||
|
||||
// Verify both were called independently
|
||||
expect(mockDirectCommand.unregister).toHaveBeenCalledTimes(1)
|
||||
expect(mockSubmenuCommand.unregister).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user