Files
dify/web/__tests__/goto-anything/slash-command-modes.test.tsx
FFXN 0e320290e1 feat: evaluation (#35353)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: jyong <718720800@qq.com>
Co-authored-by: Yansong Zhang <916125788@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: hj24 <mambahj24@gmail.com>
Co-authored-by: hj24 <huangjian@dify.ai>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Ayush Baluni <73417844+aayushbaluni@users.noreply.github.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: jimcody1995 <jjimcody@gmail.com>
Co-authored-by: James <63717587+jamesrayammons@users.noreply.github.com>
Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: Coding On Star <447357187@qq.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: jerryzai <jerryzh8710@protonmail.com>
Co-authored-by: NVIDIAN <speedy.hpc@hotmail.com>
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Junghwan <70629228+shaun0927@users.noreply.github.com>
Co-authored-by: HeYinKazune <70251095+HeYin-OS@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: Jingyi <jingyi.qi@dify.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: sxxtony <166789813+sxxtony@users.noreply.github.com>
2026-04-17 16:37:21 +08:00

214 lines
7.0 KiB
TypeScript

import type { SlashCommandHandler } from '../../app/components/goto-anything/actions/commands/types'
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
// Mock the registry
vi.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: vi.fn(),
search: vi.fn().mockResolvedValue([
{
id: 'docs',
title: 'Documentation',
description: 'Open documentation',
type: 'command' as const,
data: { command: 'navigation.docs', args: {} },
},
]),
register: vi.fn(),
unregister: vi.fn(),
}
const mockSubmenuCommand: SlashCommandHandler = {
name: 'theme',
description: 'Change theme',
mode: 'submenu',
search: vi.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: vi.fn(),
unregister: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(slashCommandRegistry.findCommand).mockImplementation((name: string) => {
if (name === 'docs')
return mockDirectCommand
if (name === 'theme')
return mockSubmenuCommand
return undefined
})
vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([
mockDirectCommand,
mockSubmenuCommand,
])
})
describe('Direct Mode Commands', () => {
it('should execute immediately when selected', () => {
const mockSetShow = vi.fn()
const mockSetSearchQuery = vi.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 = vi.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 = vi.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: vi.fn(),
register: vi.fn(),
unregister: vi.fn(),
}
vi.mocked(slashCommandRegistry.findCommand).mockReturnValue(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: vi.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)
})
})
})