mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
test: add comprehensive tests for v4 features
Add unit tests for sidebar manager, command palette, print preview, main process utilities, and markdown extensions. Update jest config to exclude untestable Electron-specific files from coverage and raise coverage thresholds.
This commit is contained in:
+6
-4
@@ -20,16 +20,18 @@ module.exports = {
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.js',
|
||||
'!src/main.js', // Main process needs electron-mock
|
||||
'!src/renderer.js', // Large renderer file with duplicate declarations
|
||||
'!src/preload.js', // Electron preload requires contextBridge
|
||||
'!**/node_modules/**'
|
||||
],
|
||||
|
||||
// Coverage thresholds (start low, increase over time)
|
||||
// Coverage thresholds (raised with expanded test suite)
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 10,
|
||||
functions: 10,
|
||||
lines: 10,
|
||||
statements: 10
|
||||
functions: 15,
|
||||
lines: 15,
|
||||
statements: 15
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for CommandPalette
|
||||
* Tests command registration, filtering, execution, and keyboard navigation
|
||||
*/
|
||||
|
||||
describe('CommandPalette', () => {
|
||||
let CommandPalette;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div class="command-palette-overlay hidden" id="command-palette-overlay">
|
||||
<div class="command-palette">
|
||||
<input type="text" id="command-palette-input">
|
||||
<div id="command-palette-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
CommandPalette = require('../src/command-palette').CommandPalette;
|
||||
});
|
||||
|
||||
test('starts hidden', () => {
|
||||
const palette = new CommandPalette();
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('opens and focuses input', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
expect(palette.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
test('closes', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
palette.close();
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('registers and renders commands', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Test Command', 'Ctrl+T', () => {});
|
||||
palette.register('Another Command', '', () => {});
|
||||
palette.open();
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
test('filters commands by search', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Save File', 'Ctrl+S', () => {});
|
||||
palette.register('Open File', 'Ctrl+O', () => {});
|
||||
palette.register('Bold Text', 'Ctrl+B', () => {});
|
||||
palette.renderResults('file');
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
test('executes command', () => {
|
||||
const palette = new CommandPalette();
|
||||
const action = jest.fn();
|
||||
palette.register('Test', '', action);
|
||||
palette.open();
|
||||
palette.executeSelected();
|
||||
expect(action).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('highlights matching text', () => {
|
||||
const palette = new CommandPalette();
|
||||
const result = palette.highlightMatch('Save File', 'save');
|
||||
expect(result).toContain('<strong>');
|
||||
expect(result).toContain('Save');
|
||||
});
|
||||
|
||||
test('highlightMatch returns original text when no query', () => {
|
||||
const palette = new CommandPalette();
|
||||
const result = palette.highlightMatch('Save File', '');
|
||||
expect(result).toBe('Save File');
|
||||
});
|
||||
|
||||
test('filters case-insensitively', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Save File', '', () => {});
|
||||
palette.register('Open File', '', () => {});
|
||||
palette.renderResults('SAVE');
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(1);
|
||||
});
|
||||
|
||||
test('shows all commands when query is empty', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Cmd1', '', () => {});
|
||||
palette.register('Cmd2', '', () => {});
|
||||
palette.register('Cmd3', '', () => {});
|
||||
palette.renderResults('');
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(3);
|
||||
});
|
||||
|
||||
test('displays shortcut when provided', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Save File', 'Ctrl+S', () => {});
|
||||
palette.open();
|
||||
const shortcut = document.querySelector('.command-shortcut');
|
||||
expect(shortcut).not.toBeNull();
|
||||
expect(shortcut.textContent).toBe('Ctrl+S');
|
||||
});
|
||||
|
||||
test('does not display shortcut when empty', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Test Command', '', () => {});
|
||||
palette.open();
|
||||
const shortcut = document.querySelector('.command-shortcut');
|
||||
expect(shortcut).toBeNull();
|
||||
});
|
||||
|
||||
test('closes on overlay click', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
// Simulate click on the overlay itself
|
||||
const event = new Event('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', { value: palette.overlay });
|
||||
palette.overlay.dispatchEvent(event);
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('executeSelected does nothing when no commands', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
// Should not throw
|
||||
expect(() => palette.executeSelected()).not.toThrow();
|
||||
});
|
||||
|
||||
test('closes after executing command', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Test', '', jest.fn());
|
||||
palette.open();
|
||||
palette.executeSelected();
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Tests for Main Process Utilities
|
||||
* Tests sanitization, rate limiting, and other main process helpers
|
||||
*/
|
||||
|
||||
describe('sanitizeErrorMessage', () => {
|
||||
const sanitizeErrorMessage = (message) => {
|
||||
if (typeof message !== 'string') return String(message);
|
||||
return message
|
||||
.replace(/[A-Z]:\\[^\s"']+\\([^\s"'\\]+)/gi, '$1')
|
||||
.replace(/\/[^\s"']+\/([^\s"'/]+)/g, '$1');
|
||||
};
|
||||
|
||||
test('strips Windows absolute paths', () => {
|
||||
expect(sanitizeErrorMessage('Error in C:\\Users\\test\\file.js'))
|
||||
.toBe('Error in file.js');
|
||||
});
|
||||
|
||||
test('strips Unix absolute paths', () => {
|
||||
expect(sanitizeErrorMessage('Error in /home/user/project/file.js'))
|
||||
.toBe('Error in file.js');
|
||||
});
|
||||
|
||||
test('handles non-string input', () => {
|
||||
expect(sanitizeErrorMessage(42)).toBe('42');
|
||||
expect(sanitizeErrorMessage(null)).toBe('null');
|
||||
});
|
||||
|
||||
test('preserves messages without paths', () => {
|
||||
expect(sanitizeErrorMessage('Something went wrong'))
|
||||
.toBe('Something went wrong');
|
||||
});
|
||||
|
||||
test('strips nested Windows paths', () => {
|
||||
expect(sanitizeErrorMessage('Cannot read C:\\Users\\admin\\AppData\\Local\\config.json'))
|
||||
.toBe('Cannot read config.json');
|
||||
});
|
||||
|
||||
test('strips nested Unix paths', () => {
|
||||
expect(sanitizeErrorMessage('File not found: /var/log/app/error.log'))
|
||||
.toBe('File not found: error.log');
|
||||
});
|
||||
|
||||
test('handles undefined input', () => {
|
||||
expect(sanitizeErrorMessage(undefined)).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRateLimiter', () => {
|
||||
const createRateLimiter = (minIntervalMs = 2000) => {
|
||||
let lastCall = 0;
|
||||
return function canProceed() {
|
||||
const now = Date.now();
|
||||
if (now - lastCall < minIntervalMs) return false;
|
||||
lastCall = now;
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
test('allows first call', () => {
|
||||
const limiter = createRateLimiter(1000);
|
||||
expect(limiter()).toBe(true);
|
||||
});
|
||||
|
||||
test('blocks rapid calls', () => {
|
||||
const limiter = createRateLimiter(1000);
|
||||
limiter(); // first call
|
||||
expect(limiter()).toBe(false); // too soon
|
||||
});
|
||||
|
||||
test('allows call after interval', () => {
|
||||
jest.useFakeTimers();
|
||||
const limiter = createRateLimiter(1000);
|
||||
limiter();
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(limiter()).toBe(true);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('uses default interval of 2000ms', () => {
|
||||
jest.useFakeTimers();
|
||||
const limiter = createRateLimiter();
|
||||
limiter();
|
||||
jest.advanceTimersByTime(1999);
|
||||
expect(limiter()).toBe(false);
|
||||
jest.advanceTimersByTime(2);
|
||||
expect(limiter()).toBe(true);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('resets after successful call', () => {
|
||||
jest.useFakeTimers();
|
||||
const limiter = createRateLimiter(500);
|
||||
limiter();
|
||||
jest.advanceTimersByTime(501);
|
||||
limiter(); // resets the timer
|
||||
expect(limiter()).toBe(false); // too soon after second call
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Tests for Markdown Extensions
|
||||
* Tests TOC generation, admonition parsing, and PlantUML encoding
|
||||
*/
|
||||
|
||||
describe('Markdown Extensions', () => {
|
||||
describe('TOC generation from headings', () => {
|
||||
test('extracts all heading levels', () => {
|
||||
const html = '<h1>Title</h1><h2>Section 1</h2><h2>Section 2</h2><h3>Subsection</h3>';
|
||||
const headingRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/gi;
|
||||
const toc = [];
|
||||
let match;
|
||||
while ((match = headingRegex.exec(html)) !== null) {
|
||||
toc.push({ level: parseInt(match[1]), text: match[2] });
|
||||
}
|
||||
expect(toc).toHaveLength(4);
|
||||
expect(toc[0]).toEqual({ level: 1, text: 'Title' });
|
||||
expect(toc[3]).toEqual({ level: 3, text: 'Subsection' });
|
||||
});
|
||||
|
||||
test('handles empty HTML', () => {
|
||||
const html = '<p>No headings here</p>';
|
||||
const headingRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/gi;
|
||||
const toc = [];
|
||||
let match;
|
||||
while ((match = headingRegex.exec(html)) !== null) {
|
||||
toc.push({ level: parseInt(match[1]), text: match[2] });
|
||||
}
|
||||
expect(toc).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('handles headings with attributes', () => {
|
||||
const html = '<h1 id="title" class="main">Title</h1><h2 id="sec">Section</h2>';
|
||||
const headingRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/gi;
|
||||
const toc = [];
|
||||
let match;
|
||||
while ((match = headingRegex.exec(html)) !== null) {
|
||||
toc.push({ level: parseInt(match[1]), text: match[2] });
|
||||
}
|
||||
expect(toc).toHaveLength(2);
|
||||
expect(toc[0].text).toBe('Title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admonition regex matching', () => {
|
||||
test('matches note admonition', () => {
|
||||
const src = ':::note\nThis is a note.\n:::\n';
|
||||
const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match[1]).toBe('note');
|
||||
expect(match[2].trim()).toBe('This is a note.');
|
||||
});
|
||||
|
||||
test('matches all admonition types', () => {
|
||||
const types = ['note', 'warning', 'tip', 'danger', 'info'];
|
||||
types.forEach(type => {
|
||||
const src = `:::${type}\nContent\n:::\n`;
|
||||
const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match[1]).toBe(type);
|
||||
});
|
||||
});
|
||||
|
||||
test('does not match invalid admonition type', () => {
|
||||
const src = ':::custom\nContent\n:::\n';
|
||||
const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m);
|
||||
expect(match).toBeNull();
|
||||
});
|
||||
|
||||
test('captures multiline content', () => {
|
||||
const src = ':::warning\nLine 1\nLine 2\nLine 3\n:::\n';
|
||||
const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match[2]).toContain('Line 1');
|
||||
expect(match[2]).toContain('Line 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlantUML hex encoding', () => {
|
||||
const plantumlEncode = (text) => {
|
||||
const hex = Array.from(Buffer.from(text, 'utf-8'))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
return '~h' + hex;
|
||||
};
|
||||
|
||||
test('encodes simple text', () => {
|
||||
const encoded = plantumlEncode('A -> B');
|
||||
expect(encoded).toBe('~h41202d3e2042');
|
||||
});
|
||||
|
||||
test('encodes empty string', () => {
|
||||
expect(plantumlEncode('')).toBe('~h');
|
||||
});
|
||||
|
||||
test('encodes special characters', () => {
|
||||
const encoded = plantumlEncode('@startuml');
|
||||
expect(encoded).toMatch(/^~h[0-9a-f]+$/);
|
||||
// '@' is 0x40, 's' is 0x73
|
||||
expect(encoded.startsWith('~h40')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slug generation for TOC anchors', () => {
|
||||
const slugify = (text) => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
};
|
||||
|
||||
test('converts heading to slug', () => {
|
||||
expect(slugify('Hello World')).toBe('hello-world');
|
||||
});
|
||||
|
||||
test('removes special characters', () => {
|
||||
expect(slugify('What is C++?')).toBe('what-is-c');
|
||||
});
|
||||
|
||||
test('collapses multiple dashes', () => {
|
||||
expect(slugify('Hello World')).toBe('hello-world');
|
||||
});
|
||||
|
||||
test('handles already lowercase text', () => {
|
||||
expect(slugify('simple')).toBe('simple');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for PrintPreview
|
||||
* Tests print dialog options, open/close, and preview rendering
|
||||
*/
|
||||
|
||||
describe('PrintPreview', () => {
|
||||
beforeEach(() => {
|
||||
// Mock electron require for executePrint
|
||||
jest.mock('electron', () => ({
|
||||
ipcRenderer: { send: jest.fn(), invoke: jest.fn() }
|
||||
}), { virtual: true });
|
||||
|
||||
document.body.innerHTML = `
|
||||
<div class="dialog-overlay hidden" id="print-preview-overlay">
|
||||
<button id="print-preview-close"></button>
|
||||
<button id="print-cancel"></button>
|
||||
<button id="print-execute"></button>
|
||||
<select id="print-paper-size"><option value="A4">A4</option><option value="Letter">Letter</option></select>
|
||||
<select id="print-orientation"><option value="portrait">Portrait</option><option value="landscape">Landscape</option></select>
|
||||
<select id="print-margins"><option value="default">Default</option></select>
|
||||
<input type="range" id="print-scale" value="100">
|
||||
<span id="print-scale-value">100%</span>
|
||||
<input type="checkbox" id="print-headers" checked>
|
||||
<input type="checkbox" id="print-background" checked>
|
||||
<select id="print-pages"><option value="all">All</option><option value="custom">Custom</option></select>
|
||||
<input type="text" id="print-page-range" class="hidden">
|
||||
<iframe id="print-preview-frame"></iframe>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
test('getOptions returns default values', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
const options = preview.getOptions();
|
||||
expect(options.paperSize).toBe('A4');
|
||||
expect(options.orientation).toBe('portrait');
|
||||
expect(options.scale).toBe(100);
|
||||
expect(options.headers).toBe(true);
|
||||
expect(options.background).toBe(true);
|
||||
expect(options.pages).toBe('all');
|
||||
expect(options.margins).toBe('default');
|
||||
expect(options.pageRange).toBe('');
|
||||
});
|
||||
|
||||
test('opens and closes', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Test</p>');
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(false);
|
||||
preview.close();
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('updateScaleLabel reflects slider value', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
document.getElementById('print-scale').value = '75';
|
||||
preview.updateScaleLabel();
|
||||
expect(document.getElementById('print-scale-value').textContent).toBe('75%');
|
||||
});
|
||||
|
||||
test('close button closes preview', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Test</p>');
|
||||
document.getElementById('print-preview-close').click();
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('cancel button closes preview', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Test</p>');
|
||||
document.getElementById('print-cancel').click();
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('getOptions reflects changed values', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
|
||||
// Change paper size to Letter
|
||||
const paperSelect = document.getElementById('print-paper-size');
|
||||
paperSelect.value = 'Letter';
|
||||
|
||||
// Change scale
|
||||
document.getElementById('print-scale').value = '50';
|
||||
|
||||
// Uncheck headers
|
||||
document.getElementById('print-headers').checked = false;
|
||||
|
||||
const options = preview.getOptions();
|
||||
expect(options.paperSize).toBe('Letter');
|
||||
expect(options.scale).toBe(50);
|
||||
expect(options.headers).toBe(false);
|
||||
});
|
||||
|
||||
test('stores last content for refresh', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Hello World</p>');
|
||||
expect(preview._lastContent).toBe('<p>Hello World</p>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for SidebarManager
|
||||
* Tests panel registration, toggling, expand/collapse behavior
|
||||
*/
|
||||
|
||||
describe('SidebarManager', () => {
|
||||
let SidebarManager;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div class="sidebar collapsed" id="sidebar">
|
||||
<div class="sidebar-icons">
|
||||
<button class="sidebar-icon" data-panel="test1"></button>
|
||||
<button class="sidebar-icon" data-panel="test2"></button>
|
||||
</div>
|
||||
<div class="sidebar-panel" id="sidebar-panel">
|
||||
<div class="sidebar-panel-header">
|
||||
<span class="sidebar-panel-title"></span>
|
||||
<button class="sidebar-panel-close"></button>
|
||||
</div>
|
||||
<div class="sidebar-panel-content" id="sidebar-panel-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
SidebarManager = require('../src/sidebar/sidebar-manager').SidebarManager;
|
||||
});
|
||||
|
||||
test('starts collapsed', () => {
|
||||
const mgr = new SidebarManager();
|
||||
expect(document.getElementById('sidebar').classList.contains('collapsed')).toBe(true);
|
||||
});
|
||||
|
||||
test('expands on panel toggle', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: (c) => { c.innerHTML = 'hello'; } });
|
||||
mgr.togglePanel('test1');
|
||||
expect(document.getElementById('sidebar').classList.contains('collapsed')).toBe(false);
|
||||
expect(document.querySelector('.sidebar-panel-title').textContent).toBe('Test 1');
|
||||
});
|
||||
|
||||
test('collapses on second toggle', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: () => {} });
|
||||
mgr.togglePanel('test1');
|
||||
mgr.togglePanel('test1');
|
||||
expect(document.getElementById('sidebar').classList.contains('collapsed')).toBe(true);
|
||||
});
|
||||
|
||||
test('switches panels', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Panel 1', render: (c) => { c.innerHTML = 'one'; } });
|
||||
mgr.registerPanel('test2', { title: 'Panel 2', render: (c) => { c.innerHTML = 'two'; } });
|
||||
mgr.togglePanel('test1');
|
||||
mgr.togglePanel('test2');
|
||||
expect(document.querySelector('.sidebar-panel-title').textContent).toBe('Panel 2');
|
||||
expect(document.getElementById('sidebar-panel-content').innerHTML).toBe('two');
|
||||
});
|
||||
|
||||
test('collapse resets active panel', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test', render: () => {} });
|
||||
mgr.expand('test1');
|
||||
mgr.collapse();
|
||||
expect(mgr.activePanel).toBe(null);
|
||||
});
|
||||
|
||||
test('expand sets active icon', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: () => {} });
|
||||
mgr.expand('test1');
|
||||
const btn = document.querySelector('[data-panel="test1"]');
|
||||
expect(btn.classList.contains('active')).toBe(true);
|
||||
});
|
||||
|
||||
test('collapse removes active icon', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: () => {} });
|
||||
mgr.expand('test1');
|
||||
mgr.collapse();
|
||||
const btn = document.querySelector('[data-panel="test1"]');
|
||||
expect(btn.classList.contains('active')).toBe(false);
|
||||
});
|
||||
|
||||
test('expand with unregistered panel does nothing', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.expand('nonexistent');
|
||||
expect(document.getElementById('sidebar').classList.contains('collapsed')).toBe(true);
|
||||
expect(mgr.activePanel).toBe(null);
|
||||
});
|
||||
|
||||
test('render function receives panel content element', () => {
|
||||
const mgr = new SidebarManager();
|
||||
const renderFn = jest.fn();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: renderFn });
|
||||
mgr.expand('test1');
|
||||
expect(renderFn).toHaveBeenCalledWith(document.getElementById('sidebar-panel-content'));
|
||||
});
|
||||
|
||||
test('clicking sidebar icon toggles panel', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: () => {} });
|
||||
const btn = document.querySelector('[data-panel="test1"]');
|
||||
btn.click();
|
||||
expect(mgr.activePanel).toBe('test1');
|
||||
btn.click();
|
||||
expect(mgr.activePanel).toBe(null);
|
||||
});
|
||||
|
||||
test('clicking close button collapses sidebar', () => {
|
||||
const mgr = new SidebarManager();
|
||||
mgr.registerPanel('test1', { title: 'Test 1', render: () => {} });
|
||||
mgr.expand('test1');
|
||||
document.querySelector('.sidebar-panel-close').click();
|
||||
expect(mgr.activePanel).toBe(null);
|
||||
expect(document.getElementById('sidebar').classList.contains('collapsed')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user