mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-28 08:21:27 -03:00
feat(bulk): add shift+click range selection in bulk mode
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { describe, it, beforeEach, afterEach, expect } from 'vitest';
|
||||
import { state } from '../../../static/js/state/index.js';
|
||||
import { MODEL_TYPES } from '../../../static/js/api/apiConfig.js';
|
||||
import { eventManager } from '../../../static/js/utils/EventManager.js';
|
||||
import { BulkManager } from '../../../static/js/managers/BulkManager.js';
|
||||
|
||||
function createCard(filePath) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'model-card';
|
||||
card.dataset.filepath = filePath;
|
||||
document.body.appendChild(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
describe('BulkManager shift+click range selection', () => {
|
||||
let bulk;
|
||||
|
||||
beforeEach(() => {
|
||||
eventManager.cleanup();
|
||||
state.currentPageType = MODEL_TYPES.LORA;
|
||||
state.bulkMode = true;
|
||||
state.selectedModels.clear();
|
||||
state.loraMetadataCache.clear();
|
||||
document.body.innerHTML = '';
|
||||
|
||||
bulk = new BulkManager();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
eventManager.cleanup();
|
||||
delete state.virtualScroller;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
function attachFakeScroller(filePaths) {
|
||||
const items = filePaths.map((fp) => ({
|
||||
file_path: fp,
|
||||
file_name: fp.split('/').pop(),
|
||||
folder: '',
|
||||
usage_tips: '{}',
|
||||
}));
|
||||
state.virtualScroller = {
|
||||
items,
|
||||
findIndexByFilePath: (fp) => items.findIndex((item) => item.file_path === fp),
|
||||
};
|
||||
return items;
|
||||
}
|
||||
|
||||
it('a plain click selects a single card and sets it as the shift anchor', () => {
|
||||
const cardA = createCard('/models/a.safetensors');
|
||||
|
||||
bulk.toggleCardSelection(cardA);
|
||||
|
||||
expect(state.selectedModels.has('/models/a.safetensors')).toBe(true);
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/a.safetensors');
|
||||
});
|
||||
|
||||
it('shift+click selects every item between anchor and target', () => {
|
||||
attachFakeScroller(['/models/a', '/models/b', '/models/c', '/models/d']);
|
||||
const cards = ['/models/a', '/models/b', '/models/c', '/models/d'].map(createCard);
|
||||
|
||||
bulk.toggleCardSelection(cards[0]);
|
||||
bulk.toggleCardSelection(cards[3], true);
|
||||
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/a', '/models/b', '/models/c', '/models/d']);
|
||||
cards.forEach((card) => expect(card.classList.contains('selected')).toBe(true));
|
||||
// Anchor stays on the original plain-clicked card
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/a');
|
||||
});
|
||||
|
||||
it('shift+click works when the target precedes the anchor', () => {
|
||||
attachFakeScroller(['/models/a', '/models/b', '/models/c', '/models/d']);
|
||||
const cards = ['/models/a', '/models/b', '/models/c', '/models/d'].map(createCard);
|
||||
|
||||
bulk.toggleCardSelection(cards[3]);
|
||||
bulk.toggleCardSelection(cards[1], true);
|
||||
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/b', '/models/c', '/models/d']);
|
||||
});
|
||||
|
||||
it('consecutive shift+clicks re-derive the range from the same anchor', () => {
|
||||
attachFakeScroller(['/models/a', '/models/b', '/models/c', '/models/d', '/models/e']);
|
||||
const cards = ['/models/a', '/models/b', '/models/c', '/models/d', '/models/e'].map(createCard);
|
||||
|
||||
bulk.toggleCardSelection(cards[0]);
|
||||
bulk.toggleCardSelection(cards[2], true);
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/a', '/models/b', '/models/c']);
|
||||
|
||||
bulk.toggleCardSelection(cards[4], true);
|
||||
expect([...state.selectedModels].sort()).toEqual([
|
||||
'/models/a', '/models/b', '/models/c', '/models/d', '/models/e',
|
||||
]);
|
||||
|
||||
bulk.toggleCardSelection(cards[1], true);
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/a', '/models/b']);
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/a');
|
||||
});
|
||||
|
||||
it('shift+click drops selections that fall outside the new range', () => {
|
||||
attachFakeScroller(['/models/a', '/models/b', '/models/c', '/models/d', '/models/e']);
|
||||
const cards = ['/models/a', '/models/b', '/models/c', '/models/d', '/models/e'].map(createCard);
|
||||
|
||||
bulk.toggleCardSelection(cards[0]);
|
||||
|
||||
// Simulate an out-of-range selection (e.g. from a previous marquee)
|
||||
state.selectedModels.add('/models/e');
|
||||
cards[4].classList.add('selected');
|
||||
|
||||
bulk.toggleCardSelection(cards[2], true);
|
||||
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/a', '/models/b', '/models/c']);
|
||||
expect(cards[4].classList.contains('selected')).toBe(false);
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/a');
|
||||
});
|
||||
|
||||
it('shift+click without an anchor falls back to a plain toggle', () => {
|
||||
attachFakeScroller(['/models/a', '/models/b', '/models/c']);
|
||||
const cardC = createCard('/models/c');
|
||||
|
||||
bulk.toggleCardSelection(cardC, true);
|
||||
|
||||
expect([...state.selectedModels]).toEqual(['/models/c']);
|
||||
expect(cardC.classList.contains('selected')).toBe(true);
|
||||
// The fallback click becomes the new anchor
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/c');
|
||||
});
|
||||
|
||||
it('shift+click falls back to a plain toggle when the anchor is no longer listed', () => {
|
||||
const items = attachFakeScroller(['/models/a', '/models/b', '/models/c', '/models/d']);
|
||||
const cards = ['/models/a', '/models/b', '/models/c', '/models/d'].map(createCard);
|
||||
|
||||
bulk.toggleCardSelection(cards[0]);
|
||||
|
||||
// Simulate a filter change removing the anchor from the loaded items
|
||||
items.splice(0, 1);
|
||||
|
||||
bulk.toggleCardSelection(cards[3], true);
|
||||
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/a', '/models/d']);
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/d');
|
||||
});
|
||||
|
||||
it('range selection populates metadata cache for items without rendered cards', () => {
|
||||
attachFakeScroller(['/models/a', '/models/b', '/models/c', '/models/d']);
|
||||
|
||||
// Only the endpoints exist in the DOM (b, c are virtualized away)
|
||||
const cardA = createCard('/models/a');
|
||||
const cardD = createCard('/models/d');
|
||||
|
||||
bulk.toggleCardSelection(cardA);
|
||||
bulk.toggleCardSelection(cardD, true);
|
||||
|
||||
expect([...state.selectedModels].sort()).toEqual(['/models/a', '/models/b', '/models/c', '/models/d']);
|
||||
const cacheB = state.loraMetadataCache.get('/models/b');
|
||||
expect(cacheB.fileName).toBe('b');
|
||||
});
|
||||
|
||||
it('exiting bulk mode clears the anchor along with the selection', () => {
|
||||
const cardA = createCard('/models/a');
|
||||
bulk.toggleCardSelection(cardA);
|
||||
expect(bulk.bulkAnchorFilepath).toBe('/models/a');
|
||||
|
||||
bulk.toggleBulkMode();
|
||||
|
||||
expect(bulk.bulkAnchorFilepath).toBeNull();
|
||||
expect(state.selectedModels.size).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user