Files
ComfyUI-Lora-Manager/tests/frontend/pages/otherDisabledPage.test.js
T
Will Miao 84146b62fd feat(other-models): announce the feature only when folders are available
Other Models management is opt-in and its folders come from
folder_paths.get_folder_paths(). In plugin mode ComfyUI registers vae,
upscale_models, text_encoders, clip_vision and controlnet out of the box, so
enabling the feature works immediately. Standalone only knows the keys present
in settings.json.folder_paths, and that file is edited by hand - there is no UI
for those keys - so a standalone user who followed the announcement banner
reached "Enable Other Models" and then an empty page.

Gate the announcement on the capability instead of on how the process was
started:

- Config.get_other_models_availability() probes every canonical other key
  (legacy clip collapses into text_encoders where the host exposes
  map_legacy) and reports which sub_types resolve to a folder that exists on
  disk. It deliberately ignores enable_other_models: the question is "could
  this work here at all?". An empty folder counts, because CivitAI downloads
  can target it.
- /api/lm/settings exposes it as the derived, non-persisted
  other_models_paths_available flag; a probe failure yields null and the
  banner fails open.
- BannerService only registers the announcement when the flag is not false.
  `=== false` (not falsy) keeps a cached/older payload working, and nothing is
  written to dismissed_banners, so the banner can return once folders exist.
- The Other page grows an "enabled but nothing to scan" empty state driven by
  config.other_roots, showing the settings.json snippet for standalone and a
  pointer to ComfyUI model paths otherwise, plus an Open Settings action. It
  also covers the corner where only a non-default sub_type has a folder.

Translate the six other.noPaths.* keys into all nine locales and record the
new "folder key" / "on disk" terminology in the i18n guidelines.

Backend tests and pytest tests/i18n could not run in this environment (no
pytest/platformdirs); the probe was exercised against a stubbed folder_paths.
Frontend: 120 files / 1101 JS tests passed.
2026-09-13 21:47:16 +08:00

104 lines
3.2 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
const initializeAppMock = vi.fn();
const showToastMock = vi.fn();
vi.mock('../../../static/js/core.js', () => ({
appCore: {
initialize: initializeAppMock,
},
}));
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
showToast: showToastMock,
}));
describe('Other Models disabled page', () => {
const originalLocation = window.location;
let enableOtherModels;
let initializeOtherDisabledPage;
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
initializeAppMock.mockResolvedValue(undefined);
document.body.innerHTML = [
'<button id="enableOtherModelsBtn"></button>',
'<button id="openOtherModelsSettingsBtn"></button>',
].join('');
Object.defineProperty(window, 'location', {
value: { ...originalLocation, reload: vi.fn() },
configurable: true,
writable: true,
});
({ enableOtherModels, initializeOtherDisabledPage } = await import(
'../../../static/js/other_disabled.js'
));
await initializeOtherDisabledPage();
});
afterEach(() => {
Object.defineProperty(window, 'location', {
value: originalLocation,
configurable: true,
writable: true,
});
delete global.fetch;
delete window.modalManager;
});
it('boots the shared app core so the header stays usable', () => {
expect(initializeAppMock).toHaveBeenCalledTimes(1);
});
it('opens the Library settings from the no-folders state', () => {
const showModal = vi.fn();
window.modalManager = { showModal };
document.getElementById('openOtherModelsSettingsBtn').dispatchEvent(
new MouseEvent('click', { bubbles: true }),
);
expect(showModal).toHaveBeenCalledWith('settingsModal');
});
it('enables Other Models through the settings API and reloads', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true }),
});
await enableOtherModels();
expect(global.fetch).toHaveBeenCalledWith(
'/api/lm/settings',
expect.objectContaining({ method: 'POST' }),
);
expect(JSON.parse(global.fetch.mock.calls[0][1].body)).toEqual({
enable_other_models: true,
});
expect(window.location.reload).toHaveBeenCalledTimes(1);
expect(showToastMock).not.toHaveBeenCalled();
});
it('re-enables the button and toasts when enabling fails', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
json: async () => ({ success: false, error: 'boom' }),
});
await enableOtherModels();
expect(window.location.reload).not.toHaveBeenCalled();
expect(showToastMock).toHaveBeenCalledWith(
'other.disabled.enableFailed',
expect.objectContaining({ message: 'boom' }),
'error',
);
expect(document.getElementById('enableOtherModelsBtn').disabled).toBe(false);
});
});