mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
test(frontend): extend coverage for comfyui widgets and helpers
This commit is contained in:
7
scripts/api.js
Normal file
7
scripts/api.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const api = {
|
||||||
|
fetchApi: (...args) => fetch(...args),
|
||||||
|
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
|
||||||
|
removeEventListener: (eventName, handler) => document.removeEventListener(eventName, handler),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
12
scripts/app.js
Normal file
12
scripts/app.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const app = {
|
||||||
|
canvas: { ds: { scale: 1 } },
|
||||||
|
extensionManager: {
|
||||||
|
toast: {
|
||||||
|
add: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registerExtension: () => {},
|
||||||
|
graphToPrompt: async () => ({ workflow: { nodes: new Map() } }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -5,7 +5,7 @@ import { showToast } from './utils/uiHelpers.js';
|
|||||||
// Chart.js import (assuming it's available globally or via CDN)
|
// Chart.js import (assuming it's available globally or via CDN)
|
||||||
// If Chart.js isn't available, we'll need to add it to the project
|
// If Chart.js isn't available, we'll need to add it to the project
|
||||||
|
|
||||||
class StatisticsManager {
|
export class StatisticsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.charts = {};
|
this.charts = {};
|
||||||
this.data = {};
|
this.data = {};
|
||||||
|
|||||||
139
tests/frontend/components/autocomplete.behavior.test.js
Normal file
139
tests/frontend/components/autocomplete.behavior.test.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
API_MODULE,
|
||||||
|
APP_MODULE,
|
||||||
|
CARET_HELPER_MODULE,
|
||||||
|
PREVIEW_COMPONENT_MODULE,
|
||||||
|
AUTOCOMPLETE_MODULE,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
API_MODULE: new URL('../../../scripts/api.js', import.meta.url).pathname,
|
||||||
|
APP_MODULE: new URL('../../../scripts/app.js', import.meta.url).pathname,
|
||||||
|
CARET_HELPER_MODULE: new URL('../../../web/comfyui/textarea_caret_helper.js', import.meta.url).pathname,
|
||||||
|
PREVIEW_COMPONENT_MODULE: new URL('../../../web/comfyui/loras_widget_components.js', import.meta.url).pathname,
|
||||||
|
AUTOCOMPLETE_MODULE: new URL('../../../web/comfyui/autocomplete.js', import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fetchApiMock = vi.fn();
|
||||||
|
const caretHelperInstance = {
|
||||||
|
getBeforeCursor: vi.fn(() => ''),
|
||||||
|
getCursorOffset: vi.fn(() => ({ left: 0, top: 0 })),
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewTooltipMock = {
|
||||||
|
show: vi.fn(),
|
||||||
|
hide: vi.fn(),
|
||||||
|
cleanup: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock(API_MODULE, () => ({
|
||||||
|
api: {
|
||||||
|
fetchApi: fetchApiMock,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(APP_MODULE, () => ({
|
||||||
|
app: {
|
||||||
|
canvas: {
|
||||||
|
ds: { scale: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(CARET_HELPER_MODULE, () => ({
|
||||||
|
TextAreaCaretHelper: vi.fn(() => caretHelperInstance),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(PREVIEW_COMPONENT_MODULE, () => ({
|
||||||
|
PreviewTooltip: vi.fn(() => previewTooltipMock),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AutoComplete widget interactions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
document.head.querySelectorAll('style').forEach((styleEl) => styleEl.remove());
|
||||||
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
|
fetchApiMock.mockReset();
|
||||||
|
caretHelperInstance.getBeforeCursor.mockReset();
|
||||||
|
caretHelperInstance.getCursorOffset.mockReset();
|
||||||
|
caretHelperInstance.getBeforeCursor.mockReturnValue('');
|
||||||
|
caretHelperInstance.getCursorOffset.mockReturnValue({ left: 0, top: 0 });
|
||||||
|
previewTooltipMock.show.mockReset();
|
||||||
|
previewTooltipMock.hide.mockReset();
|
||||||
|
previewTooltipMock.cleanup.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches and renders search results when input exceeds the minimum characters', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
fetchApiMock.mockResolvedValue({
|
||||||
|
json: () => Promise.resolve({ success: true, relative_paths: ['models/example.safetensors'] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
caretHelperInstance.getBeforeCursor.mockReturnValue('example');
|
||||||
|
caretHelperInstance.getCursorOffset.mockReturnValue({ left: 15, top: 25 });
|
||||||
|
|
||||||
|
const input = document.createElement('textarea');
|
||||||
|
document.body.append(input);
|
||||||
|
|
||||||
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||||
|
const autoComplete = new AutoComplete(input, 'loras', { debounceDelay: 0, showPreview: false });
|
||||||
|
|
||||||
|
input.value = 'example';
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(fetchApiMock).toHaveBeenCalledWith('/lm/loras/relative-paths?search=example&limit=20');
|
||||||
|
const items = autoComplete.dropdown.querySelectorAll('.comfy-autocomplete-item');
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(autoComplete.dropdown.style.display).toBe('block');
|
||||||
|
expect(autoComplete.isVisible).toBe(true);
|
||||||
|
expect(caretHelperInstance.getCursorOffset).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts the selected LoRA with usage tip strengths and restores focus', async () => {
|
||||||
|
fetchApiMock.mockImplementation((url) => {
|
||||||
|
if (url.includes('usage-tips-by-path')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
success: true,
|
||||||
|
usage_tips: JSON.stringify({ strength: '1.5', clip_strength: '0.9' }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
json: () => Promise.resolve({ success: true, relative_paths: ['models/example.safetensors'] }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
caretHelperInstance.getBeforeCursor.mockReturnValue('alpha, example');
|
||||||
|
|
||||||
|
const input = document.createElement('textarea');
|
||||||
|
input.value = 'alpha, example';
|
||||||
|
input.selectionStart = input.value.length;
|
||||||
|
input.focus = vi.fn();
|
||||||
|
input.setSelectionRange = vi.fn();
|
||||||
|
document.body.append(input);
|
||||||
|
|
||||||
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||||
|
const autoComplete = new AutoComplete(input, 'loras', { debounceDelay: 0, showPreview: false });
|
||||||
|
|
||||||
|
await autoComplete.insertSelection('models/example.safetensors');
|
||||||
|
|
||||||
|
expect(fetchApiMock).toHaveBeenCalledWith(
|
||||||
|
'/lm/loras/usage-tips-by-path?relative_path=models%2Fexample.safetensors',
|
||||||
|
);
|
||||||
|
expect(input.value).toContain('<lora:example:1.5:0.9>, ');
|
||||||
|
expect(autoComplete.dropdown.style.display).toBe('none');
|
||||||
|
expect(input.focus).toHaveBeenCalled();
|
||||||
|
expect(input.setSelectionRange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
108
tests/frontend/components/lorasWidgetEvents.interactions.test.js
Normal file
108
tests/frontend/components/lorasWidgetEvents.interactions.test.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
EVENTS_MODULE,
|
||||||
|
API_MODULE,
|
||||||
|
APP_MODULE,
|
||||||
|
COMPONENTS_MODULE,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
EVENTS_MODULE: new URL('../../../web/comfyui/loras_widget_events.js', import.meta.url).pathname,
|
||||||
|
API_MODULE: new URL('../../../scripts/api.js', import.meta.url).pathname,
|
||||||
|
APP_MODULE: new URL('../../../scripts/app.js', import.meta.url).pathname,
|
||||||
|
COMPONENTS_MODULE: new URL('../../../web/comfyui/loras_widget_components.js', import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(API_MODULE, () => ({
|
||||||
|
api: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(APP_MODULE, () => ({
|
||||||
|
app: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(COMPONENTS_MODULE, () => ({
|
||||||
|
createMenuItem: vi.fn(),
|
||||||
|
createDropIndicator: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('LoRA widget drag interactions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const dragStyle = document.getElementById('comfy-lora-drag-style');
|
||||||
|
if (dragStyle) {
|
||||||
|
dragStyle.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.classList.remove('comfy-lora-dragging');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjusts a single LoRA strength while syncing collapsed clip strength', async () => {
|
||||||
|
const { handleStrengthDrag } = await import(EVENTS_MODULE);
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
value: [
|
||||||
|
{ name: 'Test', strength: 0.5, clipStrength: 0.25, expanded: false },
|
||||||
|
],
|
||||||
|
callback: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleStrengthDrag('Test', 0.5, 100, { clientX: 140 }, widget, false);
|
||||||
|
|
||||||
|
expect(widget.value[0].strength).toBeCloseTo(0.54, 2);
|
||||||
|
expect(widget.value[0].clipStrength).toBeCloseTo(0.54, 2);
|
||||||
|
expect(widget.callback).toHaveBeenCalledWith(widget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies proportional drag updates to all LoRAs', async () => {
|
||||||
|
const { handleAllStrengthsDrag } = await import(EVENTS_MODULE);
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
value: [
|
||||||
|
{ name: 'A', strength: 0.4, clipStrength: 0.4 },
|
||||||
|
{ name: 'B', strength: 0.6, clipStrength: 0.6 },
|
||||||
|
],
|
||||||
|
callback: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialStrengths = [
|
||||||
|
{ modelStrength: 0.4, clipStrength: 0.4 },
|
||||||
|
{ modelStrength: 0.6, clipStrength: 0.6 },
|
||||||
|
];
|
||||||
|
|
||||||
|
handleAllStrengthsDrag(initialStrengths, 100, { clientX: 160 }, widget);
|
||||||
|
|
||||||
|
expect(widget.value[0].strength).toBeCloseTo(0.41, 2);
|
||||||
|
expect(widget.value[1].strength).toBeCloseTo(0.62, 2);
|
||||||
|
expect(widget.callback).toHaveBeenCalledWith(widget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initiates drag gestures, updates strength, and clears cursor state on mouseup', async () => {
|
||||||
|
const module = await import(EVENTS_MODULE);
|
||||||
|
const renderSpy = vi.fn();
|
||||||
|
const previewSpy = { hide: vi.fn() };
|
||||||
|
|
||||||
|
const dragEl = document.createElement('div');
|
||||||
|
dragEl.className = 'comfy-lora-entry';
|
||||||
|
document.body.append(dragEl);
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
value: [{ name: 'Test', strength: 0.5, clipStrength: 0.5 }],
|
||||||
|
callback: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.initDrag(dragEl, 'Test', widget, false, previewSpy, renderSpy);
|
||||||
|
|
||||||
|
dragEl.dispatchEvent(new MouseEvent('mousedown', { clientX: 50, bubbles: true }));
|
||||||
|
expect(document.body.classList.contains('comfy-lora-dragging')).toBe(true);
|
||||||
|
|
||||||
|
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 70, bubbles: true }));
|
||||||
|
expect(renderSpy).toHaveBeenCalledWith(widget.value, widget);
|
||||||
|
expect(previewSpy.hide).toHaveBeenCalled();
|
||||||
|
expect(widget.value[0].strength).not.toBe(0.5);
|
||||||
|
|
||||||
|
document.dispatchEvent(new MouseEvent('mouseup'));
|
||||||
|
expect(document.body.classList.contains('comfy-lora-dragging')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
142
tests/frontend/pages/statistics.dashboard.test.js
Normal file
142
tests/frontend/pages/statistics.dashboard.test.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||||
|
import { renderTemplate, resetDom } from '../utils/domFixtures.js';
|
||||||
|
|
||||||
|
const { CORE_MODULE, UI_HELPERS_MODULE, STATISTICS_MODULE } = vi.hoisted(() => ({
|
||||||
|
CORE_MODULE: new URL('../../../static/js/core.js', import.meta.url).pathname,
|
||||||
|
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||||
|
STATISTICS_MODULE: new URL('../../../static/js/statistics.js', import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const appCoreInitializeMock = vi.fn();
|
||||||
|
const showToastMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock(CORE_MODULE, () => ({
|
||||||
|
appCore: {
|
||||||
|
initialize: appCoreInitializeMock,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||||
|
showToast: showToastMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Statistics dashboard rendering', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetDom();
|
||||||
|
appCoreInitializeMock.mockResolvedValue();
|
||||||
|
showToastMock.mockReset();
|
||||||
|
globalThis.Chart = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete window.statsManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hydrates dashboard panels with fetched data and wires tab interactions', async () => {
|
||||||
|
renderTemplate('statistics.html');
|
||||||
|
|
||||||
|
const dataset = {
|
||||||
|
'/api/lm/stats/collection-overview': {
|
||||||
|
data: {
|
||||||
|
total_models: 4,
|
||||||
|
total_size: 4096,
|
||||||
|
total_generations: 200,
|
||||||
|
lora_count: 2,
|
||||||
|
checkpoint_count: 1,
|
||||||
|
embedding_count: 1,
|
||||||
|
unused_loras: 1,
|
||||||
|
unused_checkpoints: 0,
|
||||||
|
unused_embeddings: 0,
|
||||||
|
lora_size: 2048,
|
||||||
|
checkpoint_size: 1024,
|
||||||
|
embedding_size: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/lm/stats/usage-analytics': {
|
||||||
|
data: {
|
||||||
|
top_loras: [
|
||||||
|
{ name: 'Lora A', base_model: 'SDXL', folder: 'loras', usage_count: 10 },
|
||||||
|
],
|
||||||
|
top_checkpoints: [
|
||||||
|
{ name: 'Checkpoint A', base_model: 'SDXL', folder: 'checkpoints', usage_count: 5 },
|
||||||
|
],
|
||||||
|
top_embeddings: [
|
||||||
|
{ name: 'Embedding A', base_model: 'SDXL', folder: 'embeddings', usage_count: 7 },
|
||||||
|
],
|
||||||
|
usage_timeline: [
|
||||||
|
{ date: '2024-01-01', lora_usage: 5, checkpoint_usage: 3, embedding_usage: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/lm/stats/base-model-distribution': {
|
||||||
|
data: {
|
||||||
|
loras: { SDXL: 2 },
|
||||||
|
checkpoints: { SDXL: 1 },
|
||||||
|
embeddings: { SDXL: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/lm/stats/tag-analytics': {
|
||||||
|
data: {
|
||||||
|
top_tags: [
|
||||||
|
{ tag: 'anime', count: 5 },
|
||||||
|
{ tag: 'photo', count: 3 },
|
||||||
|
],
|
||||||
|
total_unique_tags: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/lm/stats/storage-analytics': {
|
||||||
|
data: {
|
||||||
|
loras: [
|
||||||
|
{ name: 'Lora A', base_model: 'SDXL', size: 2048, usage_count: 10 },
|
||||||
|
],
|
||||||
|
checkpoints: [
|
||||||
|
{ name: 'Checkpoint A', base_model: 'SDXL', size: 1024, usage_count: 5 },
|
||||||
|
],
|
||||||
|
embeddings: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/lm/stats/insights': {
|
||||||
|
data: {
|
||||||
|
insights: [
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
title: 'Balance usage',
|
||||||
|
description: 'Redistribute usage across models.',
|
||||||
|
suggestion: 'Try lesser-used checkpoints.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { StatisticsManager } = await import(STATISTICS_MODULE);
|
||||||
|
const manager = new StatisticsManager();
|
||||||
|
const refreshSpy = vi.spyOn(manager, 'refreshChartsInPanel');
|
||||||
|
vi.spyOn(manager, 'fetchData').mockImplementation((endpoint) => Promise.resolve(dataset[endpoint]));
|
||||||
|
|
||||||
|
await manager.initialize();
|
||||||
|
|
||||||
|
expect(manager.initialized).toBe(true);
|
||||||
|
expect(document.querySelectorAll('.metric-card').length).toBeGreaterThan(0);
|
||||||
|
expect(document.querySelector('#topLorasList .model-item')).not.toBeNull();
|
||||||
|
expect(document.querySelector('#tagCloud').textContent).toContain('anime');
|
||||||
|
expect(document.querySelector('#insightsList .insight-card')).not.toBeNull();
|
||||||
|
|
||||||
|
const usageButton = document.querySelector('.tab-button[data-tab="usage"]');
|
||||||
|
usageButton.click();
|
||||||
|
|
||||||
|
expect(refreshSpy).toHaveBeenCalledWith('usage');
|
||||||
|
expect(document.getElementById('usage-panel').classList.contains('active')).toBe(true);
|
||||||
|
expect(document.querySelector('.tab-button.active').dataset.tab).toBe('usage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces an error toast when statistics data fails to load', async () => {
|
||||||
|
const { StatisticsManager } = await import(STATISTICS_MODULE);
|
||||||
|
const manager = new StatisticsManager();
|
||||||
|
vi.spyOn(manager, 'fetchData').mockRejectedValue(new Error('unavailable'));
|
||||||
|
|
||||||
|
await manager.loadAllData();
|
||||||
|
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith('toast.general.statisticsLoadFailed', {}, 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
108
tests/frontend/utils/uiHelpers.dom.test.js
Normal file
108
tests/frontend/utils/uiHelpers.dom.test.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
I18N_MODULE,
|
||||||
|
STATE_MODULE,
|
||||||
|
STORAGE_MODULE,
|
||||||
|
CONSTANTS_MODULE,
|
||||||
|
EVENT_MANAGER_MODULE,
|
||||||
|
UI_HELPERS_MODULE,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
||||||
|
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
|
||||||
|
STORAGE_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname,
|
||||||
|
CONSTANTS_MODULE: new URL('../../../static/js/utils/constants.js', import.meta.url).pathname,
|
||||||
|
EVENT_MANAGER_MODULE: new URL('../../../static/js/utils/EventManager.js', import.meta.url).pathname,
|
||||||
|
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const translateMock = vi.fn((key, _params, fallback) => fallback || key);
|
||||||
|
const getStorageItemMock = vi.fn();
|
||||||
|
const setStorageItemMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock(I18N_MODULE, () => ({
|
||||||
|
translate: translateMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(STATE_MODULE, () => ({
|
||||||
|
state: {},
|
||||||
|
getCurrentPageState: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(STORAGE_MODULE, () => ({
|
||||||
|
getStorageItem: getStorageItemMock,
|
||||||
|
setStorageItem: setStorageItemMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(CONSTANTS_MODULE, () => ({
|
||||||
|
NODE_TYPE_ICONS: {},
|
||||||
|
DEFAULT_NODE_COLOR: '#ffffff',
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(EVENT_MANAGER_MODULE, () => ({
|
||||||
|
eventManager: {
|
||||||
|
emit: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
off: vi.fn(),
|
||||||
|
addHandler: vi.fn(),
|
||||||
|
removeHandler: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('UI helper DOM utilities', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
document.body.removeAttribute('data-theme');
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
getStorageItemMock.mockReset();
|
||||||
|
setStorageItemMock.mockReset();
|
||||||
|
translateMock.mockReset();
|
||||||
|
globalThis.requestAnimationFrame = (cb) => cb();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates toast elements and cleans them up after timeout', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
translateMock.mockReturnValue('Toast message');
|
||||||
|
|
||||||
|
const { showToast } = await import(UI_HELPERS_MODULE);
|
||||||
|
|
||||||
|
showToast('uiHelpers.clipboard.copied', {}, 'success');
|
||||||
|
|
||||||
|
const container = document.querySelector('.toast-container');
|
||||||
|
expect(container).not.toBeNull();
|
||||||
|
expect(container.querySelectorAll('.toast')).toHaveLength(1);
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
vi.advanceTimersByTime(2000);
|
||||||
|
|
||||||
|
const toast = container.querySelector('.toast');
|
||||||
|
toast.dispatchEvent(new Event('transitionend', { bubbles: true }));
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(toast.classList.contains('show')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles the persisted theme and updates DOM attributes', async () => {
|
||||||
|
getStorageItemMock.mockReturnValue('light');
|
||||||
|
document.body.innerHTML = '<button class="theme-toggle"></button>';
|
||||||
|
globalThis.matchMedia = vi.fn(() => ({
|
||||||
|
matches: false,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { toggleTheme } = await import(UI_HELPERS_MODULE);
|
||||||
|
|
||||||
|
const nextTheme = toggleTheme();
|
||||||
|
|
||||||
|
expect(nextTheme).toBe('dark');
|
||||||
|
expect(setStorageItemMock).toHaveBeenCalledWith('theme', 'dark');
|
||||||
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
||||||
|
expect(document.body.dataset.theme).toBe('dark');
|
||||||
|
expect(document.querySelector('.theme-toggle').classList.contains('theme-dark')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user