diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index b85307a..f2e72f9 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -2416,6 +2416,96 @@ test('workflow analysis and loaded model caches stay independent and cloned', () assert.equal(dialog.workflowAnalysisCaches.get(dialog.workflowKey).data.missing_models.length, 1); }); +test('Missing Models filter toggles persist state and rerender once per change', () => { + const bindMissingFilterToggle = eval( + `(${extractMethod(missingBrowserMethodsSource, 'bindMissingFilterToggle')})` + ); + const wireMissingModelsBrowser = eval( + `(${extractMethod(missingBrowserMethodsSource, 'wireMissingModelsBrowser')})` + ); + const window = new Window(); + const previousDocument = globalThis.document; + const previousElement = globalThis.Element; + const previousHTMLElement = globalThis.HTMLElement; + const previousLocalStorage = globalThis.localStorage; + const localStorageValues = new Map(); + const container = window.document.createElement('div'); + container.innerHTML = ` +
+ + + +
+ `; + const renderOptions = []; + const dialog = { + showResolvedModels: false, + showAutoDownloadModels: true, + showInactiveModels: false, + showResolvedModelsStorageKey: 'test_show_resolved_models', + showAutoDownloadModelsStorageKey: 'test_show_auto_download_models', + showInactiveModelsStorageKey: 'test_show_inactive_models', + missingModelsTypeFilterMenuOpen: false, + bindMissingFilterToggle, + wireMissingBrowserSplitter() {}, + wireVisibleMissingModelRows() {}, + setupMissingModelsVirtualizer() {}, + displayMissingModels(_container, _data, options) { + renderOptions.push(options); + }, + }; + + globalThis.document = window.document; + globalThis.Element = window.Element; + globalThis.HTMLElement = window.HTMLElement; + globalThis.localStorage = { + getItem(key) { + return localStorageValues.has(key) ? localStorageValues.get(key) : null; + }, + setItem(key, value) { + localStorageValues.set(key, String(value)); + }, + removeItem(key) { + localStorageValues.delete(key); + }, + }; + + try { + wireMissingModelsBrowser.call(dialog, container, {}, []); + wireMissingModelsBrowser.call(dialog, container, {}, []); + + const resolvedToggle = container.querySelector('#mr-show-resolved-models'); + const autoDownloadToggle = container.querySelector('#mr-show-auto-download-models'); + const inactiveToggle = container.querySelector('#mr-show-inactive-models'); + resolvedToggle.checked = true; + autoDownloadToggle.checked = false; + inactiveToggle.checked = true; + resolvedToggle.dispatchEvent(new window.Event('change')); + autoDownloadToggle.dispatchEvent(new window.Event('change')); + inactiveToggle.dispatchEvent(new window.Event('change')); + + assert.equal(dialog.showResolvedModels, true); + assert.equal(dialog.showAutoDownloadModels, false); + assert.equal(dialog.showInactiveModels, true); + assert.equal(safeStorage.getItem(dialog.showResolvedModelsStorageKey), '1'); + assert.equal(safeStorage.getItem(dialog.showAutoDownloadModelsStorageKey), '0'); + assert.equal(safeStorage.getItem(dialog.showInactiveModelsStorageKey), '1'); + assert.deepEqual(renderOptions, [ + { preserveBrowser: true }, + { preserveBrowser: true }, + { preserveBrowser: true }, + ]); + } finally { + safeStorage.removeItem(dialog.showResolvedModelsStorageKey); + safeStorage.removeItem(dialog.showAutoDownloadModelsStorageKey); + safeStorage.removeItem(dialog.showInactiveModelsStorageKey); + globalThis.document = previousDocument; + globalThis.Element = previousElement; + globalThis.HTMLElement = previousHTMLElement; + globalThis.localStorage = previousLocalStorage; + } +}); + test('node widget changes request a content-preserving Missing Models refresh', async () => { const log = { debug() {} }; const refreshForActiveWorkflowChange = eval( diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index acc7ffe..d185474 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -1083,6 +1083,19 @@ export const missingBrowserMethods = { }); }, + bindMissingFilterToggle(toggle, { boundKey, storageKey, setValue, container, data }) { + if (!toggle || toggle.dataset[boundKey] === 'true') return; + toggle.dataset[boundKey] = 'true'; + toggle.addEventListener('change', () => { + const value = Boolean(toggle.checked); + setValue(value); + try { + safeStorage.setItem(storageKey, value ? '1' : '0'); + } catch (_e) {} + this.displayMissingModels(container, data, { preserveBrowser: true }); + }); + }, + wireMissingModelsBrowser(container, data, sortedMissingModels) { this.wireMissingBrowserSplitter(container); @@ -1161,40 +1174,31 @@ export const missingBrowserMethods = { } const showResolvedToggle = container.querySelector('#mr-show-resolved-models'); - if (showResolvedToggle && showResolvedToggle.dataset.mlResolvedBound !== 'true') { - showResolvedToggle.dataset.mlResolvedBound = 'true'; - showResolvedToggle.addEventListener('change', () => { - this.showResolvedModels = Boolean(showResolvedToggle.checked); - try { - safeStorage.setItem(this.showResolvedModelsStorageKey, this.showResolvedModels ? '1' : '0'); - } catch (_e) {} - this.displayMissingModels(container, data, { preserveBrowser: true }); - }); - } + this.bindMissingFilterToggle(showResolvedToggle, { + boundKey: 'mlResolvedBound', + storageKey: this.showResolvedModelsStorageKey, + setValue: value => { this.showResolvedModels = value; }, + container, + data, + }); const showAutoDownloadToggle = container.querySelector('#mr-show-auto-download-models'); - if (showAutoDownloadToggle && showAutoDownloadToggle.dataset.mlAutoDownloadBound !== 'true') { - showAutoDownloadToggle.dataset.mlAutoDownloadBound = 'true'; - showAutoDownloadToggle.addEventListener('change', () => { - this.showAutoDownloadModels = Boolean(showAutoDownloadToggle.checked); - try { - safeStorage.setItem(this.showAutoDownloadModelsStorageKey, this.showAutoDownloadModels ? '1' : '0'); - } catch (_e) {} - this.displayMissingModels(container, data, { preserveBrowser: true }); - }); - } + this.bindMissingFilterToggle(showAutoDownloadToggle, { + boundKey: 'mlAutoDownloadBound', + storageKey: this.showAutoDownloadModelsStorageKey, + setValue: value => { this.showAutoDownloadModels = value; }, + container, + data, + }); const showInactiveToggle = container.querySelector('#mr-show-inactive-models'); - if (showInactiveToggle && showInactiveToggle.dataset.mlInactiveBound !== 'true') { - showInactiveToggle.dataset.mlInactiveBound = 'true'; - showInactiveToggle.addEventListener('change', () => { - this.showInactiveModels = Boolean(showInactiveToggle.checked); - try { - safeStorage.setItem(this.showInactiveModelsStorageKey, this.showInactiveModels ? '1' : '0'); - } catch (_e) {} - this.displayMissingModels(container, data, { preserveBrowser: true }); - }); - } + this.bindMissingFilterToggle(showInactiveToggle, { + boundKey: 'mlInactiveBound', + storageKey: this.showInactiveModelsStorageKey, + setValue: value => { this.showInactiveModels = value; }, + container, + data, + }); const getCurrentMissingModels = () => ( Array.isArray(this.missingModels)