mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-07 06:20:15 -03:00
When a KSampler marked as 'Send Gen Params Target' has widget inputs wired to Primitive nodes (PrimitiveNode, PrimitiveInt, PrimitiveFloat, etc.), sending gen params from the Lora Manager UI now updates the Primitive node's value instead of the KSampler widget. This is necessary because ComfyUI's execution engine reads from the connected input, ignoring the widget value when a wire is present. Also fix two minor issues found during review: - Remove unnecessary String() wrapping on numeric gen params (seed, steps, cfg) to preserve native types through the JSON/WS path - Correct misleading isNodeEnabled comment: LGraphEventMode values are 0=Always, 2=Never, 4=Bypass (not 'Normal/Enabled')
247 lines
10 KiB
JavaScript
247 lines
10 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
|
|
// genParamsMapper is pure logic with zero dependencies — safe to import directly
|
|
import {
|
|
SAMPLER_DISPLAY_TO_INTERNAL,
|
|
SCHEDULER_SUFFIXES,
|
|
SCHEDULER_ONLY_VALUES,
|
|
PARAM_TO_WIDGET_CANDIDATES,
|
|
parseCombinedSamplerName,
|
|
resolveSamplerScheduler,
|
|
findMatchingWidgets,
|
|
} from '../../../static/js/utils/genParamsMapper.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constants sanity
|
|
// ---------------------------------------------------------------------------
|
|
describe('constants', () => {
|
|
it('maps at least the common samplers', () => {
|
|
expect(SAMPLER_DISPLAY_TO_INTERNAL['Euler']).toBe('euler');
|
|
expect(SAMPLER_DISPLAY_TO_INTERNAL['Euler a']).toBe('euler_ancestral');
|
|
expect(SAMPLER_DISPLAY_TO_INTERNAL['DPM++ 2M']).toBe('dpmpp_2m');
|
|
expect(SAMPLER_DISPLAY_TO_INTERNAL['DPM++ 2M SDE']).toBe('dpmpp_2m_sde');
|
|
expect(SAMPLER_DISPLAY_TO_INTERNAL['LCM']).toBe('lcm');
|
|
expect(SAMPLER_DISPLAY_TO_INTERNAL['DDIM']).toBe('ddim');
|
|
});
|
|
|
|
it('lists all 9 scheduler suffixes', () => {
|
|
expect(SCHEDULER_SUFFIXES).toHaveLength(9);
|
|
expect(SCHEDULER_SUFFIXES).toContain('karras');
|
|
expect(SCHEDULER_SUFFIXES).toContain('simple');
|
|
expect(SCHEDULER_SUFFIXES).toContain('exponential');
|
|
});
|
|
|
|
it('marks scheduler-only values', () => {
|
|
expect(SCHEDULER_ONLY_VALUES.has('karras')).toBe(true);
|
|
expect(SCHEDULER_ONLY_VALUES.has('simple')).toBe(true);
|
|
expect(SCHEDULER_ONLY_VALUES.has('euler')).toBe(false);
|
|
});
|
|
|
|
it('has widget candidates for all param keys', () => {
|
|
expect(PARAM_TO_WIDGET_CANDIDATES.seed).toContain('seed');
|
|
expect(PARAM_TO_WIDGET_CANDIDATES.sampler).toContain('sampler_name');
|
|
expect(PARAM_TO_WIDGET_CANDIDATES.scheduler).toContain('scheduler');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// parseCombinedSamplerName
|
|
// ---------------------------------------------------------------------------
|
|
describe('parseCombinedSamplerName', () => {
|
|
it('parses space-separated sampler + scheduler', () => {
|
|
expect(parseCombinedSamplerName('Euler a Karras')).toEqual({
|
|
sampler: 'euler_ancestral',
|
|
scheduler: 'karras',
|
|
});
|
|
});
|
|
|
|
it('parses DPM++ 2M Karras', () => {
|
|
expect(parseCombinedSamplerName('DPM++ 2M Karras')).toEqual({
|
|
sampler: 'dpmpp_2m',
|
|
scheduler: 'karras',
|
|
});
|
|
});
|
|
|
|
it('parses DPM++ 2M beta', () => {
|
|
expect(parseCombinedSamplerName('DPM++ 2M beta')).toEqual({
|
|
sampler: 'dpmpp_2m',
|
|
scheduler: 'beta',
|
|
});
|
|
});
|
|
|
|
it('parses DPM++ SDE Karras', () => {
|
|
expect(parseCombinedSamplerName('DPM++ SDE Karras')).toEqual({
|
|
sampler: 'dpmpp_sde',
|
|
scheduler: 'karras',
|
|
});
|
|
});
|
|
|
|
it('parses underscore-separated er_sde_beta', () => {
|
|
expect(parseCombinedSamplerName('er_sde_beta')).toEqual({
|
|
sampler: 'er_sde',
|
|
scheduler: 'beta',
|
|
});
|
|
});
|
|
|
|
it('returns null for sampler-only values', () => {
|
|
expect(parseCombinedSamplerName('Euler a')).toBeNull();
|
|
expect(parseCombinedSamplerName('LCM')).toBeNull();
|
|
});
|
|
|
|
it('returns null for unrecognised suffix', () => {
|
|
expect(parseCombinedSamplerName('Euler something_unknown')).toBeNull();
|
|
});
|
|
|
|
it('returns null for null/empty', () => {
|
|
expect(parseCombinedSamplerName(null)).toBeNull();
|
|
expect(parseCombinedSamplerName('')).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resolveSamplerScheduler — the main resolver used by the send feature
|
|
// ---------------------------------------------------------------------------
|
|
describe('resolveSamplerScheduler', () => {
|
|
// --- Category 1: simple display names ---
|
|
it('resolves Euler → euler', () => {
|
|
expect(resolveSamplerScheduler('Euler')).toEqual({ sampler: 'euler', scheduler: null });
|
|
});
|
|
|
|
it('resolves Euler a → euler_ancestral', () => {
|
|
expect(resolveSamplerScheduler('Euler a')).toEqual({ sampler: 'euler_ancestral', scheduler: null });
|
|
});
|
|
|
|
it('resolves DPM++ 2M → dpmpp_2m', () => {
|
|
expect(resolveSamplerScheduler('DPM++ 2M')).toEqual({ sampler: 'dpmpp_2m', scheduler: null });
|
|
});
|
|
|
|
it('resolves LCM → lcm', () => {
|
|
expect(resolveSamplerScheduler('LCM')).toEqual({ sampler: 'lcm', scheduler: null });
|
|
});
|
|
|
|
// --- Category 2: already-internal names ---
|
|
it('passes through lowercase internal names', () => {
|
|
expect(resolveSamplerScheduler('euler')).toEqual({ sampler: 'euler', scheduler: null });
|
|
expect(resolveSamplerScheduler('heunpp2')).toEqual({ sampler: 'heunpp2', scheduler: null });
|
|
expect(resolveSamplerScheduler('lcm')).toEqual({ sampler: 'lcm', scheduler: null });
|
|
expect(resolveSamplerScheduler('er_sde')).toEqual({ sampler: 'er_sde', scheduler: null });
|
|
});
|
|
|
|
// --- Category 3: combined names ---
|
|
it('resolves Euler a Karras → euler_ancestral + karras', () => {
|
|
expect(resolveSamplerScheduler('Euler a Karras')).toEqual({
|
|
sampler: 'euler_ancestral',
|
|
scheduler: 'karras',
|
|
});
|
|
});
|
|
|
|
it('resolves DPM++ 2M Karras → dpmpp_2m + karras', () => {
|
|
expect(resolveSamplerScheduler('DPM++ 2M Karras')).toEqual({
|
|
sampler: 'dpmpp_2m',
|
|
scheduler: 'karras',
|
|
});
|
|
});
|
|
|
|
// --- Category 4: scheduler-only ---
|
|
it('resolves scheduler-only values', () => {
|
|
expect(resolveSamplerScheduler('karras')).toEqual({ sampler: null, scheduler: 'karras' });
|
|
expect(resolveSamplerScheduler('simple')).toEqual({ sampler: null, scheduler: 'simple' });
|
|
expect(resolveSamplerScheduler('sgm_uniform')).toEqual({ sampler: null, scheduler: 'sgm_uniform' });
|
|
});
|
|
|
|
// --- Category 5: unrecognised / model-specific ---
|
|
it('returns null+null for unrecognised values', () => {
|
|
const result = resolveSamplerScheduler('AYS SDXL');
|
|
expect(result.sampler).toBeNull();
|
|
expect(result.scheduler).toBeNull();
|
|
});
|
|
|
|
it('returns null+null for Undefined', () => {
|
|
const result = resolveSamplerScheduler('Undefined');
|
|
expect(result.sampler).toBeNull();
|
|
expect(result.scheduler).toBeNull();
|
|
});
|
|
|
|
it('returns null+null for model-specific values', () => {
|
|
expect(resolveSamplerScheduler('Seedream-V45').sampler).toBeNull();
|
|
expect(resolveSamplerScheduler('GPT-Image-2').sampler).toBeNull();
|
|
});
|
|
|
|
// --- Category 6: edge cases ---
|
|
it('returns null+null for null / empty / whitespace', () => {
|
|
expect(resolveSamplerScheduler(null)).toEqual({ sampler: null, scheduler: null });
|
|
expect(resolveSamplerScheduler('')).toEqual({ sampler: null, scheduler: null });
|
|
expect(resolveSamplerScheduler(' ')).toEqual({ sampler: null, scheduler: null });
|
|
});
|
|
|
|
it('handles slash-separated custom format (extracts last segment)', () => {
|
|
// "multistep/dpmpp_2m_simple" — extracts last segment but the recursive
|
|
// call hits the "already internal name" regex before combined-name parsing,
|
|
// so it returns the raw segment as the sampler name.
|
|
const result = resolveSamplerScheduler('multistep/dpmpp_2m_simple');
|
|
expect(result.sampler).toBe('dpmpp_2m_simple');
|
|
expect(result.scheduler).toBeNull();
|
|
});
|
|
|
|
it('handles parse-error value (None', () => {
|
|
const result = resolveSamplerScheduler('(None');
|
|
expect(result.sampler).toBeNull();
|
|
expect(result.scheduler).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// findMatchingWidgets
|
|
// ---------------------------------------------------------------------------
|
|
describe('findMatchingWidgets', () => {
|
|
const resolved = {
|
|
seed: 42,
|
|
steps: 30,
|
|
cfg: 7,
|
|
sampler: 'euler_ancestral',
|
|
scheduler: 'karras',
|
|
};
|
|
|
|
it('matches seed to seed widget', () => {
|
|
const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
|
expect(updates).toContainEqual({ widgetName: 'seed', value: 42 });
|
|
expect(updates).toContainEqual({ widgetName: 'steps', value: 30 });
|
|
expect(updates).toContainEqual({ widgetName: 'cfg', value: 7 });
|
|
expect(updates).toContainEqual({ widgetName: 'sampler_name', value: 'euler_ancestral' });
|
|
expect(updates).toContainEqual({ widgetName: 'scheduler', value: 'karras' });
|
|
});
|
|
|
|
it('skips undefined/null params', () => {
|
|
const updates = findMatchingWidgets(['seed', 'steps'], { seed: 42, steps: null, cfg: undefined });
|
|
expect(updates).toHaveLength(1);
|
|
expect(updates[0].widgetName).toBe('seed');
|
|
});
|
|
|
|
it('matches noise_seed when seed widget not present', () => {
|
|
const updates = findMatchingWidgets(['noise_seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
|
const seedUpdate = updates.find(u => u.widgetName === 'noise_seed');
|
|
expect(seedUpdate).toBeDefined();
|
|
expect(seedUpdate.value).toBe(42);
|
|
});
|
|
|
|
it('matches rgthree-style sampler widget name', () => {
|
|
const updates = findMatchingWidgets(['sampler', 'scheduler'], { sampler: 'euler', scheduler: 'karras' });
|
|
expect(updates).toContainEqual({ widgetName: 'sampler', value: 'euler' });
|
|
});
|
|
|
|
it('returns empty array for empty widget list', () => {
|
|
expect(findMatchingWidgets([], resolved)).toEqual([]);
|
|
expect(findMatchingWidgets(null, resolved)).toEqual([]);
|
|
});
|
|
|
|
it('handles case-insensitive widget name matching', () => {
|
|
const updates = findMatchingWidgets(['SEED', 'STEPS', 'CFG'], resolved);
|
|
expect(updates).toHaveLength(3);
|
|
});
|
|
|
|
it('returns updates in param order (seed, steps, cfg, sampler, scheduler)', () => {
|
|
const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
|
expect(updates.map(u => u.widgetName)).toEqual(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler']);
|
|
});
|
|
});
|