mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-10 03:22:12 -03:00
feat(recipes): add 'Has Workflow' filter to the recipes filter panel
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../../static/js/state/index.js', () => ({
|
||||
getCurrentPageState: vi.fn(() => ({
|
||||
filters: {},
|
||||
})),
|
||||
state: {
|
||||
currentPageType: 'recipes',
|
||||
loadingManager: {
|
||||
showSimpleLoading: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: vi.fn(),
|
||||
updatePanelPositions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
||||
getModelApiClient: vi.fn(() => ({
|
||||
loadMoreWithVirtualScroll: vi.fn().mockResolvedValue(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/storageHelpers.js', () => ({
|
||||
getStorageItem: vi.fn(),
|
||||
setStorageItem: vi.fn(),
|
||||
removeStorageItem: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
||||
translate: vi.fn((key, _params, fallback) => fallback || key),
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/managers/FilterPresetManager.js', () => ({
|
||||
FilterPresetManager: vi.fn().mockImplementation(() => ({
|
||||
renderPresets: vi.fn(),
|
||||
saveActivePreset: vi.fn(),
|
||||
restoreActivePreset: vi.fn(),
|
||||
updateAddButtonState: vi.fn(),
|
||||
hasEmptyWildcardResult: vi.fn(() => false),
|
||||
})),
|
||||
EMPTY_WILDCARD_MARKER: '__EMPTY_WILDCARD_RESULT__',
|
||||
}));
|
||||
|
||||
import { FilterManager } from '../../../static/js/managers/FilterManager.js';
|
||||
import { getStorageItem } from '../../../static/js/utils/storageHelpers.js';
|
||||
|
||||
describe('FilterManager - Has Workflow', () => {
|
||||
let manager;
|
||||
let mockFilterPanel;
|
||||
let mockActiveFiltersCount;
|
||||
|
||||
function createHasWorkflowTag() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'hasWorkflowTags';
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'filter-tag has-workflow-tag';
|
||||
container.appendChild(tag);
|
||||
document.body.appendChild(container);
|
||||
return tag;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
getStorageItem.mockReturnValue(undefined);
|
||||
document.body.innerHTML = '';
|
||||
|
||||
mockFilterPanel = document.createElement('div');
|
||||
mockFilterPanel.id = 'filterPanel';
|
||||
mockFilterPanel.classList.add('hidden');
|
||||
document.body.appendChild(mockFilterPanel);
|
||||
|
||||
mockActiveFiltersCount = document.createElement('span');
|
||||
createHasWorkflowTag();
|
||||
|
||||
const originalGetElementById = document.getElementById;
|
||||
document.getElementById = vi.fn((id) => {
|
||||
if (id === 'filterPanel') return mockFilterPanel;
|
||||
if (id === 'filterButton') return document.createElement('button');
|
||||
if (id === 'activeFiltersCount') return mockActiveFiltersCount;
|
||||
if (id === 'baseModelTags') return document.createElement('div');
|
||||
if (id === 'modelTypeTags') return document.createElement('div');
|
||||
return originalGetElementById.call(document, id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeFilters', () => {
|
||||
it('should default to false on the recipes page', () => {
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
expect(manager.filters.hasWorkflow).toBe(false);
|
||||
});
|
||||
|
||||
it('should restore a saved true value from storage', () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: true,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
expect(manager.filters.hasWorkflow).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce a truthy stored value to boolean', () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: 1,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
expect(manager.filters.hasWorkflow).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasActiveFilters', () => {
|
||||
it('should be inactive when hasWorkflow is off', () => {
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
expect(manager.hasActiveFilters()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be active when hasWorkflow is on', () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: true,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
expect(manager.hasActiveFilters()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateActiveFiltersCount', () => {
|
||||
it('should count hasWorkflow as one active filter', () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: true,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
expect(mockActiveFiltersCount.textContent).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chip interaction', () => {
|
||||
it('should activate the filter when the chip is clicked', async () => {
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
const tag = document.querySelector('.has-workflow-tag');
|
||||
expect(tag.classList.contains('active')).toBe(false);
|
||||
|
||||
tag.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(manager.filters.hasWorkflow).toBe(true);
|
||||
expect(tag.classList.contains('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('should deactivate the filter when the chip is clicked again', async () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: true,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
const tag = document.querySelector('.has-workflow-tag');
|
||||
// Restored state should mark the chip active
|
||||
expect(tag.classList.contains('active')).toBe(true);
|
||||
|
||||
tag.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(manager.filters.hasWorkflow).toBe(false);
|
||||
expect(tag.classList.contains('active')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloneFilters', () => {
|
||||
it('should include hasWorkflow in cloned filters', () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: true,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
const cloned = manager.cloneFilters();
|
||||
|
||||
expect(cloned.hasWorkflow).toBe(true);
|
||||
});
|
||||
|
||||
it('should clone an unset hasWorkflow as false', () => {
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
|
||||
const cloned = manager.cloneFilters();
|
||||
|
||||
expect(cloned.hasWorkflow).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearFilters', () => {
|
||||
it('should reset hasWorkflow to false', () => {
|
||||
getStorageItem.mockReturnValue({
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
hasWorkflow: true,
|
||||
});
|
||||
|
||||
manager = new FilterManager({ page: 'recipes' });
|
||||
expect(manager.filters.hasWorkflow).toBe(true);
|
||||
|
||||
manager.clearFilters();
|
||||
|
||||
expect(manager.filters.hasWorkflow).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -758,6 +758,31 @@ async def test_list_recipes_ignores_invalid_lora_availability_values(
|
||||
assert "lora_availability" not in filters
|
||||
|
||||
|
||||
async def test_list_recipes_passes_has_workflow_filter(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||
response = await harness.client.get("/api/lm/recipes?has_workflow=true")
|
||||
payload = await response.json()
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["items"] == []
|
||||
assert harness.scanner.last_paginated_params is not None
|
||||
filters = harness.scanner.last_paginated_params["filters"]
|
||||
assert filters["has_workflow"] is True
|
||||
|
||||
|
||||
async def test_list_recipes_ignores_non_true_has_workflow(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||
response = await harness.client.get("/api/lm/recipes?has_workflow=bogus")
|
||||
assert response.status == 200
|
||||
assert harness.scanner.last_paginated_params is not None
|
||||
filters = harness.scanner.last_paginated_params["filters"]
|
||||
assert "has_workflow" not in filters
|
||||
|
||||
|
||||
async def test_get_recipes_for_checkpoint(monkeypatch, tmp_path: Path) -> None:
|
||||
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||
harness.scanner.checkpoint_lookup["abc123"] = [
|
||||
|
||||
@@ -2064,6 +2064,52 @@ async def test_get_paginated_data_filters_by_favorite(recipe_scanner):
|
||||
assert len(result_fav_false["items"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_filters_by_has_workflow(recipe_scanner):
|
||||
scanner, _ = recipe_scanner
|
||||
|
||||
await scanner.add_recipe(
|
||||
{
|
||||
"id": "no-workflow",
|
||||
"file_path": "path/no-workflow.png",
|
||||
"title": "No Workflow Recipe",
|
||||
"modified": 1.0,
|
||||
"created_date": 1.0,
|
||||
"loras": [],
|
||||
"has_workflow": False,
|
||||
}
|
||||
)
|
||||
|
||||
await scanner.add_recipe(
|
||||
{
|
||||
"id": "with-workflow",
|
||||
"file_path": "path/with-workflow.png",
|
||||
"title": "Workflow Recipe",
|
||||
"modified": 2.0,
|
||||
"created_date": 2.0,
|
||||
"loras": [],
|
||||
"has_workflow": True,
|
||||
}
|
||||
)
|
||||
|
||||
await asyncio.sleep(0)
|
||||
await _wait_for_resort(scanner)
|
||||
|
||||
result_all = await scanner.get_paginated_data(page=1, page_size=10)
|
||||
assert len(result_all["items"]) == 2
|
||||
|
||||
result_wf = await scanner.get_paginated_data(
|
||||
page=1, page_size=10, filters={"has_workflow": True}
|
||||
)
|
||||
assert len(result_wf["items"]) == 1
|
||||
assert result_wf["items"][0]["id"] == "with-workflow"
|
||||
|
||||
result_wf_false = await scanner.get_paginated_data(
|
||||
page=1, page_size=10, filters={"has_workflow": False}
|
||||
)
|
||||
assert len(result_wf_false["items"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_filters_by_base_model_unknown_bucket(recipe_scanner):
|
||||
scanner, _ = recipe_scanner
|
||||
|
||||
Reference in New Issue
Block a user