Story 3.2 done
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Stub foundry global for conditional base class
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Hooks', { on: vi.fn(() => 99), off: vi.fn() });
|
||||
vi.stubGlobal('game', { user: { setFlag: vi.fn(), getFlag: vi.fn(() => null) } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
import { ScenePresetPanel } from '../../../../src/ui/gm/ScenePresetPanel.js';
|
||||
|
||||
describe('ScenePresetPanel', () => {
|
||||
let adapter;
|
||||
let scenePresetManager;
|
||||
let panel;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = {
|
||||
scenes: { current: vi.fn(() => ({ id: 'scene1', name: 'Test Scene' })) },
|
||||
i18n: { localize: vi.fn((key) => key) },
|
||||
notifications: { info: vi.fn() },
|
||||
};
|
||||
scenePresetManager = {
|
||||
list: vi.fn(() => [
|
||||
{ name: 'Preset 1' },
|
||||
{ name: 'Preset 2' },
|
||||
]),
|
||||
_getSceneFlagData: vi.fn(() => ({})),
|
||||
_getAutoApplyConfig: vi.fn(() => ({ enabled: false, presetName: null, preDelay: 0 })),
|
||||
configureAutoApply: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
panel = new ScenePresetPanel(adapter, scenePresetManager);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('is side-effect-free: does not call Hooks.on', () => {
|
||||
expect(Hooks.on).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stores adapter and scenePresetManager references', () => {
|
||||
expect(panel._adapter).toBe(adapter);
|
||||
expect(panel._scenePresetManager).toBe(scenePresetManager);
|
||||
});
|
||||
|
||||
it('initializes _element to null', () => {
|
||||
expect(panel._element).toBeNull();
|
||||
});
|
||||
|
||||
it('initializes _isOpen to false', () => {
|
||||
expect(panel._isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('initializes _currentScene to null', () => {
|
||||
expect(panel._currentScene).toBeNull();
|
||||
});
|
||||
|
||||
it('initializes handlers to null', () => {
|
||||
expect(panel._clickHandler).toBeNull();
|
||||
expect(panel._changeHandler).toBeNull();
|
||||
expect(panel._inputHandler).toBeNull();
|
||||
});
|
||||
|
||||
it('sets MAX_PREDELAY to 5000', () => {
|
||||
expect(panel._MAX_PREDELAY).toBe(5000);
|
||||
});
|
||||
|
||||
it('sets MIN_PREDELAY to 0', () => {
|
||||
expect(panel._MIN_PREDELAY).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init()', () => {
|
||||
it('creates the DOM element', () => {
|
||||
panel.init();
|
||||
expect(panel._element).toBeInstanceOf(HTMLElement);
|
||||
expect(panel._element.className).toBe('directors-board__preset-panel');
|
||||
});
|
||||
|
||||
it('sets role attribute to region', () => {
|
||||
panel.init();
|
||||
expect(panel._element.getAttribute('role')).toBe('region');
|
||||
});
|
||||
|
||||
it('sets aria-label using i18n', () => {
|
||||
panel.init();
|
||||
expect(adapter.i18n.localize).toHaveBeenCalledWith('video-view-manager.scenePresetPanel.title');
|
||||
expect(panel._element.getAttribute('aria-label')).toBe('video-view-manager.scenePresetPanel.title');
|
||||
});
|
||||
|
||||
it('sets aria-expanded to false initially', () => {
|
||||
panel.init();
|
||||
expect(panel._element.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
it('sets display to none initially', () => {
|
||||
panel.init();
|
||||
expect(panel._element.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('sets up event listeners', () => {
|
||||
panel.init();
|
||||
expect(panel._clickHandler).toBeDefined();
|
||||
expect(panel._inputHandler).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls _refresh() to populate initial content', () => {
|
||||
const refreshSpy = vi.spyOn(panel, '_refresh');
|
||||
panel.init();
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('element getter', () => {
|
||||
it('returns the panel element after init', () => {
|
||||
panel.init();
|
||||
expect(panel.element).toBe(panel._element);
|
||||
});
|
||||
|
||||
it('returns null before init', () => {
|
||||
expect(panel.element).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('opens the panel when closed', () => {
|
||||
panel._isOpen = false;
|
||||
panel._element.style.display = 'none';
|
||||
panel.toggle();
|
||||
expect(panel._isOpen).toBe(true);
|
||||
expect(panel._element.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('closes the panel when open', () => {
|
||||
panel._isOpen = true;
|
||||
panel._element.style.display = 'block';
|
||||
panel.toggle();
|
||||
expect(panel._isOpen).toBe(false);
|
||||
expect(panel._element.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('is a no-op when element is null', () => {
|
||||
panel._element = null;
|
||||
panel.toggle();
|
||||
expect(panel._isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('open()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('sets _isOpen to true', () => {
|
||||
panel.open();
|
||||
expect(panel._isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('sets display to block', () => {
|
||||
panel.open();
|
||||
expect(panel._element.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('sets aria-expanded to true', () => {
|
||||
panel.open();
|
||||
expect(panel._element.getAttribute('aria-expanded')).toBe('true');
|
||||
});
|
||||
|
||||
it('calls _refresh()', () => {
|
||||
const refreshSpy = vi.spyOn(panel, '_refresh');
|
||||
panel.open();
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is a no-op when element is null', () => {
|
||||
panel._element = null;
|
||||
panel.open();
|
||||
expect(panel._isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('close()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('sets _isOpen to false', () => {
|
||||
panel._isOpen = true;
|
||||
panel.close();
|
||||
expect(panel._isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('sets display to none', () => {
|
||||
panel._element.style.display = 'block';
|
||||
panel.close();
|
||||
expect(panel._element.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('sets aria-expanded to false', () => {
|
||||
panel._element.setAttribute('aria-expanded', 'true');
|
||||
panel.close();
|
||||
expect(panel._element.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
it('is a no-op when element is null', () => {
|
||||
panel._element = null;
|
||||
panel.close();
|
||||
expect(panel._isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_refresh()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('is a no-op when element is null', async () => {
|
||||
panel._element = null;
|
||||
await panel._refresh();
|
||||
// Should not throw
|
||||
});
|
||||
|
||||
it('builds empty HTML when no scene is current', async () => {
|
||||
adapter.scenes.current.mockReturnValue(null);
|
||||
await panel._refresh();
|
||||
expect(panel._element.innerHTML).toContain('noScene');
|
||||
});
|
||||
|
||||
it('stores current scene and builds HTML with scene', async () => {
|
||||
const mockScene = { id: 'scene1', name: 'Test Scene' };
|
||||
adapter.scenes.current.mockReturnValue(mockScene);
|
||||
|
||||
await panel._refresh();
|
||||
|
||||
expect(panel._currentScene).toBe(mockScene);
|
||||
expect(scenePresetManager.list).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates toggle aria-pressed state based on auto-apply enabled', async () => {
|
||||
scenePresetManager._getAutoApplyConfig.mockReturnValue({ enabled: true, presetName: null, preDelay: 0 });
|
||||
await panel._refresh();
|
||||
|
||||
const toggle = panel._element.querySelector('[data-action="toggle-auto-apply"]');
|
||||
expect(toggle).not.toBeNull();
|
||||
expect(toggle.getAttribute('aria-pressed')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_buildEmptyHtml()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('returns HTML with no scene message', () => {
|
||||
const html = panel._buildEmptyHtml();
|
||||
expect(html).toContain('noScene');
|
||||
expect(html).toContain('directors-board__preset-panel-title');
|
||||
});
|
||||
|
||||
it('uses i18n for message', () => {
|
||||
panel._buildEmptyHtml();
|
||||
expect(adapter.i18n.localize).toHaveBeenCalledWith('video-view-manager.scenePresetPanel.noScene');
|
||||
});
|
||||
|
||||
it('escapes HTML in message', () => {
|
||||
adapter.i18n.localize = vi.fn(() => '<script>alert("xss")</script>');
|
||||
const html = panel._buildEmptyHtml();
|
||||
expect(html).not.toContain('<script>');
|
||||
expect(html).toContain('<script>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_buildHtml()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('builds HTML with preset options', () => {
|
||||
const html = panel._buildHtml({
|
||||
enabled: true,
|
||||
presetName: 'Preset 1',
|
||||
preDelay: 1000,
|
||||
presets: [{ name: 'Preset 1' }, { name: 'Preset 2' }],
|
||||
});
|
||||
|
||||
expect(html).toContain('Preset 1');
|
||||
expect(html).toContain('Preset 2');
|
||||
expect(html).toContain('selected');
|
||||
});
|
||||
|
||||
it('includes default option when no preset selected', () => {
|
||||
const html = panel._buildHtml({
|
||||
enabled: false,
|
||||
presetName: null,
|
||||
preDelay: 0,
|
||||
presets: [],
|
||||
});
|
||||
|
||||
expect(html).toContain('selectPreset');
|
||||
expect(html).toContain('selected');
|
||||
});
|
||||
|
||||
it('escapes preset names in options', () => {
|
||||
const html = panel._buildHtml({
|
||||
enabled: false,
|
||||
presetName: null,
|
||||
preDelay: 0,
|
||||
presets: [{ name: '<script>xss</script>' }],
|
||||
});
|
||||
|
||||
expect(html).not.toContain('<script>');
|
||||
expect(html).toContain('<script>');
|
||||
});
|
||||
|
||||
it('includes pre-delay slider with correct value', () => {
|
||||
const html = panel._buildHtml({
|
||||
enabled: false,
|
||||
presetName: null,
|
||||
preDelay: 1500,
|
||||
presets: [],
|
||||
});
|
||||
|
||||
expect(html).toContain('value="1500"');
|
||||
expect(html).toContain('1500ms');
|
||||
});
|
||||
|
||||
it('sets slider min, max, and step', () => {
|
||||
const html = panel._buildHtml({
|
||||
enabled: false,
|
||||
presetName: null,
|
||||
preDelay: 0,
|
||||
presets: [],
|
||||
});
|
||||
|
||||
expect(html).toContain('min="0"');
|
||||
expect(html).toContain('max="5000"');
|
||||
expect(html).toContain('step="100"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_setupEventListeners()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('is a no-op when element is null', () => {
|
||||
panel._element = null;
|
||||
panel._clickHandler = null;
|
||||
panel._inputHandler = null;
|
||||
panel._setupEventListeners();
|
||||
// Should not set handlers when element is null
|
||||
expect(panel._clickHandler).toBeNull();
|
||||
expect(panel._inputHandler).toBeNull();
|
||||
});
|
||||
|
||||
it('sets up click handler', () => {
|
||||
panel._setupEventListeners();
|
||||
expect(panel._clickHandler).toBeDefined();
|
||||
expect(typeof panel._clickHandler).toBe('function');
|
||||
});
|
||||
|
||||
it('sets up input handler', () => {
|
||||
panel._setupEventListeners();
|
||||
expect(panel._inputHandler).toBeDefined();
|
||||
expect(typeof panel._inputHandler).toBe('function');
|
||||
});
|
||||
|
||||
it('adds event listeners to element', () => {
|
||||
const addSpy = vi.spyOn(panel._element, 'addEventListener');
|
||||
panel._setupEventListeners();
|
||||
expect(addSpy).toHaveBeenCalledWith('click', expect.any(Function));
|
||||
expect(addSpy).toHaveBeenCalledWith('input', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_removeEventListeners()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('is a no-op when element is null', () => {
|
||||
panel._element = null;
|
||||
panel._removeEventListeners();
|
||||
// Should not throw
|
||||
});
|
||||
|
||||
it('removes click handler', () => {
|
||||
const removeSpy = vi.spyOn(panel._element, 'removeEventListener');
|
||||
panel._removeEventListeners();
|
||||
expect(removeSpy).toHaveBeenCalledWith('click', expect.any(Function));
|
||||
});
|
||||
|
||||
it('removes input handler', () => {
|
||||
const removeSpy = vi.spyOn(panel._element, 'removeEventListener');
|
||||
panel._removeEventListeners();
|
||||
expect(removeSpy).toHaveBeenCalledWith('input', expect.any(Function));
|
||||
});
|
||||
|
||||
it('sets handlers to null after removal', () => {
|
||||
panel._removeEventListeners();
|
||||
expect(panel._clickHandler).toBeNull();
|
||||
expect(panel._inputHandler).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onToggleAutoApply()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('is a no-op when no scene is current', async () => {
|
||||
adapter.scenes.current.mockReturnValue(null);
|
||||
const mockTarget = { checked: true };
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
expect(scenePresetManager.configureAutoApply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('configures auto-apply with enabled state', async () => {
|
||||
// Create an actual HTMLInputElement for the check to work
|
||||
const mockTarget = document.createElement('input');
|
||||
mockTarget.type = 'checkbox';
|
||||
mockTarget.checked = true;
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
|
||||
expect(scenePresetManager.configureAutoApply).toHaveBeenCalledWith(
|
||||
{ id: 'scene1', name: 'Test Scene' },
|
||||
{ enabled: true, presetName: null, preDelay: 0 }
|
||||
);
|
||||
});
|
||||
|
||||
it('updates toggle aria-pressed state', async () => {
|
||||
const mockTarget = document.createElement('input');
|
||||
mockTarget.type = 'checkbox';
|
||||
mockTarget.checked = true;
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
expect(mockTarget.getAttribute('aria-pressed')).toBe('true');
|
||||
});
|
||||
|
||||
it('shows notification on enable', async () => {
|
||||
const mockTarget = document.createElement('input');
|
||||
mockTarget.type = 'checkbox';
|
||||
mockTarget.checked = true;
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
expect(adapter.notifications.info).toHaveBeenCalledWith(
|
||||
'video-view-manager.scenePresetPanel.notifications.enabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows notification on disable', async () => {
|
||||
const mockTarget = document.createElement('input');
|
||||
mockTarget.type = 'checkbox';
|
||||
mockTarget.checked = false;
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
expect(adapter.notifications.info).toHaveBeenCalledWith(
|
||||
'video-view-manager.scenePresetPanel.notifications.disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts toggle state on error', async () => {
|
||||
scenePresetManager.configureAutoApply.mockRejectedValue(new Error('Test error'));
|
||||
const mockTarget = document.createElement('input');
|
||||
mockTarget.type = 'checkbox';
|
||||
mockTarget.checked = true;
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
// After error, the checked state should be reverted to false (was true, error occurred)
|
||||
expect(mockTarget.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('shows error notification on toggle failure', async () => {
|
||||
scenePresetManager.configureAutoApply.mockRejectedValue(new Error('Test error'));
|
||||
const mockTarget = document.createElement('input');
|
||||
mockTarget.type = 'checkbox';
|
||||
mockTarget.checked = true;
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
await panel._onToggleAutoApply(mockTarget);
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onPresetSelected()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('is a no-op when no scene is current', async () => {
|
||||
adapter.scenes.current.mockReturnValue(null);
|
||||
const mockTarget = { value: 'Preset 1' };
|
||||
await panel._onPresetSelected(mockTarget);
|
||||
expect(scenePresetManager.configureAutoApply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('configures auto-apply with selected preset', async () => {
|
||||
const mockTarget = { value: 'Preset 1' };
|
||||
scenePresetManager._getAutoApplyConfig.mockReturnValue({ enabled: true, presetName: null, preDelay: 0 });
|
||||
|
||||
await panel._onPresetSelected(mockTarget);
|
||||
|
||||
expect(scenePresetManager.configureAutoApply).toHaveBeenCalledWith(
|
||||
{ id: 'scene1', name: 'Test Scene' },
|
||||
{ enabled: true, presetName: 'Preset 1', preDelay: 0 }
|
||||
);
|
||||
});
|
||||
|
||||
it('shows notification when preset is selected', async () => {
|
||||
const mockTarget = { value: 'Preset 1' };
|
||||
await panel._onPresetSelected(mockTarget);
|
||||
expect(adapter.notifications.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles null preset selection (clears preset)', async () => {
|
||||
const mockTarget = { value: '' };
|
||||
await panel._onPresetSelected(mockTarget);
|
||||
|
||||
expect(scenePresetManager.configureAutoApply).toHaveBeenCalledWith(
|
||||
{ id: 'scene1', name: 'Test Scene' },
|
||||
expect.objectContaining({ presetName: null })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onDelayChanged()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
panel._element.innerHTML = '<span class="directors-board__preset-panel-delay-value">1000ms</span>';
|
||||
});
|
||||
|
||||
it('is a no-op when no scene is current', async () => {
|
||||
adapter.scenes.current.mockReturnValue(null);
|
||||
const mockTarget = { value: '1500' };
|
||||
await panel._onDelayChanged(mockTarget);
|
||||
expect(scenePresetManager.configureAutoApply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('configures auto-apply with new delay', async () => {
|
||||
const mockTarget = { value: '1500' };
|
||||
await panel._onDelayChanged(mockTarget);
|
||||
|
||||
expect(scenePresetManager.configureAutoApply).toHaveBeenCalledWith(
|
||||
{ id: 'scene1', name: 'Test Scene' },
|
||||
expect.objectContaining({ preDelay: 1500 })
|
||||
);
|
||||
});
|
||||
|
||||
it('updates displayed value', async () => {
|
||||
const mockTarget = { value: '2000' };
|
||||
await panel._onDelayChanged(mockTarget);
|
||||
|
||||
const valueDisplay = panel._element.querySelector('.directors-board__preset-panel-delay-value');
|
||||
expect(valueDisplay.textContent).toBe('2000ms');
|
||||
});
|
||||
|
||||
it('handles invalid numeric value', async () => {
|
||||
const mockTarget = { value: 'invalid' };
|
||||
await panel._onDelayChanged(mockTarget);
|
||||
|
||||
expect(scenePresetManager.configureAutoApply).toHaveBeenCalledWith(
|
||||
{ id: 'scene1', name: 'Test Scene' },
|
||||
expect.objectContaining({ preDelay: 0 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('teardown()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('removes event listeners', () => {
|
||||
const removeSpy = vi.spyOn(panel, '_removeEventListeners');
|
||||
panel.teardown();
|
||||
expect(removeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closes the panel', () => {
|
||||
const closeSpy = vi.spyOn(panel, 'close');
|
||||
panel.teardown();
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes element from parent when parentNode exists', () => {
|
||||
// Create a proper mock element with parentNode
|
||||
const mockParent = { removeChild: vi.fn() };
|
||||
const mockElement = document.createElement('div');
|
||||
// In jsdom, parentNode is read-only, so we need to mock the entire scenario differently
|
||||
// Instead, test that teardown calls the right methods without throwing
|
||||
panel._element = mockElement;
|
||||
panel._isOpen = true;
|
||||
|
||||
// Mock parentNode getter
|
||||
Object.defineProperty(mockElement, 'parentNode', {
|
||||
value: mockParent,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
panel.teardown();
|
||||
expect(mockParent.removeChild).toHaveBeenCalledWith(mockElement);
|
||||
});
|
||||
|
||||
it('resets state', () => {
|
||||
panel._element = document.createElement('div');
|
||||
panel._isOpen = true;
|
||||
panel._currentScene = { id: 'scene1' };
|
||||
|
||||
panel.teardown();
|
||||
|
||||
expect(panel._element).toBeNull();
|
||||
expect(panel._isOpen).toBe(false);
|
||||
expect(panel._currentScene).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_escapeHtml()', () => {
|
||||
beforeEach(() => {
|
||||
panel.init();
|
||||
});
|
||||
|
||||
it('returns empty string for null input', () => {
|
||||
expect(panel._escapeHtml(null)).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string for undefined input', () => {
|
||||
expect(panel._escapeHtml(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string for non-string input', () => {
|
||||
expect(panel._escapeHtml(123)).toBe('');
|
||||
});
|
||||
|
||||
it('escapes ampersand', () => {
|
||||
expect(panel._escapeHtml('a & b')).toBe('a & b');
|
||||
});
|
||||
|
||||
it('escapes less than', () => {
|
||||
expect(panel._escapeHtml('a < b')).toBe('a < b');
|
||||
});
|
||||
|
||||
it('escapes greater than', () => {
|
||||
expect(panel._escapeHtml('a > b')).toBe('a > b');
|
||||
});
|
||||
|
||||
it('escapes double quotes', () => {
|
||||
expect(panel._escapeHtml('say "hello"')).toBe('say "hello"');
|
||||
});
|
||||
|
||||
it('escapes single quotes', () => {
|
||||
expect(panel._escapeHtml("it's")).toBe("it's");
|
||||
});
|
||||
|
||||
it('escapes multiple special characters', () => {
|
||||
const result = panel._escapeHtml('<script>alert("xss")</script>');
|
||||
expect(result).not.toContain('<');
|
||||
expect(result).not.toContain('>');
|
||||
expect(result).not.toContain('"');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user