mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
feat(banners): rotate active banners one at a time with a pager
Stacking every active banner vertically ate header height when several were active at once. Only the highest-priority banner renders now; a ‹ 1/N › pager cycles through the rest, and all active banners are still recorded in the notification-center history so cycled-away ones stay reachable. Newly registered banners preempt the displayed one only when they outrank it. Also fix the startup flow: the restart-required banner (now priority 80) outranks the model-folders setup warning (60), and the setup banner is retired once a non-empty folder path is saved. New banners.pager.* keys translated in all 9 locales.
This commit is contained in:
@@ -43,6 +43,7 @@ describe('BannerService', () => {
|
||||
// Reset banner service state
|
||||
bannerService.banners.clear();
|
||||
bannerService.initialized = false;
|
||||
bannerService.currentBannerIndex = 0;
|
||||
bannerService.recentHistory = []; // Clear history for each test
|
||||
|
||||
// Clear DOM
|
||||
@@ -331,6 +332,116 @@ describe('BannerService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Banner Rotation', () => {
|
||||
const registerTestBanner = (id, priority) => {
|
||||
bannerService.registerBanner(id, {
|
||||
id,
|
||||
title: `Banner ${id}`,
|
||||
content: `Content ${id}`,
|
||||
dismissible: true,
|
||||
priority
|
||||
});
|
||||
};
|
||||
|
||||
const displayedBannerId = () =>
|
||||
document.querySelector('#banner-container .banner-item')
|
||||
?.getAttribute('data-banner-id');
|
||||
|
||||
let dismissedStore;
|
||||
|
||||
beforeEach(() => {
|
||||
dismissedStore = [];
|
||||
storageHelpers.getStorageItem.mockImplementation((key, defaultValue) => {
|
||||
if (key === 'dismissed_banners') {
|
||||
return dismissedStore;
|
||||
}
|
||||
return defaultValue;
|
||||
});
|
||||
storageHelpers.setStorageItem.mockImplementation((key, value) => {
|
||||
if (key === 'dismissed_banners') {
|
||||
dismissedStore = value;
|
||||
}
|
||||
});
|
||||
bannerService.container = document.getElementById('banner-container');
|
||||
bannerService.initialized = true;
|
||||
});
|
||||
|
||||
it('renders only the highest priority banner when multiple are active', () => {
|
||||
registerTestBanner('low', 1);
|
||||
registerTestBanner('high', 10);
|
||||
|
||||
const rendered = document.querySelectorAll('#banner-container .banner-item');
|
||||
expect(rendered).toHaveLength(1);
|
||||
expect(displayedBannerId()).toBe('high');
|
||||
});
|
||||
|
||||
it('shows a pager with position indicator when multiple banners are active', () => {
|
||||
registerTestBanner('a', 1);
|
||||
registerTestBanner('b', 2);
|
||||
|
||||
const pager = document.querySelector('.banner-pager');
|
||||
expect(pager).not.toBeNull();
|
||||
expect(pager.querySelector('.banner-pager-indicator').textContent.trim())
|
||||
.toBe('1 / 2');
|
||||
});
|
||||
|
||||
it('does not show a pager for a single banner', () => {
|
||||
registerTestBanner('only', 1);
|
||||
|
||||
expect(document.querySelector('.banner-pager')).toBeNull();
|
||||
});
|
||||
|
||||
it('cycles to the next banner and wraps around', () => {
|
||||
registerTestBanner('a', 1);
|
||||
registerTestBanner('b', 2);
|
||||
|
||||
document.querySelector('[data-pager="next"]')
|
||||
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(displayedBannerId()).toBe('a');
|
||||
expect(document.querySelector('.banner-pager-indicator').textContent.trim())
|
||||
.toBe('2 / 2');
|
||||
|
||||
document.querySelector('[data-pager="next"]')
|
||||
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(displayedBannerId()).toBe('b');
|
||||
expect(document.querySelector('.banner-pager-indicator').textContent.trim())
|
||||
.toBe('1 / 2');
|
||||
});
|
||||
|
||||
it('cycles backwards with the previous button', () => {
|
||||
registerTestBanner('a', 1);
|
||||
registerTestBanner('b', 2);
|
||||
|
||||
document.querySelector('[data-pager="prev"]')
|
||||
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(displayedBannerId()).toBe('a');
|
||||
});
|
||||
|
||||
it('shows the next banner after the displayed one is dismissed', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
registerTestBanner('a', 1);
|
||||
registerTestBanner('b', 2);
|
||||
expect(displayedBannerId()).toBe('b');
|
||||
|
||||
await bannerService.dismissBanner('b');
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
expect(displayedBannerId()).toBe('a');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('records all active banners in history, not just the displayed one', () => {
|
||||
registerTestBanner('a', 1);
|
||||
registerTestBanner('b', 2);
|
||||
|
||||
const historyIds = bannerService.recentHistory.map(entry => entry.id);
|
||||
expect(historyIds).toEqual(expect.arrayContaining(['a', 'b']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Banner History', () => {
|
||||
const testBanner = {
|
||||
id: 'test-banner',
|
||||
|
||||
@@ -415,4 +415,75 @@ describe('SettingsManager Model Paths section', () => {
|
||||
.filter((id) => id.startsWith('model-paths-restart-'));
|
||||
expect(restartBanners).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('gives the restart banner a higher priority than startup warnings', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
manager.addModelFolderPathRow('loras');
|
||||
document.querySelector('#modelFolderPaths-loras .extra-folder-path-input').value = '/data/loras';
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
|
||||
const restartBanner = Array.from(bannerService.banners.values())
|
||||
.find((banner) => banner.id.startsWith('model-paths-restart-'));
|
||||
// Startup warnings map to 60; the restart cue must outrank them so it
|
||||
// preempts the "model folders need setup" prompt in the banner pager.
|
||||
expect(restartBanner.priority).toBeGreaterThan(60);
|
||||
});
|
||||
|
||||
it('removes the "model folders need setup" startup banner once a path is saved', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
bannerService.registerBanner('startup-missing-model-paths', {
|
||||
id: 'startup-missing-model-paths',
|
||||
title: 'Model folders need setup',
|
||||
content: 'stub',
|
||||
dismissible: false,
|
||||
priority: 60,
|
||||
});
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
manager.addModelFolderPathRow('loras');
|
||||
document.querySelector('#modelFolderPaths-loras .extra-folder-path-input').value = '/data/loras';
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
|
||||
expect(bannerService.banners.has('startup-missing-model-paths')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the setup banner when the saved paths are all empty', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({ folder_paths: { loras: ['/data/loras'] } });
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
bannerService.registerBanner('startup-missing-model-paths', {
|
||||
id: 'startup-missing-model-paths',
|
||||
title: 'Model folders need setup',
|
||||
content: 'stub',
|
||||
dismissible: false,
|
||||
priority: 60,
|
||||
});
|
||||
|
||||
// Clear every row and save: an all-empty path set must not retire the
|
||||
// setup prompt.
|
||||
document.querySelectorAll('#modelFolderPaths-loras .extra-folder-path-input')
|
||||
.forEach((input) => { input.value = ''; });
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
|
||||
expect(manager.saveSetting).toHaveBeenCalled();
|
||||
expect(bannerService.banners.has('startup-missing-model-paths')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user