From b47dde87e4b4b2db4b861487d7086f633380e40b Mon Sep 17 00:00:00 2001 From: Will Miao Date: Fri, 31 Jul 2026 10:07:52 +0800 Subject: [PATCH] fix(settings): suppress error toasts when optional model roots are empty --- static/js/managers/SettingsManager.js | 55 ++++++--- .../managers/settingsManager.library.test.js | 112 ++++++++++++++++++ 2 files changed, 150 insertions(+), 17 deletions(-) diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index c45c2a46..bc3c19fd 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1517,11 +1517,20 @@ export class SettingsManager { return data; } - async loadLoraRoots() { - try { - const defaultLoraRootSelect = document.getElementById('defaultLoraRoot'); - if (!defaultLoraRootSelect) return; + showNoRootsPlaceholder(select) { + select.innerHTML = ''; + const option = document.createElement('option'); + option.value = ''; + option.textContent = translate('settings.folderSettings.noDefault', {}, 'No Default'); + select.appendChild(option); + select.disabled = true; + } + async loadLoraRoots() { + const defaultLoraRootSelect = document.getElementById('defaultLoraRoot'); + if (!defaultLoraRootSelect) return; + + try { // Fetch lora roots const response = await fetch('/api/lm/loras/roots'); if (!response.ok) { @@ -1530,10 +1539,12 @@ export class SettingsManager { const data = await response.json(); if (!data.roots || data.roots.length === 0) { - throw new Error('No LoRA roots found'); + this.showNoRootsPlaceholder(defaultLoraRootSelect); + return; } defaultLoraRootSelect.innerHTML = ''; + defaultLoraRootSelect.disabled = false; // Add options for each root data.roots.forEach(root => { @@ -1548,15 +1559,16 @@ export class SettingsManager { } catch (error) { console.error('Error loading LoRA roots:', error); + this.showNoRootsPlaceholder(defaultLoraRootSelect); showToast('toast.settings.loraRootsFailed', { message: error.message }, 'error'); } } async loadCheckpointRoots() { - try { - const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot'); - if (!defaultCheckpointRootSelect) return; + const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot'); + if (!defaultCheckpointRootSelect) return; + try { // Fetch checkpoint roots (checkpoint paths only, not unet) const response = await fetch('/api/lm/checkpoints/checkpoints_roots'); if (!response.ok) { @@ -1565,10 +1577,12 @@ export class SettingsManager { const data = await response.json(); if (!data.roots || data.roots.length === 0) { - throw new Error('No checkpoint roots found'); + this.showNoRootsPlaceholder(defaultCheckpointRootSelect); + return; } defaultCheckpointRootSelect.innerHTML = ''; + defaultCheckpointRootSelect.disabled = false; // Add options for each root data.roots.forEach(root => { @@ -1583,15 +1597,16 @@ export class SettingsManager { } catch (error) { console.error('Error loading checkpoint roots:', error); + this.showNoRootsPlaceholder(defaultCheckpointRootSelect); showToast('toast.settings.checkpointRootsFailed', { message: error.message }, 'error'); } } async loadUnetRoots() { - try { - const defaultUnetRootSelect = document.getElementById('defaultUnetRoot'); - if (!defaultUnetRootSelect) return; + const defaultUnetRootSelect = document.getElementById('defaultUnetRoot'); + if (!defaultUnetRootSelect) return; + try { // Fetch unet roots (diffusion model paths only) const response = await fetch('/api/lm/checkpoints/unet_roots'); if (!response.ok) { @@ -1600,10 +1615,12 @@ export class SettingsManager { const data = await response.json(); if (!data.roots || data.roots.length === 0) { - throw new Error('No diffusion model roots found'); + this.showNoRootsPlaceholder(defaultUnetRootSelect); + return; } defaultUnetRootSelect.innerHTML = ''; + defaultUnetRootSelect.disabled = false; // Add options for each root data.roots.forEach(root => { @@ -1618,15 +1635,16 @@ export class SettingsManager { } catch (error) { console.error('Error loading diffusion model roots:', error); + this.showNoRootsPlaceholder(defaultUnetRootSelect); showToast('toast.settings.unetRootsFailed', { message: error.message }, 'error'); } } async loadEmbeddingRoots() { - try { - const defaultEmbeddingRootSelect = document.getElementById('defaultEmbeddingRoot'); - if (!defaultEmbeddingRootSelect) return; + const defaultEmbeddingRootSelect = document.getElementById('defaultEmbeddingRoot'); + if (!defaultEmbeddingRootSelect) return; + try { // Fetch embedding roots const response = await fetch('/api/lm/embeddings/roots'); if (!response.ok) { @@ -1635,10 +1653,12 @@ export class SettingsManager { const data = await response.json(); if (!data.roots || data.roots.length === 0) { - throw new Error('No embedding roots found'); + this.showNoRootsPlaceholder(defaultEmbeddingRootSelect); + return; } defaultEmbeddingRootSelect.innerHTML = ''; + defaultEmbeddingRootSelect.disabled = false; // Add options for each root data.roots.forEach(root => { @@ -1653,6 +1673,7 @@ export class SettingsManager { } catch (error) { console.error('Error loading embedding roots:', error); + this.showNoRootsPlaceholder(defaultEmbeddingRootSelect); showToast('toast.settings.embeddingRootsFailed', { message: error.message }, 'error'); } } diff --git a/tests/frontend/managers/settingsManager.library.test.js b/tests/frontend/managers/settingsManager.library.test.js index f14c470c..714a7ec3 100644 --- a/tests/frontend/managers/settingsManager.library.test.js +++ b/tests/frontend/managers/settingsManager.library.test.js @@ -106,6 +106,118 @@ afterEach(() => { }); }); +describe('SettingsManager root selects', () => { + const rootCases = [ + { + method: 'loadLoraRoots', + selectId: 'defaultLoraRoot', + endpoint: '/api/lm/loras/roots', + errorKey: 'toast.settings.loraRootsFailed', + }, + { + method: 'loadCheckpointRoots', + selectId: 'defaultCheckpointRoot', + endpoint: '/api/lm/checkpoints/checkpoints_roots', + errorKey: 'toast.settings.checkpointRootsFailed', + }, + { + method: 'loadUnetRoots', + selectId: 'defaultUnetRoot', + endpoint: '/api/lm/checkpoints/unet_roots', + errorKey: 'toast.settings.unetRootsFailed', + }, + { + method: 'loadEmbeddingRoots', + selectId: 'defaultEmbeddingRoot', + endpoint: '/api/lm/embeddings/roots', + errorKey: 'toast.settings.embeddingRootsFailed', + }, + ]; + + const appendRootSelect = (id) => { + const select = document.createElement('select'); + select.id = id; + document.body.appendChild(select); + return select; + }; + + it.each(rootCases)( + 'populates the $method select with roots and keeps it enabled', + async ({ method, selectId, endpoint }) => { + const manager = createManager(); + const select = appendRootSelect(selectId); + select.disabled = true; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + success: true, + roots: ['/models/root-a', '/models/root-b'], + }), + }); + + await manager[method](); + + expect(global.fetch).toHaveBeenCalledWith(endpoint); + expect(Array.from(select.options).map(option => option.value)).toEqual([ + '/models/root-a', + '/models/root-b', + ]); + expect(select.disabled).toBe(false); + expect(showToast).not.toHaveBeenCalled(); + } + ); + + it.each(rootCases)( + 'shows a placeholder and no error toast when $method has empty roots', + async ({ method, selectId, endpoint }) => { + const manager = createManager(); + const select = appendRootSelect(selectId); + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + success: true, + roots: [], + }), + }); + + await manager[method](); + + expect(global.fetch).toHaveBeenCalledWith(endpoint); + expect(select.options).toHaveLength(1); + expect(select.options[0].value).toBe(''); + expect(select.options[0].textContent).toBe('No Default'); + expect(select.disabled).toBe(true); + expect(showToast).not.toHaveBeenCalled(); + } + ); + + it.each(rootCases)( + 'shows an error toast when the $method roots request fails', + async ({ method, selectId, errorKey }) => { + const manager = createManager(); + const select = appendRootSelect(selectId); + + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + }); + + await manager[method](); + + expect(select.options).toHaveLength(1); + expect(select.options[0].value).toBe(''); + expect(select.disabled).toBe(true); + expect(showToast).toHaveBeenCalledWith( + errorKey, + expect.objectContaining({ message: expect.any(String) }), + 'error', + ); + } + ); +}); + describe('SettingsManager library controls', () => { it('loads libraries and populates the select', async () => { const manager = createManager();