mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
test(frontend): add filtering coverage for model pages
This commit is contained in:
44
docs/frontend-filtering-test-matrix.md
Normal file
44
docs/frontend-filtering-test-matrix.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# LoRA & Checkpoints Filtering/Sorting Test Matrix
|
||||
|
||||
This matrix captures the scenarios that Phase 3 frontend tests should cover for the LoRA and Checkpoint managers. It focuses on how search, filter, sort, and duplicate badge toggles interact so future specs can share fixtures and expectations.
|
||||
|
||||
## Scope
|
||||
|
||||
- **Components**: `PageControls`, `FilterManager`, `SearchManager`, and `ModelDuplicatesManager` wiring invoked through `CheckpointsPageManager` and `LorasPageManager`.
|
||||
- **Templates**: `templates/loras.html` and `templates/checkpoints.html` along with shared filter panel and toolbar partials.
|
||||
- **APIs**: Requests issued through `baseModelApi.fetchModels` (via `resetAndReload`/`refreshModels`) and duplicates badge updates.
|
||||
|
||||
## Shared Setup Considerations
|
||||
|
||||
1. Render full page templates using `renderLorasPage` / `renderCheckpointsPage` helpers before importing modules so DOM queries resolve.
|
||||
2. Stub storage helpers (`getStorageItem`, `setStorageItem`, `getSessionItem`, `setSessionItem`) to observe persistence behavior without mutating real storage.
|
||||
3. Mock `sidebarManager` to capture refresh calls triggered after sort/filter actions.
|
||||
4. Provide fake API implementations exposing `resetAndReload`, `refreshModels`, `fetchFromCivitai`, `toggleBulkMode`, and `clearCustomFilter` so control events remain asynchronous but deterministic.
|
||||
5. Supply a minimal `ModelDuplicatesManager` mock exposing `toggleDuplicateMode`, `checkDuplicatesCount`, and `updateDuplicatesBadgeAfterRefresh` to validate duplicate badge wiring.
|
||||
|
||||
## Scenario Matrix
|
||||
|
||||
| ID | Feature | Scenario | LoRAs Expectations | Checkpoints Expectations | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| F-01 | Search filter | Typing a query updates `pageState.filters.search`, persists to session, and triggers `resetAndReload` on submit | Validate `SearchManager` writes query and reloads via API stub; confirm LoRA cards pass query downstream | Same as LoRAs | Cover `enter` press and clicking search icon |
|
||||
| F-02 | Tag filter | Selecting a tag chip adds it to filters, applies active styling, and reloads results | Tag stored under `filters.tags`; `FilterManager.applyFilters` persists and triggers `resetAndReload(true)` | Same; ensure base model tag set is scoped to checkpoints dataset | Include removal path |
|
||||
| F-03 | Base model filter | Toggling base model checkboxes updates `filters.baseModel`, persists, and reloads | Ensure only LoRA-supported models show; toggle multi-select | Ensure SDXL/Flux base models appear as expected | Capture UI state restored from storage on next init |
|
||||
| F-04 | Favorites-only | Clicking favorites toggle updates session flag and calls `resetAndReload(true)` | Button gains `.active` class and API called | Same | Verify duplicates badge refresh when active |
|
||||
| F-05 | Sort selection | Changing sort select saves preference (legacy + new format) and reloads | Confirm `PageControls.saveSortPreference` invoked with option and API called | Same with checkpoints-specific defaults | Cover `convertLegacySortFormat` branch |
|
||||
| F-06 | Filter persistence | Re-initializing manager loads stored filters/sort and updates DOM | Filters pre-populate chips/checkboxes; favorites state restored | Same | Requires simulating repeated construction |
|
||||
| F-07 | Combined filters | Applying search + tag + base model yields aggregated query params for fetch | Assert API receives merged filter payload | Same | Validate toast messaging for active filters |
|
||||
| F-08 | Clearing filters | Using "Clear filters" resets state, storage, and reloads list | `FilterManager.clearFilters` empties `filters`, removes active class, shows toast | Same | Ensure favorites-only toggle unaffected |
|
||||
| F-09 | Duplicate badge toggle | Pressing "Find duplicates" toggles duplicate mode and updates badge counts post-refresh | `ModelDuplicatesManager.toggleDuplicateMode` invoked and badge refresh called after API rebuild | Same plus checkpoint-specific duplicate badge dataset | Connects to future duplicate-specific specs |
|
||||
| F-10 | Bulk actions menu | Opening bulk dropdown keeps filters intact and closes on outside click | Validate dropdown class toggling and no unintended reload | Same | Guard against regression when dropdown interacts with filters |
|
||||
|
||||
## Automation Coverage Status
|
||||
|
||||
- ✅ F-01 Search filter, F-02 Tag filter, F-03 Base model filter, F-04 Favorites-only toggle, F-05 Sort selection, and F-09 Duplicate badge toggle are covered by `tests/frontend/components/pageControls.filtering.test.js` for both LoRA and checkpoint pages.
|
||||
- ⏳ F-06 Filter persistence, F-07 Combined filters, F-08 Clearing filters, and F-10 Bulk actions remain to be automated alongside upcoming bulk mode refinements.
|
||||
|
||||
## Coverage Gaps & Follow-Ups
|
||||
|
||||
- Write Vitest suites that exercise the matrix for both managers, sharing fixtures through page helpers to avoid duplication.
|
||||
- Capture API parameter assertions by inspecting `baseModelApi.fetchModels` mocks rather than relying solely on state mutations.
|
||||
- Add regression cases for legacy storage migrations (old filter keys) once fixtures exist for older payloads.
|
||||
- Extend duplicate badge coverage with scenarios where `checkDuplicatesCount` signals zero duplicates versus pending calculations.
|
||||
@@ -9,7 +9,7 @@ This roadmap tracks the planned rollout of automated testing for the ComfyUI LoR
|
||||
| Phase 0 | Establish baseline tooling | Add Node test runner, jsdom environment, and seed smoke tests | ✅ Complete | Vitest + jsdom configured, example state tests committed |
|
||||
| Phase 1 | Cover state management logic | Unit test selectors, derived data helpers, and storage utilities under `static/js/state` and `static/js/utils` | ✅ Complete | Storage helpers and state selectors now exercised via deterministic suites |
|
||||
| Phase 2 | Test AppCore orchestration | Simulate page bootstrapping, infinite scroll hooks, and manager registration using JSDOM DOM fixtures | ✅ Complete | AppCore initialization + page feature suites now validate manager wiring, infinite scroll hooks, and onboarding gating |
|
||||
| Phase 3 | Validate page-specific managers | Add focused suites for `loras`, `checkpoints`, `embeddings`, and `recipes` managers covering filtering, sorting, and bulk actions | 🚧 In Progress | LoRA + checkpoints smoke suites landed; outlining filter/sort coverage before extending to embeddings |
|
||||
| Phase 3 | Validate page-specific managers | Add focused suites for `loras`, `checkpoints`, `embeddings`, and `recipes` managers covering filtering, sorting, and bulk actions | 🚧 In Progress | LoRA + checkpoints smoke suites landed; filter/sort scenario matrix drafted to guide upcoming specs |
|
||||
| Phase 4 | Interaction-level regression tests | Exercise template fragments, modals, and menus to ensure UI wiring remains intact | ⚪ Not Started | Evaluate Playwright component testing or happy-path DOM snapshots |
|
||||
| Phase 5 | Continuous integration & coverage | Integrate frontend tests into CI workflow and track coverage metrics | ⚪ Not Started | Align reporting directories with backend coverage for unified reporting |
|
||||
|
||||
@@ -22,8 +22,11 @@ This roadmap tracks the planned rollout of automated testing for the ComfyUI LoR
|
||||
- [x] Extend AppCore orchestration tests to cover manager wiring, bulk menu setup, and onboarding gating scenarios.
|
||||
- [ ] Evaluate integrating coverage reporting once test surface grows (> 20 specs).
|
||||
- [x] Create shared fixtures for the loras and checkpoints pages once dedicated manager suites are added.
|
||||
- [ ] Draft focused test matrix for loras/checkpoints manager filtering and sorting paths ahead of Phase 3.
|
||||
- [x] Draft focused test matrix for loras/checkpoints manager filtering and sorting paths ahead of Phase 3.
|
||||
- [x] Implement LoRAs manager filtering/sorting specs for scenarios F-01–F-05 & F-09; queue remaining edge cases after duplicate/bulk flows stabilize.
|
||||
- [x] Implement checkpoints manager filtering/sorting specs for scenarios F-01–F-05 & F-09; cover remaining paths alongside bulk action work.
|
||||
- [x] Implement checkpoints page manager smoke tests covering initialization and duplicate badge wiring.
|
||||
- [ ] Outline focused checkpoints scenarios (filtering, sorting, duplicate badge toggles) to feed into the shared test matrix.
|
||||
- [x] Outline focused checkpoints scenarios (filtering, sorting, duplicate badge toggles) to feed into the shared test matrix.
|
||||
- [ ] Add duplicate badge regression coverage for zero/pending states after API refreshes.
|
||||
|
||||
Maintaining this roadmap alongside code changes will make it easier to append new automated test tasks and update their progress.
|
||||
|
||||
367
tests/frontend/components/pageControls.filtering.test.js
Normal file
367
tests/frontend/components/pageControls.filtering.test.js
Normal file
@@ -0,0 +1,367 @@
|
||||
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||
|
||||
const loadMoreWithVirtualScrollMock = vi.fn();
|
||||
const refreshModelsMock = vi.fn();
|
||||
const fetchCivitaiMetadataMock = vi.fn();
|
||||
const resetAndReloadMock = vi.fn();
|
||||
const getModelApiClientMock = vi.fn();
|
||||
const apiClientMock = {
|
||||
loadMoreWithVirtualScroll: loadMoreWithVirtualScrollMock,
|
||||
refreshModels: refreshModelsMock,
|
||||
fetchCivitaiMetadata: fetchCivitaiMetadataMock,
|
||||
};
|
||||
|
||||
const showToastMock = vi.fn();
|
||||
const updatePanelPositionsMock = vi.fn();
|
||||
const downloadManagerMock = {
|
||||
showDownloadModal: vi.fn(),
|
||||
};
|
||||
|
||||
const sidebarManagerMock = {
|
||||
initialize: vi.fn(async () => {
|
||||
sidebarManagerMock.isInitialized = true;
|
||||
}),
|
||||
refresh: vi.fn(async () => {}),
|
||||
cleanup: vi.fn(),
|
||||
isInitialized: false,
|
||||
};
|
||||
|
||||
const createAlphabetBarMock = vi.fn(() => ({ destroy: vi.fn() }));
|
||||
|
||||
getModelApiClientMock.mockReturnValue(apiClientMock);
|
||||
|
||||
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
||||
getModelApiClient: getModelApiClientMock,
|
||||
resetAndReload: resetAndReloadMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: showToastMock,
|
||||
updatePanelPositions: updatePanelPositionsMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/managers/DownloadManager.js', () => ({
|
||||
downloadManager: downloadManagerMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/components/SidebarManager.js', () => ({
|
||||
sidebarManager: sidebarManagerMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/components/alphabet/index.js', () => ({
|
||||
createAlphabetBar: createAlphabetBarMock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
|
||||
loadMoreWithVirtualScrollMock.mockResolvedValue(undefined);
|
||||
refreshModelsMock.mockResolvedValue(undefined);
|
||||
fetchCivitaiMetadataMock.mockResolvedValue(undefined);
|
||||
resetAndReloadMock.mockResolvedValue(undefined);
|
||||
getModelApiClientMock.mockReturnValue(apiClientMock);
|
||||
|
||||
sidebarManagerMock.isInitialized = false;
|
||||
sidebarManagerMock.initialize.mockImplementation(async () => {
|
||||
sidebarManagerMock.isInitialized = true;
|
||||
});
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, base_models: [] }),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.modelDuplicatesManager;
|
||||
delete global.fetch;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function renderControlsDom(pageKey) {
|
||||
document.body.dataset.page = pageKey;
|
||||
document.body.innerHTML = `
|
||||
<div class="header-search">
|
||||
<div class="search-container">
|
||||
<input id="searchInput" />
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<button id="searchOptionsToggle" class="search-options-toggle"></button>
|
||||
<button id="filterButton" class="search-filter-toggle">
|
||||
<span id="activeFiltersCount" class="filter-badge" style="display: none">0</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="searchOptionsPanel" class="search-options-panel hidden">
|
||||
<button id="closeSearchOptions"></button>
|
||||
<div class="search-option-tag active" data-option="filename"></div>
|
||||
</div>
|
||||
<div id="filterPanel" class="filter-panel hidden">
|
||||
<div id="baseModelTags" class="filter-tags"></div>
|
||||
<div id="modelTagsFilter" class="filter-tags"></div>
|
||||
<button class="clear-filter"></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="actions">
|
||||
<div class="action-buttons">
|
||||
<div class="control-group">
|
||||
<select id="sortSelect">
|
||||
<option value="name:asc">Name Asc</option>
|
||||
<option value="name:desc">Name Desc</option>
|
||||
<option value="date:desc">Date Desc</option>
|
||||
<option value="date:asc">Date Asc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group dropdown-group">
|
||||
<button data-action="refresh" class="dropdown-main"></button>
|
||||
<button class="dropdown-toggle"></button>
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-item" data-action="quick-refresh"></div>
|
||||
<div class="dropdown-item" data-action="full-rebuild"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button data-action="fetch"></button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button data-action="download"></button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button data-action="bulk"></button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button data-action="find-duplicates"></button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="favoriteFilterBtn" class="favorite-filter"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="customFilterIndicator" class="control-group hidden">
|
||||
<div class="filter-active">
|
||||
<i class="fas fa-times-circle clear-filter"></i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('SearchManager filtering scenarios', () => {
|
||||
it.each([
|
||||
['loras'],
|
||||
['checkpoints'],
|
||||
])('updates filters and reloads results for %s page', async (pageKey) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
renderControlsDom(pageKey);
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState(pageKey);
|
||||
const { getCurrentPageState } = stateModule;
|
||||
const { SearchManager } = await import('../../../static/js/managers/SearchManager.js');
|
||||
|
||||
new SearchManager({ page: pageKey, searchDelay: 0 });
|
||||
|
||||
const input = document.getElementById('searchInput');
|
||||
input.value = 'flux';
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(getCurrentPageState().filters.search).toBe('flux');
|
||||
expect(loadMoreWithVirtualScrollMock).toHaveBeenCalledWith(true, false);
|
||||
expect(loadMoreWithVirtualScrollMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterManager tag and base model filters', () => {
|
||||
it.each([
|
||||
['loras'],
|
||||
['checkpoints'],
|
||||
])('toggles tag chips and persists filters for %s page', async (pageKey) => {
|
||||
renderControlsDom(pageKey);
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState(pageKey);
|
||||
const { getCurrentPageState } = stateModule;
|
||||
const { FilterManager } = await import('../../../static/js/managers/FilterManager.js');
|
||||
|
||||
const manager = new FilterManager({ page: pageKey });
|
||||
manager.createTagFilterElements([{ tag: 'style', count: 5 }]);
|
||||
|
||||
const tagChip = document.querySelector('.filter-tag.tag-filter');
|
||||
expect(tagChip).not.toBeNull();
|
||||
|
||||
tagChip.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
await vi.waitFor(() => expect(loadMoreWithVirtualScrollMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(getCurrentPageState().filters.tags).toEqual(['style']);
|
||||
expect(tagChip.classList.contains('active')).toBe(true);
|
||||
expect(document.getElementById('activeFiltersCount').textContent).toBe('1');
|
||||
expect(document.getElementById('activeFiltersCount').style.display).toBe('inline-flex');
|
||||
|
||||
const storageKey = `lora_manager_${pageKey}_filters`;
|
||||
const storedFilters = JSON.parse(localStorage.getItem(storageKey));
|
||||
expect(storedFilters.tags).toEqual(['style']);
|
||||
|
||||
loadMoreWithVirtualScrollMock.mockClear();
|
||||
|
||||
tagChip.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
await vi.waitFor(() => expect(loadMoreWithVirtualScrollMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(getCurrentPageState().filters.tags).toEqual([]);
|
||||
expect(document.getElementById('activeFiltersCount').style.display).toBe('none');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['loras'],
|
||||
['checkpoints'],
|
||||
])('toggles base model chips and reloads %s results', async (pageKey) => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
base_models: [{ name: 'SDXL', count: 2 }],
|
||||
}),
|
||||
});
|
||||
|
||||
renderControlsDom(pageKey);
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState(pageKey);
|
||||
const { getCurrentPageState } = stateModule;
|
||||
const { FilterManager } = await import('../../../static/js/managers/FilterManager.js');
|
||||
|
||||
const manager = new FilterManager({ page: pageKey });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const chip = document.querySelector('[data-base-model="SDXL"]');
|
||||
expect(chip).not.toBeNull();
|
||||
});
|
||||
|
||||
const baseModelChip = document.querySelector('[data-base-model="SDXL"]');
|
||||
|
||||
baseModelChip.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
await vi.waitFor(() => expect(loadMoreWithVirtualScrollMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(getCurrentPageState().filters.baseModel).toEqual(['SDXL']);
|
||||
expect(baseModelChip.classList.contains('active')).toBe(true);
|
||||
|
||||
const storageKey = `lora_manager_${pageKey}_filters`;
|
||||
const storedFilters = JSON.parse(localStorage.getItem(storageKey));
|
||||
expect(storedFilters.baseModel).toEqual(['SDXL']);
|
||||
|
||||
loadMoreWithVirtualScrollMock.mockClear();
|
||||
|
||||
baseModelChip.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
await vi.waitFor(() => expect(loadMoreWithVirtualScrollMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(getCurrentPageState().filters.baseModel).toEqual([]);
|
||||
expect(baseModelChip.classList.contains('active')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageControls favorites, sorting, and duplicates scenarios', () => {
|
||||
it('persists favorites toggle for LoRAs and triggers reload', async () => {
|
||||
renderControlsDom('loras');
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState('loras');
|
||||
const { LorasControls } = await import('../../../static/js/components/controls/LorasControls.js');
|
||||
|
||||
const controls = new LorasControls();
|
||||
|
||||
await controls.toggleFavoritesOnly();
|
||||
|
||||
expect(sessionStorage.getItem('lora_manager_show_favorites_only_loras')).toBe('true');
|
||||
expect(stateModule.getCurrentPageState().showFavoritesOnly).toBe(true);
|
||||
expect(document.getElementById('favoriteFilterBtn').classList.contains('active')).toBe(true);
|
||||
expect(resetAndReloadMock).toHaveBeenCalledWith(true);
|
||||
|
||||
resetAndReloadMock.mockClear();
|
||||
|
||||
await controls.toggleFavoritesOnly();
|
||||
|
||||
expect(sessionStorage.getItem('lora_manager_show_favorites_only_loras')).toBe('false');
|
||||
expect(stateModule.getCurrentPageState().showFavoritesOnly).toBe(false);
|
||||
expect(document.getElementById('favoriteFilterBtn').classList.contains('active')).toBe(false);
|
||||
expect(resetAndReloadMock).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('persists favorites toggle for checkpoints and triggers reload', async () => {
|
||||
renderControlsDom('checkpoints');
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState('checkpoints');
|
||||
const { CheckpointsControls } = await import('../../../static/js/components/controls/CheckpointsControls.js');
|
||||
|
||||
const controls = new CheckpointsControls();
|
||||
|
||||
await controls.toggleFavoritesOnly();
|
||||
|
||||
expect(sessionStorage.getItem('lora_manager_show_favorites_only_checkpoints')).toBe('true');
|
||||
expect(stateModule.getCurrentPageState().showFavoritesOnly).toBe(true);
|
||||
expect(document.getElementById('favoriteFilterBtn').classList.contains('active')).toBe(true);
|
||||
expect(resetAndReloadMock).toHaveBeenCalledWith(true);
|
||||
|
||||
resetAndReloadMock.mockClear();
|
||||
|
||||
await controls.toggleFavoritesOnly();
|
||||
|
||||
expect(sessionStorage.getItem('lora_manager_show_favorites_only_checkpoints')).toBe('false');
|
||||
expect(stateModule.getCurrentPageState().showFavoritesOnly).toBe(false);
|
||||
expect(document.getElementById('favoriteFilterBtn').classList.contains('active')).toBe(false);
|
||||
expect(resetAndReloadMock).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('saves sort selection and reloads models', async () => {
|
||||
renderControlsDom('loras');
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState('loras');
|
||||
const { LorasControls } = await import('../../../static/js/components/controls/LorasControls.js');
|
||||
|
||||
new LorasControls();
|
||||
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
sortSelect.value = 'date:asc';
|
||||
sortSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
await vi.waitFor(() => expect(resetAndReloadMock).toHaveBeenCalledTimes(1));
|
||||
expect(localStorage.getItem('lora_manager_loras_sort')).toBe('date:asc');
|
||||
expect(stateModule.getCurrentPageState().sortBy).toBe('date:asc');
|
||||
});
|
||||
|
||||
it('converts legacy sort preference on initialization', async () => {
|
||||
localStorage.setItem('loras_sort', 'date');
|
||||
|
||||
renderControlsDom('loras');
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState('loras');
|
||||
const { LorasControls } = await import('../../../static/js/components/controls/LorasControls.js');
|
||||
|
||||
new LorasControls();
|
||||
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
expect(sortSelect.value).toBe('date:desc');
|
||||
expect(stateModule.getCurrentPageState().sortBy).toBe('date:desc');
|
||||
});
|
||||
|
||||
it('updates duplicate badge after refresh and toggles duplicate mode from controls', async () => {
|
||||
renderControlsDom('checkpoints');
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState('checkpoints');
|
||||
const { CheckpointsControls } = await import('../../../static/js/components/controls/CheckpointsControls.js');
|
||||
|
||||
const controls = new CheckpointsControls();
|
||||
|
||||
const toggleDuplicateMode = vi.fn();
|
||||
const updateDuplicatesBadgeAfterRefresh = vi.fn();
|
||||
window.modelDuplicatesManager = {
|
||||
toggleDuplicateMode,
|
||||
updateDuplicatesBadgeAfterRefresh,
|
||||
};
|
||||
|
||||
await controls.refreshModels(true);
|
||||
expect(refreshModelsMock).toHaveBeenCalledWith(true);
|
||||
expect(updateDuplicatesBadgeAfterRefresh).toHaveBeenCalledTimes(1);
|
||||
|
||||
const duplicateButton = document.querySelector('[data-action="find-duplicates"]');
|
||||
duplicateButton.click();
|
||||
expect(toggleDuplicateMode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user