mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-13 12:52:13 -03:00
Download root rules now accept match_type "tag": the rule hits when any of its free-text tags equals one of the model's CivitAI tags (trimmed, case-insensitive), so e.g. all "character" models can be routed to a NAS root regardless of base model. Rules stay ordered — first match wins across mixed base_model/tag rules. - settings normalize keeps tag entries as free text (case-insensitive dedupe, no base-model whitelist); resolve_download_root gained a tags parameter and a match_download_root_rule helper - the download flow passes the version's model tags into rule resolution - the routing endpoint accepts tags and reports the matched rule; the versions endpoint ships model-level tags with each version so the location step can preview tag matches - the settings rule editor gained a per-rule match-type dropdown; tag rules edit a comma-separated list (committed on blur/Enter) instead of the base model checkbox panel - the download confirmation modal shows "Routed by rule: ..." next to the preselected root
591 lines
22 KiB
JavaScript
591 lines
22 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 the match-type select, 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];
|
|
expect(row.querySelector('.download-root-rule-match-type').value).toBe('tag');
|
|
expect(row.querySelector('.download-root-rule-summary').textContent)
|
|
.toBe('tags: 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('switching a persisted rule to tag persists immediately and keeps the match list', 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 select = groupRows('loras')[0].querySelector('.download-root-rule-match-type');
|
|
select.value = 'tag';
|
|
select.dispatchEvent(new Event('change'));
|
|
await vi.waitFor(() => {
|
|
expect(manager.saveSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
|
loras: [{ match_type: 'tag', match: ['Flux.1 D'], root: '/models/lora-archive' }],
|
|
checkpoints: [],
|
|
unet: [],
|
|
embeddings: [],
|
|
});
|
|
expect(groupRows('loras')[0].querySelector('.download-root-rule-tag-input')).not.toBeNull();
|
|
});
|
|
|
|
it('a draft switched to tag promotes on the first committed tag input', async () => {
|
|
appendDownloadRootRulesUi();
|
|
const manager = createManager();
|
|
manager.renderDownloadRootRules();
|
|
|
|
manager.addDownloadRootRule('loras');
|
|
const select = groupRows('loras')[0].querySelector('.download-root-rule-match-type');
|
|
select.value = 'tag';
|
|
select.dispatchEvent(new Event('change'));
|
|
|
|
// 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: [],
|
|
});
|
|
});
|
|
});
|