mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 00:50:05 +00:00
Basic commandbox
This commit is contained in:
164
browser_tests/tests/commandSearchBox.spec.ts
Normal file
164
browser_tests/tests/commandSearchBox.spec.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Command search box', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
})
|
||||
|
||||
test('Can trigger command mode with ">" prefix', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
|
||||
// Type ">" to enter command mode
|
||||
await comfyPage.searchBox.input.fill('>')
|
||||
|
||||
// Verify filter button is hidden in command mode
|
||||
const filterButton = comfyPage.page.locator('.filter-button')
|
||||
await expect(filterButton).not.toBeVisible()
|
||||
|
||||
// Verify placeholder text changes
|
||||
await expect(comfyPage.searchBox.input).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search Commands')
|
||||
)
|
||||
})
|
||||
|
||||
test('Shows command list when entering command mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>')
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
|
||||
// Check that commands are shown
|
||||
const firstItem = comfyPage.searchBox.dropdown.locator('li').first()
|
||||
await expect(firstItem).toBeVisible()
|
||||
|
||||
// Verify it shows a command item with icon
|
||||
const commandIcon = firstItem.locator('.item-icon')
|
||||
await expect(commandIcon).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can search and filter commands', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>save')
|
||||
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500) // Wait for search to complete
|
||||
|
||||
// Get all visible command items
|
||||
const items = comfyPage.searchBox.dropdown.locator('li')
|
||||
const count = await items.count()
|
||||
|
||||
// Should have filtered results
|
||||
expect(count).toBeGreaterThan(0)
|
||||
expect(count).toBeLessThan(10) // Should be filtered, not showing all
|
||||
|
||||
// Verify first result contains "save"
|
||||
const firstLabel = await items.first().locator('.item-label').textContent()
|
||||
expect(firstLabel?.toLowerCase()).toContain('save')
|
||||
})
|
||||
|
||||
test('Shows keybindings for commands that have them', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>undo')
|
||||
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Find the undo command
|
||||
const undoItem = comfyPage.searchBox.dropdown
|
||||
.locator('li')
|
||||
.filter({ hasText: 'Undo' })
|
||||
.first()
|
||||
|
||||
// Check if keybinding is shown (if configured)
|
||||
const keybinding = undoItem.locator('.item-keybinding')
|
||||
const keybindingCount = await keybinding.count()
|
||||
|
||||
// Keybinding might or might not be present depending on configuration
|
||||
if (keybindingCount > 0) {
|
||||
await expect(keybinding).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('Executes command on selection', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>new blank')
|
||||
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Count nodes before
|
||||
const nodesBefore = await comfyPage.page
|
||||
.locator('.litegraph.litenode')
|
||||
.count()
|
||||
|
||||
// Select the new blank workflow command
|
||||
const newBlankItem = comfyPage.searchBox.dropdown
|
||||
.locator('li')
|
||||
.filter({ hasText: 'New Blank Workflow' })
|
||||
.first()
|
||||
await newBlankItem.click()
|
||||
|
||||
// Search box should close
|
||||
await expect(comfyPage.searchBox.input).not.toBeVisible()
|
||||
|
||||
// Verify workflow was cleared (no nodes)
|
||||
const nodesAfter = await comfyPage.page
|
||||
.locator('.litegraph.litenode')
|
||||
.count()
|
||||
expect(nodesAfter).toBe(0)
|
||||
})
|
||||
|
||||
test.skip('Returns to node search when removing ">"', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
// Enter command mode
|
||||
await comfyPage.searchBox.input.fill('>')
|
||||
await expect(comfyPage.page.locator('.filter-button')).not.toBeVisible()
|
||||
|
||||
// Return to node search by clearing input and triggering search
|
||||
await comfyPage.searchBox.input.clear()
|
||||
await comfyPage.searchBox.input.press('Backspace') // Trigger search event
|
||||
|
||||
// Small wait for UI update
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
// Filter button should be visible again
|
||||
await expect(comfyPage.page.locator('.filter-button')).toBeVisible()
|
||||
|
||||
// Placeholder should change back
|
||||
await expect(comfyPage.searchBox.input).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search Nodes')
|
||||
)
|
||||
})
|
||||
|
||||
test('Command search is case insensitive', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
// Search with lowercase
|
||||
await comfyPage.searchBox.input.fill('>SAVE')
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Should find save commands
|
||||
const items = comfyPage.searchBox.dropdown.locator('li')
|
||||
const count = await items.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
// Verify it found save-related commands
|
||||
const firstLabel = await items.first().locator('.item-label').textContent()
|
||||
expect(firstLabel?.toLowerCase()).toContain('save')
|
||||
})
|
||||
})
|
||||
79
src/components/searchbox/CommandSearchItem.vue
Normal file
79
src/components/searchbox/CommandSearchItem.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="comfy-command-search-item">
|
||||
<span v-if="command.icon" class="item-icon" :class="command.icon" />
|
||||
<span v-else class="item-icon pi pi-chevron-right" />
|
||||
|
||||
<span class="item-label">
|
||||
<span v-html="highlightedLabel" />
|
||||
</span>
|
||||
|
||||
<span v-if="command.keybinding" class="item-keybinding">
|
||||
{{ command.keybinding.combo.toString() }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
|
||||
const { command, currentQuery } = defineProps<{
|
||||
command: ComfyCommandImpl
|
||||
currentQuery: string
|
||||
}>()
|
||||
|
||||
const highlightedLabel = computed(() => {
|
||||
const label = command.label || command.id
|
||||
if (!currentQuery) return label
|
||||
|
||||
// Simple highlighting logic - case insensitive
|
||||
const regex = new RegExp(
|
||||
`(${currentQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`,
|
||||
'gi'
|
||||
)
|
||||
return label.replace(regex, '<mark>$1</mark>')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-command-search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
|
||||
.item-label {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-label :deep(mark) {
|
||||
background-color: var(--p-highlight-background);
|
||||
color: var(--p-highlight-color);
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.item-keybinding {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--p-content-hover-background);
|
||||
color: var(--p-text-muted-color);
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
class="comfy-vue-node-search-container flex justify-center items-center w-full min-w-96"
|
||||
>
|
||||
<div
|
||||
v-if="enableNodePreview"
|
||||
v-if="enableNodePreview && !isCommandMode"
|
||||
class="comfy-vue-node-preview-container absolute left-[-350px] top-[50px]"
|
||||
>
|
||||
<NodePreview
|
||||
@@ -14,6 +14,7 @@
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="!isCommandMode"
|
||||
icon="pi pi-filter"
|
||||
severity="secondary"
|
||||
class="filter-button z-10"
|
||||
@@ -49,13 +50,22 @@
|
||||
auto-option-focus
|
||||
force-selection
|
||||
multiple
|
||||
:option-label="'display_name'"
|
||||
:option-label="getOptionLabel"
|
||||
@complete="search($event.query)"
|
||||
@option-select="emit('addNode', $event.value)"
|
||||
@option-select="onOptionSelect($event.value)"
|
||||
@focused-option-changed="setHoverSuggestion($event)"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<NodeSearchItem :node-def="option" :current-query="currentQuery" />
|
||||
<CommandSearchItem
|
||||
v-if="isCommandMode"
|
||||
:command="option"
|
||||
:current-query="currentQuery"
|
||||
/>
|
||||
<NodeSearchItem
|
||||
v-else
|
||||
:node-def="option"
|
||||
:current-query="currentQuery"
|
||||
/>
|
||||
</template>
|
||||
<!-- FilterAndValue -->
|
||||
<template #chip="{ value }">
|
||||
@@ -80,13 +90,16 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import CommandSearchItem from '@/components/searchbox/CommandSearchItem.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import { CommandSearchService } from '@/services/commandSearchService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
@@ -99,6 +112,7 @@ import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const enableNodePreview = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
|
||||
@@ -111,18 +125,50 @@ const { filters, searchLimit = 64 } = defineProps<{
|
||||
|
||||
const nodeSearchFilterVisible = ref(false)
|
||||
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
|
||||
const suggestions = ref<ComfyNodeDefImpl[]>([])
|
||||
const suggestions = ref<ComfyNodeDefImpl[] | ComfyCommandImpl[]>([])
|
||||
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
|
||||
const currentQuery = ref('')
|
||||
const isCommandMode = ref(false)
|
||||
|
||||
// Initialize command search service
|
||||
const commandSearchService = ref<CommandSearchService | null>(null)
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (isCommandMode.value) {
|
||||
return t('g.searchCommands', 'Search commands') + '...'
|
||||
}
|
||||
return filters.length === 0 ? t('g.searchNodes') + '...' : ''
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
|
||||
// Initialize command search service with commands
|
||||
watch(
|
||||
() => commandStore.commands,
|
||||
(commands) => {
|
||||
commandSearchService.value = new CommandSearchService(commands)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const search = (query: string) => {
|
||||
const queryIsEmpty = query === '' && filters.length === 0
|
||||
currentQuery.value = query
|
||||
|
||||
// Check if we're in command mode (query starts with ">")
|
||||
if (query.startsWith('>')) {
|
||||
isCommandMode.value = true
|
||||
if (commandSearchService.value) {
|
||||
suggestions.value = commandSearchService.value.searchCommands(query, {
|
||||
limit: searchLimit
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Normal node search mode
|
||||
isCommandMode.value = false
|
||||
const queryIsEmpty = query === '' && filters.length === 0
|
||||
suggestions.value = queryIsEmpty
|
||||
? nodeFrequencyStore.topNodeDefs
|
||||
: [
|
||||
@@ -132,7 +178,18 @@ const search = (query: string) => {
|
||||
]
|
||||
}
|
||||
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'addFilter',
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
): void
|
||||
(
|
||||
e: 'removeFilter',
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
): void
|
||||
(e: 'addNode', nodeDef: ComfyNodeDefImpl): void
|
||||
(e: 'executeCommand', command: ComfyCommandImpl): void
|
||||
}>()
|
||||
|
||||
let inputElement: HTMLInputElement | null = null
|
||||
const reFocusInput = async () => {
|
||||
@@ -160,11 +217,28 @@ const onRemoveFilter = async (
|
||||
await reFocusInput()
|
||||
}
|
||||
const setHoverSuggestion = (index: number) => {
|
||||
if (index === -1) {
|
||||
if (index === -1 || isCommandMode.value) {
|
||||
hoveredSuggestion.value = null
|
||||
return
|
||||
}
|
||||
const value = suggestions.value[index]
|
||||
const value = suggestions.value[index] as ComfyNodeDefImpl
|
||||
hoveredSuggestion.value = value
|
||||
}
|
||||
|
||||
const onOptionSelect = (option: ComfyNodeDefImpl | ComfyCommandImpl) => {
|
||||
if (isCommandMode.value) {
|
||||
emit('executeCommand', option as ComfyCommandImpl)
|
||||
} else {
|
||||
emit('addNode', option as ComfyNodeDefImpl)
|
||||
}
|
||||
}
|
||||
|
||||
const getOptionLabel = (
|
||||
option: ComfyNodeDefImpl | ComfyCommandImpl
|
||||
): string => {
|
||||
if ('display_name' in option) {
|
||||
return option.display_name
|
||||
}
|
||||
return option.label || option.id
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
@add-filter="addFilter"
|
||||
@remove-filter="removeFilter"
|
||||
@add-node="addNode"
|
||||
@execute-command="executeCommand"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -62,6 +64,7 @@ let disconnectOnReset = false
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const { visible } = storeToRefs(useSearchBoxStore())
|
||||
const dismissable = ref(true)
|
||||
@@ -109,6 +112,14 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
window.requestAnimationFrame(closeDialog)
|
||||
}
|
||||
|
||||
const executeCommand = async (command: ComfyCommandImpl) => {
|
||||
// Close the dialog immediately
|
||||
closeDialog()
|
||||
|
||||
// Execute the command
|
||||
await commandStore.execute(command.id)
|
||||
}
|
||||
|
||||
const newSearchBoxEnabled = computed(
|
||||
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"searchWorkflows": "Search Workflows",
|
||||
"searchSettings": "Search Settings",
|
||||
"searchNodes": "Search Nodes",
|
||||
"searchCommands": "Search Commands",
|
||||
"searchModels": "Search Models",
|
||||
"searchKeybindings": "Search Keybindings",
|
||||
"searchExtensions": "Search Extensions",
|
||||
|
||||
66
src/services/commandSearchService.ts
Normal file
66
src/services/commandSearchService.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
|
||||
export interface CommandSearchOptions {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export class CommandSearchService {
|
||||
private fuse: Fuse<ComfyCommandImpl>
|
||||
private commands: ComfyCommandImpl[]
|
||||
|
||||
constructor(commands: ComfyCommandImpl[]) {
|
||||
this.commands = commands
|
||||
this.fuse = new Fuse(commands, {
|
||||
keys: [
|
||||
{ name: 'label', weight: 2 },
|
||||
{ name: 'id', weight: 1 }
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
shouldSort: true,
|
||||
minMatchCharLength: 1
|
||||
})
|
||||
}
|
||||
|
||||
public updateCommands(commands: ComfyCommandImpl[]) {
|
||||
this.commands = commands
|
||||
const options = {
|
||||
keys: [
|
||||
{ name: 'label', weight: 2 },
|
||||
{ name: 'id', weight: 1 }
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
shouldSort: true,
|
||||
minMatchCharLength: 1
|
||||
}
|
||||
this.fuse = new Fuse(commands, options)
|
||||
}
|
||||
|
||||
public searchCommands(
|
||||
query: string,
|
||||
options?: CommandSearchOptions
|
||||
): ComfyCommandImpl[] {
|
||||
// Remove the leading ">" if present
|
||||
const searchQuery = query.startsWith('>') ? query.slice(1).trim() : query
|
||||
|
||||
// If empty query, return all commands sorted alphabetically by label
|
||||
if (!searchQuery) {
|
||||
const sortedCommands = [...this.commands].sort((a, b) => {
|
||||
const labelA = a.label || a.id
|
||||
const labelB = b.label || b.id
|
||||
return labelA.localeCompare(labelB)
|
||||
})
|
||||
return options?.limit
|
||||
? sortedCommands.slice(0, options.limit)
|
||||
: sortedCommands
|
||||
}
|
||||
|
||||
const results = this.fuse.search(searchQuery)
|
||||
const commands = results.map((result) => result.item)
|
||||
|
||||
return options?.limit ? commands.slice(0, options.limit) : commands
|
||||
}
|
||||
}
|
||||
131
tests-ui/tests/commandSearchService.test.ts
Normal file
131
tests-ui/tests/commandSearchService.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CommandSearchService } from '@/services/commandSearchService'
|
||||
import { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
|
||||
describe('CommandSearchService', () => {
|
||||
// Mock commands
|
||||
const mockCommands: ComfyCommandImpl[] = [
|
||||
new ComfyCommandImpl({
|
||||
id: 'Comfy.NewBlankWorkflow',
|
||||
label: 'New Blank Workflow',
|
||||
icon: 'pi pi-plus',
|
||||
function: () => {}
|
||||
}),
|
||||
new ComfyCommandImpl({
|
||||
id: 'Comfy.SaveWorkflow',
|
||||
label: 'Save Workflow',
|
||||
icon: 'pi pi-save',
|
||||
function: () => {}
|
||||
}),
|
||||
new ComfyCommandImpl({
|
||||
id: 'Comfy.OpenWorkflow',
|
||||
label: 'Open Workflow',
|
||||
icon: 'pi pi-folder-open',
|
||||
function: () => {}
|
||||
}),
|
||||
new ComfyCommandImpl({
|
||||
id: 'Comfy.ClearWorkflow',
|
||||
label: 'Clear Workflow',
|
||||
icon: 'pi pi-trash',
|
||||
function: () => {}
|
||||
}),
|
||||
new ComfyCommandImpl({
|
||||
id: 'Comfy.Undo',
|
||||
label: 'Undo',
|
||||
icon: 'pi pi-undo',
|
||||
function: () => {}
|
||||
})
|
||||
]
|
||||
|
||||
describe('searchCommands', () => {
|
||||
it('should return all commands sorted alphabetically when query is empty', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('')
|
||||
|
||||
expect(results).toHaveLength(mockCommands.length)
|
||||
expect(results[0].label).toBe('Clear Workflow')
|
||||
expect(results[1].label).toBe('New Blank Workflow')
|
||||
expect(results[2].label).toBe('Open Workflow')
|
||||
expect(results[3].label).toBe('Save Workflow')
|
||||
expect(results[4].label).toBe('Undo')
|
||||
})
|
||||
|
||||
it('should handle query with leading ">"', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('>workflow')
|
||||
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
results.every(
|
||||
(cmd) =>
|
||||
cmd.label?.toLowerCase().includes('workflow') ||
|
||||
cmd.id.toLowerCase().includes('workflow')
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should search by label', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('save')
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].label).toBe('Save Workflow')
|
||||
})
|
||||
|
||||
it('should search by id', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('ClearWorkflow')
|
||||
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0].id).toBe('Comfy.ClearWorkflow')
|
||||
})
|
||||
|
||||
it('should respect search limit', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('', { limit: 2 })
|
||||
|
||||
expect(results).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle partial matches', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('work')
|
||||
|
||||
expect(results.length).toBeGreaterThan(1)
|
||||
expect(
|
||||
results.every(
|
||||
(cmd) =>
|
||||
cmd.label?.toLowerCase().includes('work') ||
|
||||
cmd.id.toLowerCase().includes('work')
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should return empty array for no matches', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const results = service.searchCommands('xyz123')
|
||||
|
||||
expect(results).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateCommands', () => {
|
||||
it('should update the commands list', () => {
|
||||
const service = new CommandSearchService(mockCommands)
|
||||
const newCommands = [
|
||||
new ComfyCommandImpl({
|
||||
id: 'Test.Command',
|
||||
label: 'Test Command',
|
||||
function: () => {}
|
||||
})
|
||||
]
|
||||
|
||||
service.updateCommands(newCommands)
|
||||
const results = service.searchCommands('')
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].id).toBe('Test.Command')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user