mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
- Add LOG10(post_count) weighting to BM25 score for better relevance ranking - Prioritize tag_name prefix matches above alias matches using CASE statement - Remove frontend re-scoring logic to trust backend排序 results - Fix pagination consistency: page N+1 scores <= page N minimum score Key improvements: - '1girl' (6M posts) now ranks #1 instead of #149 for search '1' - tag_name prefix matches always appear before alias matches - Popular tags rank higher than obscure ones with same prefix - Consistent ordering across pagination boundaries Test coverage: - Add test_search_tag_name_prefix_match_priority - Add test_search_ranks_popular_tags_higher - Add test_search_pagination_ordering_consistency - Add test_search_rank_score_includes_popularity_weight - Update test data with 15 tags starting with '1' Fixes issues with autocomplete dropdown showing inconsistent results when scrolling through paginated search results.
271 lines
9.2 KiB
JavaScript
271 lines
9.2 KiB
JavaScript
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
|
|
|
const {
|
|
API_MODULE,
|
|
APP_MODULE,
|
|
CARET_HELPER_MODULE,
|
|
PREVIEW_COMPONENT_MODULE,
|
|
AUTOCOMPLETE_MODULE,
|
|
} = vi.hoisted(() => ({
|
|
API_MODULE: new URL('../../../scripts/api.js', import.meta.url).pathname,
|
|
APP_MODULE: new URL('../../../scripts/app.js', import.meta.url).pathname,
|
|
CARET_HELPER_MODULE: new URL('../../../web/comfyui/textarea_caret_helper.js', import.meta.url).pathname,
|
|
PREVIEW_COMPONENT_MODULE: new URL('../../../web/comfyui/preview_tooltip.js', import.meta.url).pathname,
|
|
AUTOCOMPLETE_MODULE: new URL('../../../web/comfyui/autocomplete.js', import.meta.url).pathname,
|
|
}));
|
|
|
|
const fetchApiMock = vi.fn();
|
|
const caretHelperInstance = {
|
|
getBeforeCursor: vi.fn(() => ''),
|
|
getCursorOffset: vi.fn(() => ({ left: 0, top: 0 })),
|
|
};
|
|
|
|
const previewTooltipMock = {
|
|
show: vi.fn(),
|
|
hide: vi.fn(),
|
|
cleanup: vi.fn(),
|
|
};
|
|
|
|
vi.mock(API_MODULE, () => ({
|
|
api: {
|
|
fetchApi: fetchApiMock,
|
|
},
|
|
}));
|
|
|
|
vi.mock(APP_MODULE, () => ({
|
|
app: {
|
|
canvas: {
|
|
ds: { scale: 1 },
|
|
},
|
|
registerExtension: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock(CARET_HELPER_MODULE, () => ({
|
|
TextAreaCaretHelper: vi.fn(() => caretHelperInstance),
|
|
}));
|
|
|
|
vi.mock(PREVIEW_COMPONENT_MODULE, () => ({
|
|
PreviewTooltip: vi.fn(() => previewTooltipMock),
|
|
}));
|
|
|
|
describe('AutoComplete widget interactions', () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
document.head.querySelectorAll('style').forEach((styleEl) => styleEl.remove());
|
|
Element.prototype.scrollIntoView = vi.fn();
|
|
fetchApiMock.mockReset();
|
|
caretHelperInstance.getBeforeCursor.mockReset();
|
|
caretHelperInstance.getCursorOffset.mockReset();
|
|
caretHelperInstance.getBeforeCursor.mockReturnValue('');
|
|
caretHelperInstance.getCursorOffset.mockReturnValue({ left: 0, top: 0 });
|
|
previewTooltipMock.show.mockReset();
|
|
previewTooltipMock.hide.mockReset();
|
|
previewTooltipMock.cleanup.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('fetches and renders search results when input exceeds the minimum characters', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
fetchApiMock.mockResolvedValue({
|
|
json: () => Promise.resolve({ success: true, relative_paths: ['models/example.safetensors'] }),
|
|
});
|
|
|
|
caretHelperInstance.getBeforeCursor.mockReturnValue('example');
|
|
caretHelperInstance.getCursorOffset.mockReturnValue({ left: 15, top: 25 });
|
|
|
|
const input = document.createElement('textarea');
|
|
document.body.append(input);
|
|
|
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
|
const autoComplete = new AutoComplete(input, 'loras', { debounceDelay: 0, showPreview: false });
|
|
|
|
input.value = 'example';
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
await vi.runAllTimersAsync();
|
|
await Promise.resolve();
|
|
|
|
expect(fetchApiMock).toHaveBeenCalledWith('/lm/loras/relative-paths?search=example&limit=100');
|
|
const items = autoComplete.dropdown.querySelectorAll('.comfy-autocomplete-item');
|
|
expect(items).toHaveLength(1);
|
|
expect(autoComplete.dropdown.style.display).toBe('block');
|
|
expect(autoComplete.isVisible).toBe(true);
|
|
expect(caretHelperInstance.getCursorOffset).toHaveBeenCalled();
|
|
});
|
|
|
|
it('inserts the selected LoRA with usage tip strengths and restores focus', async () => {
|
|
fetchApiMock.mockImplementation((url) => {
|
|
if (url.includes('usage-tips-by-path')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
usage_tips: JSON.stringify({ strength: '1.5', clip_strength: '0.9' }),
|
|
}),
|
|
});
|
|
}
|
|
|
|
return Promise.resolve({
|
|
json: () => Promise.resolve({ success: true, relative_paths: ['models/example.safetensors'] }),
|
|
});
|
|
});
|
|
|
|
caretHelperInstance.getBeforeCursor.mockReturnValue('alpha, example');
|
|
|
|
const input = document.createElement('textarea');
|
|
input.value = 'alpha, example';
|
|
input.selectionStart = input.value.length;
|
|
input.focus = vi.fn();
|
|
input.setSelectionRange = vi.fn();
|
|
document.body.append(input);
|
|
|
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
|
const autoComplete = new AutoComplete(input, 'loras', { debounceDelay: 0, showPreview: false });
|
|
|
|
await autoComplete.insertSelection('models/example.safetensors');
|
|
|
|
expect(fetchApiMock).toHaveBeenCalledWith(
|
|
'/lm/loras/usage-tips-by-path?relative_path=models%2Fexample.safetensors',
|
|
);
|
|
expect(input.value).toContain('<lora:example:1.5:0.9>, ');
|
|
expect(autoComplete.dropdown.style.display).toBe('none');
|
|
expect(input.focus).toHaveBeenCalled();
|
|
expect(input.setSelectionRange).toHaveBeenCalled();
|
|
});
|
|
|
|
it('highlights multiple include tokens while ignoring excluded ones', async () => {
|
|
const input = document.createElement('textarea');
|
|
document.body.append(input);
|
|
|
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
|
const autoComplete = new AutoComplete(input, 'loras', { showPreview: false });
|
|
|
|
const highlighted = autoComplete.highlightMatch(
|
|
'models/flux/beta-detail.safetensors',
|
|
'flux detail -beta',
|
|
);
|
|
|
|
const highlightCount = (highlighted.match(/<span/g) || []).length;
|
|
expect(highlightCount).toBe(2);
|
|
expect(highlighted).toContain('flux');
|
|
expect(highlighted).toContain('detail');
|
|
expect(highlighted).not.toMatch(/beta<\/span>/i);
|
|
});
|
|
|
|
it('handles arrow key navigation with virtual scrolling', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const mockItems = Array.from({ length: 50 }, (_, i) => `model_${i.toString().padStart(2, '0')}.safetensors`);
|
|
|
|
fetchApiMock.mockResolvedValue({
|
|
json: () => Promise.resolve({ success: true, relative_paths: mockItems }),
|
|
});
|
|
|
|
caretHelperInstance.getBeforeCursor.mockReturnValue('model');
|
|
caretHelperInstance.getCursorOffset.mockReturnValue({ left: 15, top: 25 });
|
|
|
|
const input = document.createElement('textarea');
|
|
document.body.append(input);
|
|
|
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
|
const autoComplete = new AutoComplete(input, 'loras', {
|
|
debounceDelay: 0,
|
|
showPreview: false,
|
|
enableVirtualScroll: true,
|
|
itemHeight: 40,
|
|
visibleItems: 15,
|
|
pageSize: 20,
|
|
});
|
|
|
|
input.value = 'model';
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
await vi.runAllTimersAsync();
|
|
await Promise.resolve();
|
|
|
|
expect(autoComplete.items.length).toBeGreaterThan(0);
|
|
expect(autoComplete.selectedIndex).toBe(0);
|
|
|
|
const initialSelectedEl = autoComplete.contentContainer?.querySelector('.comfy-autocomplete-item-selected');
|
|
expect(initialSelectedEl).toBeDefined();
|
|
|
|
const arrowDownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true });
|
|
input.dispatchEvent(arrowDownEvent);
|
|
|
|
expect(autoComplete.selectedIndex).toBe(1);
|
|
|
|
const secondSelectedEl = autoComplete.contentContainer?.querySelector('.comfy-autocomplete-item-selected');
|
|
expect(secondSelectedEl).toBeDefined();
|
|
expect(secondSelectedEl?.dataset.index).toBe('1');
|
|
|
|
const arrowUpEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true });
|
|
input.dispatchEvent(arrowUpEvent);
|
|
|
|
expect(autoComplete.selectedIndex).toBe(0);
|
|
|
|
const firstSelectedElAgain = autoComplete.contentContainer?.querySelector('.comfy-autocomplete-item-selected');
|
|
expect(firstSelectedElAgain).toBeDefined();
|
|
expect(firstSelectedElAgain?.dataset.index).toBe('0');
|
|
});
|
|
|
|
it('maintains selection when scrolling to invisible items', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const mockItems = Array.from({ length: 100 }, (_, i) => `item_${i.toString().padStart(3, '0')}.safetensors`);
|
|
|
|
fetchApiMock.mockResolvedValue({
|
|
json: () => Promise.resolve({ success: true, relative_paths: mockItems }),
|
|
});
|
|
|
|
caretHelperInstance.getBeforeCursor.mockReturnValue('item');
|
|
caretHelperInstance.getCursorOffset.mockReturnValue({ left: 15, top: 25 });
|
|
|
|
const input = document.createElement('textarea');
|
|
input.style.width = '400px';
|
|
input.style.height = '200px';
|
|
document.body.append(input);
|
|
|
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
|
const autoComplete = new AutoComplete(input, 'loras', {
|
|
debounceDelay: 0,
|
|
showPreview: false,
|
|
enableVirtualScroll: true,
|
|
itemHeight: 40,
|
|
visibleItems: 15,
|
|
pageSize: 20,
|
|
});
|
|
|
|
input.value = 'item';
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
await vi.runAllTimersAsync();
|
|
await Promise.resolve();
|
|
|
|
expect(autoComplete.items.length).toBeGreaterThan(0);
|
|
|
|
autoComplete.selectedIndex = 14;
|
|
|
|
const scrollTopBefore = autoComplete.scrollContainer?.scrollTop || 0;
|
|
|
|
const arrowDownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true });
|
|
input.dispatchEvent(arrowDownEvent);
|
|
|
|
await vi.runAllTimersAsync();
|
|
await Promise.resolve();
|
|
|
|
expect(autoComplete.selectedIndex).toBe(15);
|
|
|
|
const selectedEl = autoComplete.contentContainer?.querySelector('.comfy-autocomplete-item-selected');
|
|
expect(selectedEl).toBeDefined();
|
|
expect(selectedEl?.dataset.index).toBe('15');
|
|
|
|
const scrollTopAfter = autoComplete.scrollContainer?.scrollTop || 0;
|
|
expect(scrollTopAfter).toBeGreaterThanOrEqual(scrollTopBefore);
|
|
});
|
|
});
|