mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
2bc9860b24
Add per-model-type filename templates ({model_name}, {version_name},
{base_model}, {author}, {first_tag}, {hash_short}, {original_name}) so
downloaded files get informative names instead of e.g. V1.safetensors.
Empty template keeps the current filename (opt-in, off by default).
- apply template automatically after downloads; rename conflicts keep
the original name and never fail the download
- record original_file_name in metadata on rename for traceability
- bulk apply via GET|POST /api/lm/{prefix}/apply-filename-template with
WebSocket progress, sharing the auto-organize lock
- settings UI lives in the new Organization tab with validation, live
preview, and per-type 'apply to library' actions
277 lines
9.5 KiB
JavaScript
277 lines
9.5 KiB
JavaScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
vi.mock('../../../static/js/managers/ModalManager.js', () => ({
|
|
modalManager: {
|
|
closeModal: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
|
showToast: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../static/js/state/index.js', () => {
|
|
const settings = {};
|
|
return {
|
|
state: {
|
|
global: {
|
|
settings,
|
|
},
|
|
},
|
|
createDefaultSettings: () => ({
|
|
language: 'en',
|
|
download_filename_templates: { lora: '', checkpoint: '', embedding: '' },
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
|
resetAndReload: vi.fn(),
|
|
getModelApiClient: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../static/js/utils/constants.js', () => ({
|
|
DOWNLOAD_PATH_TEMPLATES: {},
|
|
DEFAULT_PATH_TEMPLATES: {},
|
|
MAPPABLE_BASE_MODELS: [],
|
|
PATH_TEMPLATE_PLACEHOLDERS: [],
|
|
FILENAME_TEMPLATE_PLACEHOLDERS: [
|
|
'{model_name}',
|
|
'{version_name}',
|
|
'{base_model}',
|
|
'{author}',
|
|
'{first_tag}',
|
|
'{hash_short}',
|
|
'{original_name}',
|
|
],
|
|
DEFAULT_FILENAME_TEMPLATES: { lora: '', checkpoint: '', embedding: '' },
|
|
DEFAULT_PRIORITY_TAG_CONFIG: {
|
|
lora: 'character, style',
|
|
checkpoint: 'base, guide',
|
|
embedding: 'hint',
|
|
},
|
|
getMappableBaseModelsDynamic: () => [],
|
|
}));
|
|
|
|
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
|
translate: (key, params, fallback) => {
|
|
if (params && fallback) {
|
|
return fallback.replace(/\{(\w+)\}/g, (match, name) => params[name] ?? match);
|
|
}
|
|
return fallback ?? '';
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../static/js/i18n/index.js', () => ({
|
|
i18n: {
|
|
getCurrentLocale: () => 'en',
|
|
setLanguage: vi.fn().mockResolvedValue(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../static/js/components/shared/ModelCard.js', () => ({
|
|
configureModelCardVideo: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../static/js/managers/BannerService.js', () => ({
|
|
bannerService: {
|
|
registerBanner: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import { SettingsManager } from '../../../static/js/managers/SettingsManager.js';
|
|
import { state } from '../../../static/js/state/index.js';
|
|
import { showToast } from '../../../static/js/utils/uiHelpers.js';
|
|
import { resetAndReload, getModelApiClient } from '../../../static/js/api/modelApiFactory.js';
|
|
|
|
const createManager = () => {
|
|
const initSettingsSpy = vi
|
|
.spyOn(SettingsManager.prototype, 'initializeSettings')
|
|
.mockResolvedValue();
|
|
const initializeSpy = vi
|
|
.spyOn(SettingsManager.prototype, 'initialize')
|
|
.mockImplementation(() => {});
|
|
|
|
const manager = new SettingsManager();
|
|
|
|
initSettingsSpy.mockRestore();
|
|
initializeSpy.mockRestore();
|
|
|
|
return manager;
|
|
};
|
|
|
|
const appendFilenameTemplateUi = (modelType = 'lora') => {
|
|
document.body.innerHTML = `
|
|
<input id="${modelType}FilenameTemplate" />
|
|
<div id="${modelType}FilenameValidation"></div>
|
|
<div id="${modelType}FilenamePreview"></div>
|
|
<button id="${modelType}ApplyFilenameTemplate" type="button"></button>
|
|
`;
|
|
};
|
|
|
|
describe('SettingsManager filename templates', () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.clearAllMocks();
|
|
state.global.settings = {
|
|
download_filename_templates: { lora: '', checkpoint: '', embedding: '' },
|
|
};
|
|
});
|
|
|
|
it('treats an empty template as valid (keep original filename)', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
expect(manager.validateFilenameTemplate('lora', '')).toBe(true);
|
|
|
|
const validation = document.getElementById('loraFilenameValidation');
|
|
expect(validation.classList.contains('valid')).toBe(true);
|
|
expect(validation.textContent).toContain('keep original filename');
|
|
});
|
|
|
|
it('rejects templates with path separators or OS-illegal characters', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
expect(manager.validateFilenameTemplate('lora', '{base_model}/{model_name}')).toBe(false);
|
|
expect(manager.validateFilenameTemplate('lora', 'a:b')).toBe(false);
|
|
|
|
const validation = document.getElementById('loraFilenameValidation');
|
|
expect(validation.classList.contains('invalid')).toBe(true);
|
|
});
|
|
|
|
it('rejects unknown placeholders', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
expect(manager.validateFilenameTemplate('lora', '{bogus}-{model_name}')).toBe(false);
|
|
|
|
const validation = document.getElementById('loraFilenameValidation');
|
|
expect(validation.textContent).toContain('{bogus}');
|
|
});
|
|
|
|
it('accepts a template using only known placeholders', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
const template = '{base_model}-{model_name}-{version_name}-{hash_short}';
|
|
expect(manager.validateFilenameTemplate('lora', template)).toBe(true);
|
|
expect(document.getElementById('loraFilenameValidation').classList.contains('valid')).toBe(true);
|
|
});
|
|
|
|
it('saves a valid template via saveSetting with the merged dict', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
manager.saveSetting = vi.fn().mockResolvedValue();
|
|
|
|
manager.updateFilenameTemplate('lora', '{model_name}');
|
|
|
|
expect(state.global.settings.download_filename_templates.lora).toBe('{model_name}');
|
|
expect(manager.saveSetting).toHaveBeenCalledWith(
|
|
'download_filename_templates',
|
|
{ lora: '{model_name}', checkpoint: '', embedding: '' },
|
|
);
|
|
});
|
|
|
|
it('does not save an invalid template', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
manager.saveSetting = vi.fn().mockResolvedValue();
|
|
|
|
manager.updateFilenameTemplate('lora', '{unknown_placeholder}');
|
|
|
|
expect(state.global.settings.download_filename_templates.lora).toBe('');
|
|
expect(manager.saveSetting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('previews the original filename when the template is empty', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
manager.updateFilenamePreview('lora', '');
|
|
|
|
expect(document.getElementById('loraFilenamePreview').textContent).toBe('V1.safetensors');
|
|
});
|
|
|
|
it('renders a preview with example placeholder values', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
manager.updateFilenamePreview('lora', '{base_model}-{model_name}-{version_name}-{hash_short}');
|
|
|
|
expect(document.getElementById('loraFilenamePreview').textContent)
|
|
.toBe('Flux.1 D-model-name-v3-a1b2c3d4e5.safetensors');
|
|
});
|
|
|
|
it('disables the apply button when the template is empty', () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
|
|
manager.updateFilenameTemplateApplyButton('lora', '');
|
|
expect(document.getElementById('loraApplyFilenameTemplate').disabled).toBe(true);
|
|
|
|
manager.updateFilenameTemplateApplyButton('lora', '{model_name}');
|
|
expect(document.getElementById('loraApplyFilenameTemplate').disabled).toBe(false);
|
|
});
|
|
|
|
it('merges backend download_filename_templates over defaults', () => {
|
|
const manager = createManager();
|
|
|
|
const merged = manager.mergeSettingsWithDefaults({
|
|
download_filename_templates: { lora: '{model_name}' },
|
|
});
|
|
expect(merged.download_filename_templates).toEqual({
|
|
lora: '{model_name}',
|
|
checkpoint: '',
|
|
embedding: '',
|
|
});
|
|
|
|
const fromString = manager.mergeSettingsWithDefaults({
|
|
download_filename_templates: '{"checkpoint":"{hash_short}"}',
|
|
});
|
|
expect(fromString.download_filename_templates).toEqual({
|
|
lora: '',
|
|
checkpoint: '{hash_short}',
|
|
embedding: '',
|
|
});
|
|
});
|
|
|
|
it('shows an info toast and does nothing when applying an empty template', async () => {
|
|
appendFilenameTemplateUi();
|
|
const manager = createManager();
|
|
vi.stubGlobal('confirm', vi.fn(() => true));
|
|
|
|
await manager.applyFilenameTemplate('lora');
|
|
|
|
expect(showToast).toHaveBeenCalledWith('settings.filenameTemplates.emptyTemplateInfo', {}, 'info');
|
|
expect(getModelApiClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('applies the template through the model API client and reloads', async () => {
|
|
appendFilenameTemplateUi();
|
|
state.global.settings.download_filename_templates.lora = '{model_name}';
|
|
const manager = createManager();
|
|
vi.stubGlobal('confirm', vi.fn(() => true));
|
|
const apiClient = { applyFilenameTemplate: vi.fn().mockResolvedValue() };
|
|
getModelApiClient.mockReturnValue(apiClient);
|
|
|
|
await manager.applyFilenameTemplate('lora');
|
|
|
|
expect(getModelApiClient).toHaveBeenCalledWith('loras');
|
|
expect(apiClient.applyFilenameTemplate).toHaveBeenCalledWith();
|
|
expect(resetAndReload).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('does not apply when the confirm dialog is declined', async () => {
|
|
appendFilenameTemplateUi();
|
|
state.global.settings.download_filename_templates.lora = '{model_name}';
|
|
const manager = createManager();
|
|
vi.stubGlobal('confirm', vi.fn(() => false));
|
|
|
|
await manager.applyFilenameTemplate('lora');
|
|
|
|
expect(getModelApiClient).not.toHaveBeenCalled();
|
|
expect(resetAndReload).not.toHaveBeenCalled();
|
|
});
|
|
});
|