Files
ComfyUI-Lora-Manager/tests/frontend/utils/infiniteScroll.test.js

199 lines
6.5 KiB
JavaScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const {
VirtualScrollerMock,
MasonryScrollerMock,
RecipeCardMock,
fetchRecipesPageMock,
getModelApiClientMock,
} = vi.hoisted(() => {
const makeScrollerClass = () => vi.fn(function (options) {
this.options = options;
this.initialize = vi.fn(async () => {});
this.dispose = vi.fn();
this.reset = vi.fn();
this.handlePageUpDown = vi.fn();
});
return {
VirtualScrollerMock: makeScrollerClass(),
MasonryScrollerMock: makeScrollerClass(),
RecipeCardMock: vi.fn(function () {
this.element = document.createElement('div');
}),
fetchRecipesPageMock: vi.fn(async () => ({ items: [], totalItems: 0, hasMore: false })),
getModelApiClientMock: vi.fn(() => ({
fetchModelsPage: vi.fn(async () => ({ items: [], totalItems: 0, hasMore: false })),
})),
};
});
vi.mock('../../../static/js/utils/VirtualScroller.js', () => ({
VirtualScroller: VirtualScrollerMock,
}));
vi.mock('../../../static/js/utils/MasonryScroller.js', () => ({
MasonryScroller: MasonryScrollerMock,
}));
vi.mock('../../../static/js/components/RecipeCard.js', () => ({
RecipeCard: RecipeCardMock,
}));
vi.mock('../../../static/js/api/recipeApi.js', () => ({
fetchRecipesPage: fetchRecipesPageMock,
}));
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
getModelApiClient: getModelApiClientMock,
}));
vi.mock('../../../static/js/components/shared/ModelCard.js', () => ({
createModelCard: vi.fn(() => document.createElement('div')),
setupModelCardEventDelegation: vi.fn(),
}));
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
showToast: vi.fn(),
}));
import {
initializeInfiniteScroll,
recreateVirtualScroll,
} from '../../../static/js/utils/infiniteScroll.js';
import { state } from '../../../static/js/state/index.js';
function setupPageDom() {
const pageContent = document.createElement('div');
pageContent.className = 'page-content';
const container = document.createElement('div');
container.className = 'container';
pageContent.appendChild(container);
document.body.appendChild(pageContent);
const grid = document.createElement('div');
vi.spyOn(document, 'getElementById').mockImplementation((id) =>
id === 'recipeGrid' || id === 'modelGrid' ? grid : null);
return { grid };
}
describe('infiniteScroll scroller class branching', () => {
let originalSettings;
beforeEach(() => {
originalSettings = state.global.settings;
state.global.settings = { ...originalSettings };
if (state.pages.recipes) {
state.pages.recipes.duplicatesMode = false;
}
state.virtualScroller = null;
state.keyboardNavHandler = null;
setupPageDom();
});
afterEach(() => {
state.global.settings = originalSettings;
state.virtualScroller = null;
state.keyboardNavHandler = null;
document.body.innerHTML = '';
vi.restoreAllMocks();
vi.clearAllMocks();
});
it('constructs MasonryScroller for the recipes page when recipes_layout is masonry', async () => {
state.global.settings.recipes_layout = 'masonry';
await initializeInfiniteScroll('recipes');
expect(MasonryScrollerMock).toHaveBeenCalledTimes(1);
expect(VirtualScrollerMock).not.toHaveBeenCalled();
expect(state.virtualScroller).toBeInstanceOf(MasonryScrollerMock);
});
it('constructs VirtualScroller for the recipes page when recipes_layout is grid', async () => {
state.global.settings.recipes_layout = 'grid';
await initializeInfiniteScroll('recipes');
expect(VirtualScrollerMock).toHaveBeenCalledTimes(1);
expect(MasonryScrollerMock).not.toHaveBeenCalled();
expect(state.virtualScroller).toBeInstanceOf(VirtualScrollerMock);
});
it('falls back to the grid branch when recipes_layout is missing', async () => {
delete state.global.settings.recipes_layout;
await expect(initializeInfiniteScroll('recipes')).resolves.toBeUndefined();
expect(VirtualScrollerMock).toHaveBeenCalledTimes(1);
expect(MasonryScrollerMock).not.toHaveBeenCalled();
});
it('always constructs VirtualScroller for the loras page regardless of recipes_layout', async () => {
state.global.settings.recipes_layout = 'masonry';
await initializeInfiniteScroll('loras');
expect(VirtualScrollerMock).toHaveBeenCalledTimes(1);
expect(MasonryScrollerMock).not.toHaveBeenCalled();
});
});
describe('recreateVirtualScroll', () => {
let originalSettings;
beforeEach(() => {
originalSettings = state.global.settings;
state.global.settings = { ...originalSettings };
if (state.pages.recipes) {
state.pages.recipes.duplicatesMode = false;
}
state.virtualScroller = null;
state.keyboardNavHandler = null;
setupPageDom();
});
afterEach(() => {
state.global.settings = originalSettings;
state.virtualScroller = null;
state.keyboardNavHandler = null;
document.body.innerHTML = '';
vi.restoreAllMocks();
vi.clearAllMocks();
});
it('cleans up keyboard navigation, disposes the old scroller, and rebuilds with the new layout', async () => {
state.global.settings.recipes_layout = 'grid';
await initializeInfiniteScroll('recipes');
const oldScroller = state.virtualScroller;
const oldKeyboardHandler = state.keyboardNavHandler;
expect(oldKeyboardHandler).toBeTypeOf('function');
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
state.global.settings.recipes_layout = 'masonry';
await recreateVirtualScroll('recipes');
// cleanupKeyboardNavigation removed the previous document keydown listener
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', oldKeyboardHandler);
// Old scroller disposed and replaced by a new masonry instance
expect(oldScroller.dispose).toHaveBeenCalledTimes(1);
expect(MasonryScrollerMock).toHaveBeenCalledTimes(1);
expect(state.virtualScroller).toBeInstanceOf(MasonryScrollerMock);
expect(state.virtualScroller).not.toBe(oldScroller);
// A fresh keyboard navigation listener was registered for the new instance
expect(state.keyboardNavHandler).toBeTypeOf('function');
expect(state.keyboardNavHandler).not.toBe(oldKeyboardHandler);
});
it('works when there is no existing virtual scroller', async () => {
state.global.settings.recipes_layout = 'masonry';
await expect(recreateVirtualScroll('recipes')).resolves.toBeUndefined();
expect(MasonryScrollerMock).toHaveBeenCalledTimes(1);
expect(state.virtualScroller).toBeInstanceOf(MasonryScrollerMock);
});
});