/** * Unit Tests for Studio Panel Navigation * * These tests verify the panel navigation functions in studio.js * Run with: npm test (requires Jest and jsdom) */ // Mock DOM elements const mockElements = {}; function createMockElement(id, display = 'none') { return { id, style: { display }, classList: { _classes: new Set(), add(cls) { this._classes.add(cls); }, remove(cls) { this._classes.delete(cls); }, contains(cls) { return this._classes.has(cls); } } }; } // Setup mock DOM before tests function setupMockDOM() { mockElements['panel-compare'] = createMockElement('panel-compare', 'flex'); mockElements['panel-tiles'] = createMockElement('panel-tiles'); mockElements['panel-messenger'] = createMockElement('panel-messenger'); mockElements['panel-video'] = createMockElement('panel-video'); mockElements['panel-correction'] = createMockElement('panel-correction'); mockElements['panel-letters'] = createMockElement('panel-letters'); mockElements['studio-sub-menu'] = createMockElement('studio-sub-menu', 'flex'); mockElements['sub-worksheets'] = createMockElement('sub-worksheets'); mockElements['sub-tiles'] = createMockElement('sub-tiles'); mockElements['sidebar-studio'] = createMockElement('sidebar-studio'); mockElements['sidebar-correction'] = createMockElement('sidebar-correction'); mockElements['sidebar-messenger'] = createMockElement('sidebar-messenger'); mockElements['sidebar-video'] = createMockElement('sidebar-video'); mockElements['sidebar-letters'] = createMockElement('sidebar-letters'); global.document = { getElementById: (id) => mockElements[id] || null, querySelectorAll: (selector) => { if (selector === '.sidebar-item') { return Object.values(mockElements).filter(el => el.id.startsWith('sidebar-')); } if (selector === '.sidebar-sub-item') { return Object.values(mockElements).filter(el => el.id.startsWith('sub-')); } return []; } }; global.console = { log: jest.fn(), error: jest.fn() }; } // Import functions (in real setup, these would be imported from studio.js) function hideAllPanels() { const panels = [ 'panel-compare', 'panel-tiles', 'panel-correction', 'panel-letters', 'panel-messenger', 'panel-video' ]; panels.forEach(panelId => { const panel = document.getElementById(panelId); if (panel) { panel.style.display = 'none'; } }); } function hideStudioSubMenu() { const subMenu = document.getElementById('studio-sub-menu'); if (subMenu) { subMenu.style.display = 'none'; } } function updateSidebarActive(activeSidebarId) { document.querySelectorAll('.sidebar-item').forEach(item => { item.classList.remove('active'); }); const activeItem = document.getElementById(activeSidebarId); if (activeItem) { activeItem.classList.add('active'); } } function updateSubNavActive(activeSubId) { document.querySelectorAll('.sidebar-sub-item').forEach(item => { item.classList.remove('active'); }); const activeItem = document.getElementById(activeSubId); if (activeItem) { activeItem.classList.add('active'); } } function showWorksheetTab() { const panelCompare = document.getElementById('panel-compare'); const panelTiles = document.getElementById('panel-tiles'); if (panelCompare) panelCompare.style.display = 'flex'; if (panelTiles) panelTiles.style.display = 'none'; updateSubNavActive('sub-worksheets'); } function showTilesTab() { const panelCompare = document.getElementById('panel-compare'); const panelTiles = document.getElementById('panel-tiles'); if (panelCompare) panelCompare.style.display = 'none'; if (panelTiles) panelTiles.style.display = 'flex'; updateSubNavActive('sub-tiles'); } function showStudioPanel() { hideAllPanels(); const subMenu = document.getElementById('studio-sub-menu'); if (subMenu) { subMenu.style.display = 'flex'; } showWorksheetTab(); updateSidebarActive('sidebar-studio'); } function showCorrectionPanel() { hideAllPanels(); hideStudioSubMenu(); const correctionPanel = document.getElementById('panel-correction'); if (correctionPanel) { correctionPanel.style.display = 'flex'; } updateSidebarActive('sidebar-correction'); } function showMessengerPanel() { hideAllPanels(); hideStudioSubMenu(); const messengerPanel = document.getElementById('panel-messenger'); if (messengerPanel) { messengerPanel.style.display = 'flex'; } updateSidebarActive('sidebar-messenger'); } function showVideoPanel() { hideAllPanels(); hideStudioSubMenu(); const videoPanel = document.getElementById('panel-video'); if (videoPanel) { videoPanel.style.display = 'flex'; } updateSidebarActive('sidebar-video'); } // Legacy alias function showCommunicationPanel() { showMessengerPanel(); } function showLettersPanel() { hideAllPanels(); hideStudioSubMenu(); const lettersPanel = document.getElementById('panel-letters'); if (lettersPanel) { lettersPanel.style.display = 'flex'; } updateSidebarActive('sidebar-letters'); } // Tests describe('Panel Navigation Functions', () => { beforeEach(() => { setupMockDOM(); }); describe('hideAllPanels', () => { test('should hide all panels', () => { // Set some panels to visible mockElements['panel-compare'].style.display = 'flex'; mockElements['panel-tiles'].style.display = 'flex'; hideAllPanels(); expect(mockElements['panel-compare'].style.display).toBe('none'); expect(mockElements['panel-tiles'].style.display).toBe('none'); expect(mockElements['panel-messenger'].style.display).toBe('none'); expect(mockElements['panel-video'].style.display).toBe('none'); expect(mockElements['panel-correction'].style.display).toBe('none'); expect(mockElements['panel-letters'].style.display).toBe('none'); }); }); describe('hideStudioSubMenu', () => { test('should hide studio sub-menu', () => { mockElements['studio-sub-menu'].style.display = 'flex'; hideStudioSubMenu(); expect(mockElements['studio-sub-menu'].style.display).toBe('none'); }); }); describe('showWorksheetTab', () => { test('should show panel-compare and hide panel-tiles', () => { mockElements['panel-tiles'].style.display = 'flex'; showWorksheetTab(); expect(mockElements['panel-compare'].style.display).toBe('flex'); expect(mockElements['panel-tiles'].style.display).toBe('none'); }); test('should activate sub-worksheets in sub-navigation', () => { showWorksheetTab(); expect(mockElements['sub-worksheets'].classList.contains('active')).toBe(true); expect(mockElements['sub-tiles'].classList.contains('active')).toBe(false); }); }); describe('showTilesTab', () => { test('should show panel-tiles and hide panel-compare', () => { mockElements['panel-compare'].style.display = 'flex'; showTilesTab(); expect(mockElements['panel-compare'].style.display).toBe('none'); expect(mockElements['panel-tiles'].style.display).toBe('flex'); }); test('should activate sub-tiles in sub-navigation', () => { showTilesTab(); expect(mockElements['sub-tiles'].classList.contains('active')).toBe(true); expect(mockElements['sub-worksheets'].classList.contains('active')).toBe(false); }); }); describe('showStudioPanel', () => { test('should hide all panels first', () => { mockElements['panel-correction'].style.display = 'flex'; showStudioPanel(); expect(mockElements['panel-correction'].style.display).toBe('none'); }); test('should show sub-menu', () => { mockElements['studio-sub-menu'].style.display = 'none'; showStudioPanel(); expect(mockElements['studio-sub-menu'].style.display).toBe('flex'); }); test('should show worksheet tab by default', () => { showStudioPanel(); expect(mockElements['panel-compare'].style.display).toBe('flex'); expect(mockElements['panel-tiles'].style.display).toBe('none'); }); test('should activate sidebar-studio', () => { showStudioPanel(); expect(mockElements['sidebar-studio'].classList.contains('active')).toBe(true); }); }); describe('showCorrectionPanel', () => { test('should hide all panels and show correction panel', () => { mockElements['panel-compare'].style.display = 'flex'; showCorrectionPanel(); expect(mockElements['panel-compare'].style.display).toBe('none'); expect(mockElements['panel-correction'].style.display).toBe('flex'); }); test('should hide studio sub-menu', () => { mockElements['studio-sub-menu'].style.display = 'flex'; showCorrectionPanel(); expect(mockElements['studio-sub-menu'].style.display).toBe('none'); }); test('should activate sidebar-correction', () => { showCorrectionPanel(); expect(mockElements['sidebar-correction'].classList.contains('active')).toBe(true); }); }); describe('showMessengerPanel', () => { test('should hide all panels and show messenger panel', () => { mockElements['panel-compare'].style.display = 'flex'; showMessengerPanel(); expect(mockElements['panel-compare'].style.display).toBe('none'); expect(mockElements['panel-messenger'].style.display).toBe('flex'); }); test('should hide studio sub-menu', () => { mockElements['studio-sub-menu'].style.display = 'flex'; showMessengerPanel(); expect(mockElements['studio-sub-menu'].style.display).toBe('none'); }); test('should activate sidebar-messenger', () => { showMessengerPanel(); expect(mockElements['sidebar-messenger'].classList.contains('active')).toBe(true); }); }); describe('showVideoPanel', () => { test('should hide all panels and show video panel', () => { mockElements['panel-compare'].style.display = 'flex'; showVideoPanel(); expect(mockElements['panel-compare'].style.display).toBe('none'); expect(mockElements['panel-video'].style.display).toBe('flex'); }); test('should hide studio sub-menu', () => { mockElements['studio-sub-menu'].style.display = 'flex'; showVideoPanel(); expect(mockElements['studio-sub-menu'].style.display).toBe('none'); }); test('should activate sidebar-video', () => { showVideoPanel(); expect(mockElements['sidebar-video'].classList.contains('active')).toBe(true); }); }); describe('showLettersPanel', () => { test('should hide all panels and show letters panel', () => { mockElements['panel-compare'].style.display = 'flex'; showLettersPanel(); expect(mockElements['panel-compare'].style.display).toBe('none'); expect(mockElements['panel-letters'].style.display).toBe('flex'); }); test('should hide studio sub-menu', () => { mockElements['studio-sub-menu'].style.display = 'flex'; showLettersPanel(); expect(mockElements['studio-sub-menu'].style.display).toBe('none'); }); }); }); describe('Sidebar Active State', () => { beforeEach(() => { setupMockDOM(); }); test('updateSidebarActive should remove active from all items', () => { mockElements['sidebar-studio'].classList.add('active'); mockElements['sidebar-correction'].classList.add('active'); updateSidebarActive('sidebar-letters'); expect(mockElements['sidebar-studio'].classList.contains('active')).toBe(false); expect(mockElements['sidebar-correction'].classList.contains('active')).toBe(false); expect(mockElements['sidebar-letters'].classList.contains('active')).toBe(true); }); }); describe('Sub-Navigation Active State', () => { beforeEach(() => { setupMockDOM(); }); test('updateSubNavActive should toggle active state correctly', () => { mockElements['sub-worksheets'].classList.add('active'); updateSubNavActive('sub-tiles'); expect(mockElements['sub-worksheets'].classList.contains('active')).toBe(false); expect(mockElements['sub-tiles'].classList.contains('active')).toBe(true); }); }); // ============================================ // JITSI VIDEOKONFERENZ MODULE TESTS // ============================================ // Mock additional elements for Jitsi tests function setupJitsiMockDOM() { setupMockDOM(); // Jitsi-specific elements mockElements['meeting-name'] = { id: 'meeting-name', value: 'Test Meeting', style: { display: 'block' } }; mockElements['jitsi-container'] = createMockElement('jitsi-container'); mockElements['jitsi-container'].innerHTML = ''; mockElements['jitsi-placeholder'] = createMockElement('jitsi-placeholder', 'flex'); mockElements['jitsi-controls'] = createMockElement('jitsi-controls'); mockElements['btn-mute'] = createMockElement('btn-mute'); mockElements['btn-mute'].textContent = '🎤 Stumm'; mockElements['btn-video'] = createMockElement('btn-video'); mockElements['btn-video'].textContent = '📹 Video aus'; mockElements['meeting-link-display'] = createMockElement('meeting-link-display'); mockElements['meeting-url'] = { id: 'meeting-url', textContent: '', style: { display: 'block' } }; // Override document methods for extended elements global.document.getElementById = (id) => mockElements[id] || null; global.document.createElement = (tag) => { return { tagName: tag.toUpperCase(), style: {}, setAttribute: jest.fn(), appendChild: jest.fn() }; }; // Mock clipboard API global.navigator = { clipboard: { writeText: jest.fn().mockResolvedValue(undefined) } }; // Mock alert global.alert = jest.fn(); } // Jitsi module state let currentJitsiMeetingUrl = null; let jitsiMicMuted = false; let jitsiVideoOff = false; // Jitsi functions (copied from studio.js for testing) async function startInstantMeeting() { console.log('Starting instant meeting...'); const meetingNameEl = document.getElementById('meeting-name'); const meetingName = meetingNameEl?.value || ''; try { const roomId = 'bp-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5); const jitsiDomain = 'meet.jit.si'; currentJitsiMeetingUrl = `https://${jitsiDomain}/${roomId}`; const container = document.getElementById('jitsi-container'); const placeholder = document.getElementById('jitsi-placeholder'); const controls = document.getElementById('jitsi-controls'); const linkDisplay = document.getElementById('meeting-link-display'); const urlDisplay = document.getElementById('meeting-url'); if (placeholder) placeholder.style.display = 'none'; if (controls) controls.style.display = 'flex'; if (linkDisplay) linkDisplay.style.display = 'flex'; if (urlDisplay) urlDisplay.textContent = currentJitsiMeetingUrl; if (container) { const iframe = document.createElement('iframe'); iframe.setAttribute('src', `${currentJitsiMeetingUrl}#config.prejoinPageEnabled=false`); iframe.setAttribute('allow', 'camera; microphone; fullscreen; display-capture'); container.appendChild(iframe); } return { success: true, url: currentJitsiMeetingUrl }; } catch (error) { console.error('Error starting meeting:', error); return { success: false, error }; } } function leaveJitsiMeeting() { const container = document.getElementById('jitsi-container'); const placeholder = document.getElementById('jitsi-placeholder'); const controls = document.getElementById('jitsi-controls'); const linkDisplay = document.getElementById('meeting-link-display'); if (container) container.innerHTML = ''; if (placeholder) placeholder.style.display = 'flex'; if (controls) controls.style.display = 'none'; if (linkDisplay) linkDisplay.style.display = 'none'; currentJitsiMeetingUrl = null; jitsiMicMuted = false; jitsiVideoOff = false; } function toggleJitsiMute() { jitsiMicMuted = !jitsiMicMuted; const btn = document.getElementById('btn-mute'); if (btn) { btn.textContent = jitsiMicMuted ? '🔇 Unmute' : '🎤 Stumm'; } return jitsiMicMuted; } function toggleJitsiVideo() { jitsiVideoOff = !jitsiVideoOff; const btn = document.getElementById('btn-video'); if (btn) { btn.textContent = jitsiVideoOff ? '📷 Video an' : '📹 Video aus'; } return jitsiVideoOff; } async function copyMeetingLink() { if (currentJitsiMeetingUrl) { await navigator.clipboard.writeText(currentJitsiMeetingUrl); alert('Meeting-Link wurde kopiert!'); return true; } return false; } function joinScheduledMeeting(meetingId) { console.log('Joining scheduled meeting:', meetingId); const jitsiDomain = 'meet.jit.si'; currentJitsiMeetingUrl = `https://${jitsiDomain}/${meetingId}`; const container = document.getElementById('jitsi-container'); const placeholder = document.getElementById('jitsi-placeholder'); const controls = document.getElementById('jitsi-controls'); if (placeholder) placeholder.style.display = 'none'; if (controls) controls.style.display = 'flex'; if (container) { const iframe = document.createElement('iframe'); iframe.setAttribute('src', `${currentJitsiMeetingUrl}#config.prejoinPageEnabled=false`); container.appendChild(iframe); } return currentJitsiMeetingUrl; } describe('Jitsi Videokonferenz Module', () => { beforeEach(() => { setupJitsiMockDOM(); currentJitsiMeetingUrl = null; jitsiMicMuted = false; jitsiVideoOff = false; }); describe('startInstantMeeting', () => { test('should create a meeting URL with bp- prefix', async () => { const result = await startInstantMeeting(); expect(result.success).toBe(true); expect(result.url).toMatch(/^https:\/\/meet\.jit\.si\/bp-/); expect(currentJitsiMeetingUrl).toBe(result.url); }); test('should hide placeholder and show controls', async () => { await startInstantMeeting(); expect(mockElements['jitsi-placeholder'].style.display).toBe('none'); expect(mockElements['jitsi-controls'].style.display).toBe('flex'); expect(mockElements['meeting-link-display'].style.display).toBe('flex'); }); test('should update meeting URL display', async () => { await startInstantMeeting(); expect(mockElements['meeting-url'].textContent).toMatch(/^https:\/\/meet\.jit\.si\/bp-/); }); }); describe('leaveJitsiMeeting', () => { test('should reset all meeting state', async () => { await startInstantMeeting(); expect(currentJitsiMeetingUrl).not.toBeNull(); leaveJitsiMeeting(); expect(currentJitsiMeetingUrl).toBeNull(); expect(jitsiMicMuted).toBe(false); expect(jitsiVideoOff).toBe(false); }); test('should show placeholder and hide controls', async () => { await startInstantMeeting(); leaveJitsiMeeting(); expect(mockElements['jitsi-placeholder'].style.display).toBe('flex'); expect(mockElements['jitsi-controls'].style.display).toBe('none'); expect(mockElements['meeting-link-display'].style.display).toBe('none'); }); }); describe('toggleJitsiMute', () => { test('should toggle mute state', () => { expect(jitsiMicMuted).toBe(false); const result1 = toggleJitsiMute(); expect(result1).toBe(true); expect(jitsiMicMuted).toBe(true); const result2 = toggleJitsiMute(); expect(result2).toBe(false); expect(jitsiMicMuted).toBe(false); }); test('should update button text', () => { toggleJitsiMute(); expect(mockElements['btn-mute'].textContent).toBe('🔇 Unmute'); toggleJitsiMute(); expect(mockElements['btn-mute'].textContent).toBe('🎤 Stumm'); }); }); describe('toggleJitsiVideo', () => { test('should toggle video state', () => { expect(jitsiVideoOff).toBe(false); const result1 = toggleJitsiVideo(); expect(result1).toBe(true); expect(jitsiVideoOff).toBe(true); const result2 = toggleJitsiVideo(); expect(result2).toBe(false); expect(jitsiVideoOff).toBe(false); }); test('should update button text', () => { toggleJitsiVideo(); expect(mockElements['btn-video'].textContent).toBe('📷 Video an'); toggleJitsiVideo(); expect(mockElements['btn-video'].textContent).toBe('📹 Video aus'); }); }); describe('copyMeetingLink', () => { test('should copy meeting URL to clipboard when active', async () => { await startInstantMeeting(); const result = await copyMeetingLink(); expect(result).toBe(true); expect(navigator.clipboard.writeText).toHaveBeenCalledWith(currentJitsiMeetingUrl); expect(alert).toHaveBeenCalledWith('Meeting-Link wurde kopiert!'); }); test('should return false when no active meeting', async () => { const result = await copyMeetingLink(); expect(result).toBe(false); expect(navigator.clipboard.writeText).not.toHaveBeenCalled(); }); }); describe('joinScheduledMeeting', () => { test('should create meeting URL with provided ID', () => { const meetingId = 'scheduled-meeting-123'; const url = joinScheduledMeeting(meetingId); expect(url).toBe(`https://meet.jit.si/${meetingId}`); expect(currentJitsiMeetingUrl).toBe(url); }); test('should hide placeholder and show controls', () => { joinScheduledMeeting('test-meeting'); expect(mockElements['jitsi-placeholder'].style.display).toBe('none'); expect(mockElements['jitsi-controls'].style.display).toBe('flex'); }); }); }); // ============================================ // MATRIX MESSENGER MODULE TESTS // ============================================ // Messenger stub functions (copied from studio.js) function startQuickMeeting() { showVideoPanel(); // In real implementation: setTimeout(() => startInstantMeeting(), 100); return 'video-panel-shown'; } function createClassRoom() { console.log('Creating class room...'); alert('Klassenraum-Erstellung wird in Phase 2 implementiert.'); return 'stub'; } function scheduleParentMeeting() { console.log('Scheduling parent meeting...'); alert('Elterngespräch-Planung wird in Phase 2 implementiert.'); return 'stub'; } function selectRoom(roomId) { console.log('Selecting room:', roomId); // Stub: Would load room messages return roomId; } function sendMessage() { const input = document.getElementById('messenger-input'); const message = input?.value?.trim(); if (!message) { console.log('Empty message, not sending'); return false; } console.log('Sending message:', message); // Stub: Would send via Matrix API if (input) input.value = ''; return true; } // Setup Messenger mock DOM function setupMessengerMockDOM() { setupMockDOM(); mockElements['messenger-input'] = { id: 'messenger-input', value: '', style: { display: 'block' } }; } describe('Matrix Messenger Module', () => { beforeEach(() => { setupMessengerMockDOM(); }); describe('startQuickMeeting', () => { test('should show video panel', () => { const result = startQuickMeeting(); expect(result).toBe('video-panel-shown'); expect(mockElements['panel-video'].style.display).toBe('flex'); }); }); describe('createClassRoom', () => { test('should return stub indicator', () => { const result = createClassRoom(); expect(result).toBe('stub'); expect(alert).toHaveBeenCalledWith('Klassenraum-Erstellung wird in Phase 2 implementiert.'); }); }); describe('scheduleParentMeeting', () => { test('should return stub indicator', () => { const result = scheduleParentMeeting(); expect(result).toBe('stub'); expect(alert).toHaveBeenCalledWith('Elterngespräch-Planung wird in Phase 2 implementiert.'); }); }); describe('selectRoom', () => { test('should return room ID', () => { const roomId = 'room-123'; const result = selectRoom(roomId); expect(result).toBe(roomId); }); }); describe('sendMessage', () => { test('should return false for empty message', () => { mockElements['messenger-input'].value = ''; const result = sendMessage(); expect(result).toBe(false); }); test('should return false for whitespace-only message', () => { mockElements['messenger-input'].value = ' '; const result = sendMessage(); expect(result).toBe(false); }); test('should return true and clear input for valid message', () => { mockElements['messenger-input'].value = 'Hello World'; const result = sendMessage(); expect(result).toBe(true); expect(mockElements['messenger-input'].value).toBe(''); }); }); }); // ============================================ // INTEGRATION TESTS // ============================================ describe('Panel Integration', () => { beforeEach(() => { setupJitsiMockDOM(); }); test('switching from messenger to video should preserve panel state', () => { showMessengerPanel(); expect(mockElements['panel-messenger'].style.display).toBe('flex'); expect(mockElements['sidebar-messenger'].classList.contains('active')).toBe(true); showVideoPanel(); expect(mockElements['panel-messenger'].style.display).toBe('none'); expect(mockElements['panel-video'].style.display).toBe('flex'); expect(mockElements['sidebar-messenger'].classList.contains('active')).toBe(false); expect(mockElements['sidebar-video'].classList.contains('active')).toBe(true); }); test('video panel should be accessible via startQuickMeeting', () => { showMessengerPanel(); startQuickMeeting(); expect(mockElements['panel-video'].style.display).toBe('flex'); expect(mockElements['sidebar-video'].classList.contains('active')).toBe(true); }); });