mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-13 12:52:13 -03:00
Fill the 26 [TODO: Translate] placeholders per locale (24 settings.downloadRootRules.* keys + modals.download.routedByRule.*) across zh-CN, zh-TW, ja, ko, fr, de, es, ru, he, following the i18n guidelines: routing/root/base-model/tag terminology per §2, R3 Latin model types and example token lists kept verbatim, R6 register, per-locale UI-label quoting in routedByRule, and fragment style for the summary/chip strings. The es tag chip uses "Tag" (the full "etiqueta" overflows the badge; the tooltip keeps the full term). Source fix per R9/R7: en.json summary.selectBaseModels/enterTags used U+2026 — now ASCII "..." (JS fallbacks and vitest assertions moved with it; truncateMiddlePath's … stays, it is a path-elision character, not a locale string). Guidelines doc: new §2 "Download root rules feature" terminology subsection + 2026-10 status entry.
747 lines
28 KiB
JavaScript
747 lines
28 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_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
|
resetAndReload: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../static/js/utils/constants.js', () => ({
|
|
DOWNLOAD_PATH_TEMPLATES: {},
|
|
DEFAULT_PATH_TEMPLATES: {},
|
|
MAPPABLE_BASE_MODELS: ['Flux.1 D', 'Pony', 'SDXL 1.0', 'Other'],
|
|
PATH_TEMPLATE_PLACEHOLDERS: {},
|
|
FILENAME_TEMPLATE_PLACEHOLDERS: [],
|
|
DEFAULT_FILENAME_TEMPLATES: { lora: '', checkpoint: '', embedding: '' },
|
|
DEFAULT_PRIORITY_TAG_CONFIG: {
|
|
lora: 'character, style',
|
|
checkpoint: 'base, guide',
|
|
embedding: 'hint',
|
|
},
|
|
getMappableBaseModelsDynamic: () => ['Flux.1 D', 'Pony', 'SDXL 1.0', 'Other'],
|
|
}));
|
|
|
|
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
|
translate: (key, params, fallback) => {
|
|
if (key === 'settings.downloadSkipBaseModels.summary.none') {
|
|
return 'None selected';
|
|
}
|
|
if (key === 'settings.downloadSkipBaseModels.summary.count') {
|
|
return `${params?.count ?? 0} selected`;
|
|
}
|
|
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';
|
|
|
|
const ROOTS = {
|
|
loras: ['/models/loras-main', '/models/lora-archive'],
|
|
checkpoints: ['/models/checkpoints'],
|
|
unet: ['/models/unet-main', '/models/unet-archive'],
|
|
embeddings: [],
|
|
};
|
|
|
|
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();
|
|
|
|
manager.saveSetting = vi.fn().mockResolvedValue();
|
|
manager.downloadRootRuleRoots = ROOTS;
|
|
|
|
return manager;
|
|
};
|
|
|
|
const appendDownloadRootRulesUi = () => {
|
|
document.body.innerHTML = `
|
|
<div id="downloadRootRulesContainer"></div>
|
|
<div id="downloadRootRulesError"></div>
|
|
`;
|
|
};
|
|
|
|
const groupRows = (typeKey) =>
|
|
document.querySelectorAll(
|
|
`#downloadRootRulesContainer .download-root-rule-row[data-rule-type="${typeKey}"]`
|
|
);
|
|
|
|
describe('SettingsManager download root rules', () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.clearAllMocks();
|
|
state.global.settings = {
|
|
download_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
|
};
|
|
});
|
|
|
|
it('normalizes malformed rules out of the setting', () => {
|
|
const manager = createManager();
|
|
|
|
const normalized = manager.normalizeDownloadRootRules({
|
|
loras: [
|
|
{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' },
|
|
{ match_type: 'base_model', match: [], root: '/models/lora-archive' },
|
|
{ match_type: 'base_model', match: ['Pony'], root: '' },
|
|
null,
|
|
'not-a-rule',
|
|
],
|
|
embeddings: 'oops',
|
|
unknownType: [{ match_type: 'base_model', match: ['Pony'], root: '/x' }],
|
|
});
|
|
|
|
expect(normalized).toEqual({
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
});
|
|
|
|
it('renders one group per type and a hint when no roots are configured', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D', 'Pony'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
expect(document.querySelectorAll('.download-root-rule-group')).toHaveLength(4);
|
|
expect(groupRows('loras')).toHaveLength(1);
|
|
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
|
.toBe('Flux.1 D, Pony');
|
|
expect(groupRows('loras')[0].querySelector('select.download-root-rule-root').value)
|
|
.toBe('/models/lora-archive');
|
|
// embeddings has no roots configured
|
|
const embeddingsGroup = document.querySelectorAll('.download-root-rule-group')[3];
|
|
expect(embeddingsGroup.querySelector('.download-root-rules-hint')).not.toBeNull();
|
|
expect(embeddingsGroup.querySelector('.add-mapping-btn')).toBeNull();
|
|
});
|
|
|
|
it('add rule creates a draft and the first base model selection promotes and persists it', async () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
manager.addDownloadRootRule('loras');
|
|
|
|
expect(manager.downloadRootRuleDrafts.loras).toEqual({
|
|
match_type: 'base_model',
|
|
match: [],
|
|
root: '/models/loras-main',
|
|
});
|
|
expect(groupRows('loras')).toHaveLength(1);
|
|
// A second add while a draft exists is a no-op
|
|
manager.addDownloadRootRule('loras');
|
|
expect(groupRows('loras')).toHaveLength(1);
|
|
|
|
// The draft row panel is open; tick a base model checkbox
|
|
const checkbox = groupRows('loras')[0].querySelector(
|
|
'.download-root-rule-panel input[value="Flux.1 D"]'
|
|
);
|
|
checkbox.checked = true;
|
|
checkbox.dispatchEvent(new Event('change'));
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.downloadRootRuleDrafts.loras).toBeUndefined();
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/loras-main' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
expect(state.global.settings.download_root_rules.loras).toHaveLength(1);
|
|
});
|
|
|
|
it('removing a rule persists the updated list', async () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [
|
|
{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' },
|
|
{ match_type: 'base_model', match: ['Pony'], root: '/models/loras-main' },
|
|
],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
manager.removeDownloadRootRule('loras', 0, false);
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [{ match_type: 'base_model', match: ['Pony'], root: '/models/loras-main' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
});
|
|
|
|
it('move down swaps rule order and persists', async () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [
|
|
{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' },
|
|
{ match_type: 'base_model', match: ['Pony'], root: '/models/loras-main' },
|
|
],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
manager.moveDownloadRootRule('loras', 0, 1);
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [
|
|
{ match_type: 'base_model', match: ['Pony'], root: '/models/loras-main' },
|
|
{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' },
|
|
],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
|
|
// Out-of-range moves are no-ops
|
|
manager.saveSetting.mockClear();
|
|
manager.moveDownloadRootRule('loras', 1, 1);
|
|
expect(manager.saveSetting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('unchecking the last base model of a persisted rule removes the rule', async () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
const row = groupRows('loras')[0];
|
|
const checkbox = row.querySelector('.download-root-rule-panel input[value="Flux.1 D"]');
|
|
expect(checkbox.checked).toBe(true);
|
|
checkbox.checked = false;
|
|
checkbox.dispatchEvent(new Event('change'));
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
expect(groupRows('loras')).toHaveLength(0);
|
|
});
|
|
|
|
it('loadDownloadRootRules fetches roots for every type and renders', async () => {
|
|
appendDownloadRootRulesUi();
|
|
const fetchMock = vi.fn().mockImplementation(async (url) => ({
|
|
ok: true,
|
|
json: async () => {
|
|
if (url.includes('loras/roots')) return { roots: ROOTS.loras };
|
|
if (url.includes('checkpoints_roots')) return { roots: ROOTS.checkpoints };
|
|
if (url.includes('unet_roots')) return { roots: ROOTS.unet };
|
|
if (url.includes('embeddings/roots')) return { roots: [] };
|
|
return {};
|
|
},
|
|
}));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
try {
|
|
const manager = createManager();
|
|
manager.downloadRootRuleRoots = null;
|
|
|
|
await manager.loadDownloadRootRules();
|
|
|
|
expect(manager.downloadRootRuleRoots).toEqual(ROOTS);
|
|
expect(document.querySelectorAll('.download-root-rule-group')).toHaveLength(4);
|
|
expect(document.getElementById('downloadRootRulesError').textContent).toBe('');
|
|
} finally {
|
|
vi.unstubAllGlobals();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('SettingsManager download root rules default-root cue', () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.clearAllMocks();
|
|
state.global.settings = {
|
|
download_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
|
};
|
|
});
|
|
|
|
const defaultHintFor = (typeIndex) =>
|
|
document.querySelectorAll('.download-root-rule-group')[typeIndex]
|
|
.querySelector('.download-root-rule-default-hint');
|
|
|
|
it('renders a default hint under the group header when a default root is set', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.default_lora_root = '/models/loras-main';
|
|
const manager = createManager();
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
expect(defaultHintFor(0).textContent)
|
|
.toBe('Default: /models/loras-main — unmatched downloads go here');
|
|
});
|
|
|
|
it('omits the default hint when the type has no roots or no default root', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.default_lora_root = '';
|
|
state.global.settings.default_embedding_root = '/models/embeddings';
|
|
const manager = createManager();
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
// loras: default root is empty -> no hint
|
|
expect(defaultHintFor(0)).toBeNull();
|
|
// embeddings: no roots configured -> noRoots hint only, no default hint
|
|
expect(defaultHintFor(3)).toBeNull();
|
|
});
|
|
|
|
it('marks the default root option with a (default) suffix but keeps the plain value', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.default_lora_root = '/models/lora-archive';
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/loras-main' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
const options = groupRows('loras')[0].querySelectorAll('select.download-root-rule-root option');
|
|
expect([...options].map((o) => o.value)).toEqual(['/models/loras-main', '/models/lora-archive']);
|
|
expect([...options].map((o) => o.textContent)).toEqual([
|
|
'/models/loras-main',
|
|
'/models/lora-archive (default)',
|
|
]);
|
|
});
|
|
|
|
it('compares roots loosely: separators, trailing slashes and case are ignored', () => {
|
|
const manager = createManager();
|
|
|
|
expect(manager.isDownloadRootRuleDefaultRoot('C:\\Models\\Loras\\', 'c:/models/loras')).toBe(true);
|
|
expect(manager.isDownloadRootRuleDefaultRoot('/a/b/', '/a/b')).toBe(true);
|
|
expect(manager.isDownloadRootRuleDefaultRoot('/a/b', '/a/c')).toBe(false);
|
|
expect(manager.isDownloadRootRuleDefaultRoot('/a/b', '')).toBe(false);
|
|
});
|
|
|
|
it('does not render groups before the roots have been fetched', () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.downloadRootRuleRoots = null;
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
expect(document.querySelectorAll('.download-root-rule-group')).toHaveLength(0);
|
|
});
|
|
|
|
it('changing a default root select re-renders the rules editor with the new default', async () => {
|
|
appendDownloadRootRulesUi();
|
|
document.body.insertAdjacentHTML(
|
|
'beforeend',
|
|
'<select id="defaultLoraRoot"><option value="/models/loras-main">a</option><option value="/models/lora-archive" selected>b</option></select>'
|
|
);
|
|
state.global.settings.default_lora_root = '/models/loras-main';
|
|
const manager = createManager();
|
|
manager.applyFrontendSettings = vi.fn();
|
|
// Mimic the real saveSetting, which writes the value into state.
|
|
manager.saveSetting = vi.fn().mockImplementation(async (key, value) => {
|
|
state.global.settings[key] = value;
|
|
});
|
|
|
|
manager.renderDownloadRootRules();
|
|
expect(defaultHintFor(0).textContent)
|
|
.toBe('Default: /models/loras-main — unmatched downloads go here');
|
|
|
|
await manager.saveSelectSetting('defaultLoraRoot', 'default_lora_root');
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('default_lora_root', '/models/lora-archive');
|
|
expect(defaultHintFor(0).textContent)
|
|
.toBe('Default: /models/lora-archive — unmatched downloads go here');
|
|
});
|
|
});
|
|
|
|
|
|
describe('SettingsManager download root rules tag matching', () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.clearAllMocks();
|
|
state.global.settings = {
|
|
download_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
|
};
|
|
});
|
|
|
|
it('normalizes tag rules as free text with case-insensitive dedupe', () => {
|
|
const manager = createManager();
|
|
|
|
const normalized = manager.normalizeDownloadRootRules({
|
|
loras: [
|
|
{ match_type: 'tag', match: [' character ', 'Style', 'CHARACTER'], root: '/models/lora-archive' },
|
|
{ match_type: 'tag', match: [], root: '/models/lora-archive' },
|
|
{ match_type: 'unknown', match: ['x'], root: '/models/lora-archive' },
|
|
{ match_type: 'tag', match: ['character'], root: '' },
|
|
],
|
|
});
|
|
|
|
expect(normalized.loras).toEqual([
|
|
{ match_type: 'tag', match: ['character', 'Style'], root: '/models/lora-archive' },
|
|
]);
|
|
});
|
|
|
|
it('renders a tag rule with a static type chip, summary and tag input', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'tag', match: ['character', 'style'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
const row = groupRows('loras')[0];
|
|
// The match type is fixed at creation: a chip, not a dropdown.
|
|
const chip = row.querySelector('.download-root-rule-match-chip');
|
|
expect(chip.textContent).toBe('Tag');
|
|
expect(chip.title).toBe('Tag');
|
|
expect(row.querySelector('.download-root-rule-match-type')).toBeNull();
|
|
expect(row.querySelector('.download-root-rule-summary').textContent)
|
|
.toBe('character, style');
|
|
expect(row.querySelector('.download-root-rule-tag-input').value)
|
|
.toBe('character, style');
|
|
// No base model checkbox list for tag rules
|
|
expect(row.querySelector('.base-model-skip-list')).toBeNull();
|
|
});
|
|
|
|
it('renders a base model rule chip with the full name as tooltip', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
const chip = groupRows('loras')[0].querySelector('.download-root-rule-match-chip');
|
|
expect(chip.textContent).toBe('BM');
|
|
expect(chip.title).toBe('Base model');
|
|
});
|
|
|
|
it('a tag draft promotes on the first committed tag input', async () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
manager.addDownloadRootRule('loras', 'tag');
|
|
|
|
// Still a draft: nothing persisted yet
|
|
expect(manager.saveSetting).not.toHaveBeenCalled();
|
|
|
|
const tagInput = groupRows('loras')[0].querySelector('.download-root-rule-tag-input');
|
|
tagInput.value = 'character, Style, CHARACTER';
|
|
tagInput.dispatchEvent(new Event('blur'));
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.downloadRootRuleDrafts.loras).toBeUndefined();
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [{ match_type: 'tag', match: ['character', 'Style'], root: '/models/loras-main' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
});
|
|
|
|
it('committing an empty tag list on a persisted rule removes the rule', async () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'tag', match: ['character'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
const tagInput = groupRows('loras')[0].querySelector('.download-root-rule-tag-input');
|
|
tagInput.value = ' , ';
|
|
tagInput.dispatchEvent(new Event('blur'));
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
expect(groupRows('loras')).toHaveLength(0);
|
|
});
|
|
|
|
it('editing tags on a persisted rule parses, dedupes and persists', async () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'tag', match: ['character'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
const tagInput = groupRows('loras')[0].querySelector('.download-root-rule-tag-input');
|
|
tagInput.value = 'realistic, style, REALISTIC';
|
|
tagInput.dispatchEvent(new Event('blur'));
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [{ match_type: 'tag', match: ['realistic', 'style'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe('SettingsManager download root rules row layout', () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.clearAllMocks();
|
|
state.global.settings = {
|
|
download_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
|
};
|
|
});
|
|
|
|
it('offers separate add buttons for base model and tag rules', () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
const buttons = document.querySelectorAll('.download-root-rule-group')[0]
|
|
.querySelectorAll('.download-root-rule-add-row .add-mapping-btn');
|
|
expect(buttons).toHaveLength(2);
|
|
expect(buttons[0].textContent).toContain('Base model rule');
|
|
expect(buttons[1].textContent).toContain('Tag rule');
|
|
|
|
buttons[1].click();
|
|
expect(manager.downloadRootRuleDrafts.loras.match_type).toBe('tag');
|
|
|
|
// While a draft exists both buttons are disabled
|
|
const disabledButtons = document.querySelectorAll('.download-root-rule-group')[0]
|
|
.querySelectorAll('.download-root-rule-add-row .add-mapping-btn');
|
|
expect([...disabledButtons].every((btn) => btn.disabled)).toBe(true);
|
|
});
|
|
|
|
it('base model add button creates a base_model draft', () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
document.querySelectorAll('.download-root-rule-group')[0]
|
|
.querySelectorAll('.download-root-rule-add-row .add-mapping-btn')[0]
|
|
.click();
|
|
|
|
expect(manager.downloadRootRuleDrafts.loras.match_type).toBe('base_model');
|
|
expect(groupRows('loras')[0].querySelector('.download-root-rule-match-chip').textContent)
|
|
.toBe('BM');
|
|
});
|
|
|
|
it('the summary toggle uses a chevron icon with accessible title and aria state', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/loras-main' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
const toggle = groupRows('loras')[0].querySelector('.download-root-rule-toggle');
|
|
expect(toggle.querySelector('.download-root-rule-chevron')).not.toBeNull();
|
|
expect(toggle.querySelector('.base-model-skip-toggle-label')).toBeNull();
|
|
expect(toggle.getAttribute('aria-expanded')).toBe('false');
|
|
expect(toggle.title).toBe('Edit');
|
|
|
|
toggle.click();
|
|
|
|
const reopened = groupRows('loras')[0].querySelector('.download-root-rule-toggle');
|
|
expect(reopened.getAttribute('aria-expanded')).toBe('true');
|
|
expect(reopened.title).toBe('Collapse');
|
|
});
|
|
|
|
it('truncateMiddlePath keeps short paths intact and ellipsizes long ones', () => {
|
|
const manager = createManager();
|
|
|
|
expect(manager.truncateMiddlePath('/models/loras')).toBe('/models/loras');
|
|
|
|
const long = '/models/library/very/deeply/nested/folder/structure/loras';
|
|
const truncated = manager.truncateMiddlePath(long);
|
|
expect(truncated.startsWith(long.slice(0, 24))).toBe(true);
|
|
expect(truncated.endsWith(long.slice(-16))).toBe(true);
|
|
expect(truncated).toContain('…');
|
|
expect(truncated.length).toBe(24 + 1 + 16);
|
|
});
|
|
|
|
it('truncates long root paths in the dropdown but keeps full values and titles', () => {
|
|
appendDownloadRootRulesUi();
|
|
const longRoot = '/models/library/very/deeply/nested/folder/structure/loras';
|
|
const manager = createManager();
|
|
manager.downloadRootRuleRoots = {
|
|
loras: [longRoot],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: longRoot }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
const rootSelect = groupRows('loras')[0].querySelector('select.download-root-rule-root');
|
|
expect(rootSelect.options[0].value).toBe(longRoot);
|
|
expect(rootSelect.options[0].title).toBe(longRoot);
|
|
expect(rootSelect.options[0].textContent).toBe(manager.truncateMiddlePath(longRoot));
|
|
expect(rootSelect.title).toBe(longRoot);
|
|
});
|
|
|
|
it('keeps the (default) suffix after the truncated path', () => {
|
|
appendDownloadRootRulesUi();
|
|
const longRoot = '/models/library/very/deeply/nested/folder/structure/loras';
|
|
const manager = createManager();
|
|
manager.downloadRootRuleRoots = {
|
|
loras: [longRoot],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
state.global.settings.default_lora_root = longRoot;
|
|
state.global.settings.download_root_rules = {
|
|
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: longRoot }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
|
|
manager.renderDownloadRootRules();
|
|
|
|
const option = groupRows('loras')[0]
|
|
.querySelector('select.download-root-rule-root').options[0];
|
|
expect(option.textContent).toBe(`${manager.truncateMiddlePath(longRoot)} (default)`);
|
|
});
|
|
|
|
it('shows actionable placeholder summaries for empty drafts', () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
manager.addDownloadRootRule('loras', 'base_model');
|
|
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
|
.toBe('Select base models...');
|
|
|
|
manager.removeDownloadRootRule('loras', 0, true);
|
|
manager.addDownloadRootRule('loras', 'tag');
|
|
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
|
.toBe('Enter tags...');
|
|
});
|
|
|
|
it('summarizes more than two matches as first two plus a count', () => {
|
|
appendDownloadRootRulesUi();
|
|
state.global.settings.download_root_rules = {
|
|
loras: [
|
|
{ match_type: 'base_model', match: ['Flux.1 D', 'Wan Video', 'Pony', 'SDXL 1.0'], root: '/models/loras-main' },
|
|
],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
};
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
|
.toBe('Flux.1 D, Wan Video, +2 more');
|
|
});
|
|
});
|