const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert'); // Create a simple mock for DOM const createMockElement = () => ({ style: {}, innerHTML: '', textContent: '', dataset: {}, className: '', addEventListener: () => {}, removeEventListener: () => {}, appendChild: () => {}, insertBefore: () => {}, remove: () => {}, querySelector: () => null, querySelectorAll: () => [], setAttribute: () => {}, removeAttribute: () => {}, hasAttribute: () => false, closest: () => null, contains: () => false, focus: () => {}, blur: () => {}, click: () => {}, dispatchEvent: () => true, classList: { add: () => {}, remove: () => {} }, get parentElement() { return null; } }); // Mock DOM global.document = { getElementById: () => createMockElement(), querySelector: () => null, querySelectorAll: () => [], addEventListener: () => {}, removeEventListener: () => {}, createElement: () => createMockElement(), createRange: () => ({ selectNodeContents: () => {} }), getSelection: () => ({ addRange: () => {}, removeAllRanges: () => {} }) }; global.window = { addEventListener: () => {}, dispatchEvent: () => true, getComputedStyle: () => ({ display: 'block' }) }; // Mock ipcRenderer with proper module handling const mockIpcRenderer = { invoke: async (channel, ...args) => { switch (channel) { case 'select-directory': return { success: true, directory: '/test/dir' }; case 'scan-directory': return { success: true, files: [] }; case 'rename-file': return { success: true, message: 'Renamed' }; case 'begin-mapping': return { success: true }; case 'log-audit-event': return { success: true }; case 'move-file-to-folder': return { success: true }; case 'log-file-info': return { success: true, fileInfo: {} }; case 'search-tvdb': return { success: true, results: [] }; case 'get-show-details': return { success: true, data: { seasons: [] } }; case 'get-season-episodes': return { success: true, data: { episodes: [] } }; case 'test-tvdb-api': return { success: true, message: 'OK' }; default: return { success: true, data: null }; } }, on: () => {} }; // Mock fs const mockFs = { existsSync: () => true, statSync: () => ({ size: 1024 * 1024 }), mkdirSync: () => {}, renameSync: () => {} }; // Set up module mocks before loading anything const Module = require('module'); const path = require('path'); const originalRequire = Module.prototype.require.bind(Module.prototype); Module.prototype.require = function(id) { if (id === 'electron') return mockIpcRenderer; if (id === 'fs') return mockFs; // For UIManager, intercept and return a mock if (id.includes('UIManager')) { // We'll handle UIManager separately return null; } // For relative paths in renderer, resolve properly if (id.startsWith('./')) { try { return originalRequire(id); } catch (e) { // Try without .js extension const idNoExt = id.replace(/\.js$/, ''); return originalRequire(idNoExt); } } return originalRequire(id); }; // Now we can safely require the modules const AppState = require('./utils/renderer/AppState.js'); const EpisodeManager = require('./utils/renderer/EpisodeManager.js'); const TagManager = require('./utils/renderer/TagManager.js'); const SearchManager = require('./utils/renderer/SearchManager.js'); const FileManager = require('./utils/renderer/FileManager.js'); const ModalManager = require('./utils/renderer/ModalManager.js'); const ProgressManager = require('./utils/renderer/ProgressManager.js'); const FileListManager = require('./utils/renderer/FileListManager.js'); // Create a separate mock UIManager for integration tests class MockUIManager { constructor() { this.appState = new AppState(); } } // ============ AppState Tests ============ describe('AppState', () => { let appState; beforeEach(() => { appState = new AppState(); }); test('should create instance with default values', () => { assert.strictEqual(appState.currentDirectory, null); assert.deepStrictEqual(appState.currentFiles, []); assert.strictEqual(appState.currentShow, null); assert.deepStrictEqual(appState.currentSeasons, []); assert.deepStrictEqual(appState.currentEpisodes, []); assert.strictEqual(appState.selectedSeasonEpisodeCount, 0); }); test('should set and get current directory', () => { appState.setCurrentDirectory('/test/dir'); assert.strictEqual(appState.getCurrentDirectory(), '/test/dir'); }); test('should set and get current files', () => { const files = [{ name: 'file1.mp4' }, { name: 'file2.mp4' }]; appState.setCurrentFiles(files); assert.deepStrictEqual(appState.getCurrentFiles(), files); }); test('should set and get current show', () => { const show = { id: '123', name: 'Test Show' }; appState.setCurrentShow(show); assert.deepStrictEqual(appState.getCurrentShow(), show); }); test('should set and get current seasons', () => { const seasons = [{ number: 1 }, { number: 2 }]; appState.setCurrentSeasons(seasons); assert.strictEqual(appState.getCurrentSeasons().length, 2); }); test('should set and get current episodes', () => { const episodes = [{ number: 1 }, { number: 2 }]; appState.setCurrentEpisodes(episodes); assert.strictEqual(appState.getCurrentEpisodes().length, 2); }); test('should set and get selected season episode count', () => { appState.setSelectedSeasonEpisodeCount(10); assert.strictEqual(appState.getSelectedSeasonEpisodeCount(), 10); }); test('should set and get updating episode numbers flag', () => { appState.setUpdatingEpisodeNumbers(true); assert.strictEqual(appState.isUpdatingEpisodeNumbers(), true); }); test('should reset all state', () => { appState.setCurrentDirectory('/test'); appState.setCurrentFiles([{ name: 'file.mp4' }]); appState.setCurrentShow({ id: '1' }); appState.reset(); assert.strictEqual(appState.currentDirectory, null); assert.deepStrictEqual(appState.currentFiles, []); assert.strictEqual(appState.currentShow, null); }); }); // ============ EpisodeManager Tests ============ describe('EpisodeManager', () => { let episodeManager; let fileListEl; beforeEach(() => { episodeManager = new EpisodeManager(); fileListEl = global.document.getElementById('file-list'); }); test('should update episode numbers sequentially', () => { fileListEl.innerHTML = `
1
2
`; episodeManager.updateEpisodeNumbers(fileListEl); const episodes = fileListEl.querySelectorAll('.episode-number'); assert.strictEqual(episodes[0].textContent, '1'); assert.strictEqual(episodes[1].textContent, '2'); }); test('should handle episode ranges', () => { fileListEl.innerHTML = `
1-3
`; episodeManager.updateEpisodeNumbers(fileListEl); const episodes = fileListEl.querySelectorAll('.episode-number'); assert.strictEqual(episodes[0].textContent, '1'); }); test('should skip if already updating', () => { episodeManager.setIsUpdatingEpisodeNumbers(true); episodeManager.updateEpisodeNumbers(fileListEl); assert.strictEqual(episodeManager.getIsUpdatingEpisodeNumbers(), false); }); test('should make episode range editable', () => { const episodeEl = { contentEditable: false, textContent: '1', dataset: { episodeStart: '1', episodeEnd: '1' }, focus: () => {}, classList: { add: () => {} } }; episodeManager.makeEpisodeRangeEditable(episodeEl); assert.strictEqual(episodeEl.contentEditable, 'true'); }); test('should not edit if already editing', () => { const episodeEl = { contentEditable: 'true', textContent: '1', dataset: { episodeStart: '1', episodeEnd: '1' }, focus: () => {}, classList: { add: () => {} } }; episodeManager.makeEpisodeRangeEditable(episodeEl); assert.strictEqual(episodeEl.contentEditable, 'true'); }); test('should handle left arrow click', () => { const episodeEl = { dataset: { episodeStart: '5', episodeEnd: '5' }, setAttribute: () => {} }; const element = { closest: () => ({ dataset: { filePath: '/test/file.mp4' } }), querySelector: () => episodeEl }; const mockFileListEl = { querySelectorAll: () => [ { dataset: { filePath: '/test/file.mp4' } }, { dataset: { filePath: '/test/file2.mp4' } } ] }; episodeManager.handleEpisodeArrowClick(element, 'left', mockFileListEl); }); test('should handle right arrow click', () => { const episodeEl = { dataset: { episodeStart: '5', episodeEnd: '5' }, setAttribute: () => {} }; const element = { closest: () => ({ dataset: { filePath: '/test/file.mp4' } }), querySelector: () => episodeEl }; const mockFileListEl = { querySelectorAll: () => [ { dataset: { filePath: '/test/file.mp4' } }, { dataset: { filePath: '/test/file2.mp4' } } ] }; episodeManager.handleEpisodeArrowClick(element, 'right', mockFileListEl); }); test('should get episode range from element', () => { const episodeEl = { dataset: { episodeStart: '1', episodeEnd: '3' } }; const range = episodeManager.getEpisodeRange(episodeEl); assert.strictEqual(range.start, 1); assert.strictEqual(range.end, 3); }); test('should return single episode when no range', () => { const episodeEl = { dataset: { episodeStart: '5' } }; const range = episodeManager.getEpisodeRange(episodeEl); assert.strictEqual(range.start, 5); assert.strictEqual(range.end, 5); }); test('should calculate total episode count', () => { fileListEl.innerHTML = `
1-3
4-5
`; const count = episodeManager.calculateTotalEpisodeCount(fileListEl); assert.strictEqual(count, 5); }); test('should get last episode end', () => { fileListEl.innerHTML = `
1-3
4-5
`; const last = episodeManager.getLastEpisodeEnd(fileListEl); assert.strictEqual(last, 5); }); test('should highlight episodes in range', () => { const container = { querySelectorAll: () => [ { textContent: 'E1', classList: { add: () => {} } }, { textContent: 'E2', classList: { add: () => {} } }, { textContent: 'E5', classList: { add: () => {} } } ] }; episodeManager.highlightEpisodes(1, 2, container); }); test('should remove highlights from all episodes', () => { const container = { querySelectorAll: () => [ { classList: { remove: () => {} } }, { classList: { remove: () => {} } } ] }; episodeManager.removeEpisodeHighlights(container); }); }); // ============ TagManager Tests ============ describe('TagManager', () => { let tagManager; beforeEach(() => { tagManager = new TagManager(); }); test('should initialize with default tag colors', () => { assert.strictEqual(tagManager.tagColors.extra, '#FFD700'); assert.strictEqual(tagManager.tagColors.commentary, '#17a2b8'); assert.strictEqual(tagManager.tagColors.delete, '#dc3545'); }); test('should tag file', () => { tagManager.tagFile('/test/file.mp4', 'extra', () => {}); }); test('should untag file', () => { tagManager.untagFile('/test/file.mp4', 'extra'); }); test('should move tagged file', async () => { const result = await tagManager.moveTaggedFile('/test/file.mp4', 'extra'); assert.strictEqual(result.success, true); }); test('should move all tagged files', async () => { const result = await tagManager.moveAllTaggedFiles( () => {}, () => {}, () => {} ); assert.strictEqual(result.successful, 0); assert.strictEqual(result.failed, 0); }); test('should return array of tagged files', () => { const taggedFiles = tagManager.getTaggedFiles(); assert.deepStrictEqual(taggedFiles, []); }); }); // ============ SearchManager Tests ============ describe('SearchManager', () => { let searchManager; beforeEach(() => { const searchInputEl = global.document.getElementById('search-input'); const searchResultsEl = global.document.getElementById('search-results'); searchManager = new SearchManager(searchInputEl, searchResultsEl); }); test('should create instance with elements', () => { assert.ok(searchManager.searchInputEl); assert.ok(searchManager.searchResultsEl); }); test('should search with valid query', async () => { const result = await searchManager.searchShows('test'); assert.ok(result.success || result.success === undefined); }); test('should return empty results for short query', async () => { const result = await searchManager.searchShows('a'); assert.deepStrictEqual(result.results, []); }); test('should display search results', () => { const results = [ { seriesName: 'Show 1', firstAired: '2020-01-01' }, { name: 'Show 2', firstAired: '2021-01-01' } ]; searchManager.displaySearchResults(results); assert.strictEqual(searchManager.searchResultsEl.children.length, 2); }); test('should handle empty results', () => { searchManager.displaySearchResults([]); assert.ok(searchManager.searchResultsEl.innerHTML.includes('No shows found')); }); test('should handle results without seriesName', () => { const results = [{ name: 'Show with name property' }]; searchManager.displaySearchResults(results); assert.ok(searchManager.searchResultsEl.innerHTML.includes('Show with name property')); }); test('should clear search results', () => { searchManager.searchResultsEl.innerHTML = '
Test
'; searchManager.clearSearchResults(); assert.strictEqual(searchManager.searchResultsEl.innerHTML, ''); }); test('should select show and update title', async () => { const show = { id: '123', seriesName: 'Test Show' }; let titleUpdated = false; await searchManager.selectShow(show, (title) => { titleUpdated = true; }, () => {} ); assert.strictEqual(titleUpdated, true); }); test('should display seasons with episode counts', () => { const seasons = [ { number: 1, episodeCount: 10, type: 'Season' }, { number: 2, episodeCount: 12, type: 'Special' } ]; searchManager.displaySeasons(seasons, () => {}, () => {}); assert.ok(searchManager.searchResultsEl.innerHTML.includes('Season 1')); }); test('should handle empty seasons', () => { searchManager.displaySeasons([], () => {}, () => {}); assert.ok(searchManager.searchResultsEl.innerHTML.includes('No seasons available')); }); test('should fetch and display episodes', async () => { const onEpisodesDisplay = (episodes, error) => { if (error) assert.ok(error); }; await searchManager.fetchAndDisplayEpisodes('123', 1, onEpisodesDisplay); }); test('should display episodes with back button', () => { const episodes = [ { number: 1, name: 'Episode 1', runtime: 30 }, { number: 2, name: 'Episode 2', runtime: 45 } ]; searchManager.displayEpisodes(episodes, 2, () => {}, () => {}); assert.ok(searchManager.searchResultsEl.innerHTML.includes('E1')); }); test('should test TVDB API connectivity', async () => { const result = await searchManager.testTVDBAPI(); assert.ok(result.success || result.success === undefined); }); }); // ============ FileManager Tests ============ describe('FileManager', () => { let fileManager; beforeEach(() => { fileManager = new FileManager(); }); test('should select directory', async () => { const result = await fileManager.selectDirectory(); assert.strictEqual(result.success, true); }); test('should scan directory', async () => { const result = await fileManager.scanDirectory('/test/dir'); assert.strictEqual(result.success, true); }); test('should rename file', async () => { const result = await fileManager.renameFile('/test/old.mp4', 'new.mp4'); assert.strictEqual(result.success, true); }); test('should begin mapping files', async () => { const result = await fileManager.beginMapping('/test/dir', [], null); assert.strictEqual(result.success, true); }); test('should log audit event', async () => { const result = await fileManager.logAuditEvent('/test/dir', 'test_action', { key: 'value' }); assert.strictEqual(result.success, true); }); test('should move file to folder', async () => { const result = await fileManager.moveFileToFolder('/test/file.mp4', 'extras'); assert.strictEqual(result.success, true); }); test('should log file info', async () => { const result = await fileManager.logFileInfo('/test/file.mp4'); assert.strictEqual(result.success, true); }); test('should collect file data from UI', () => { const fileListEl = { querySelectorAll: () => [ { querySelector: (selector) => { if (selector === '.file-name') return { dataset: { filePath: '/test/file.mp4' } }; if (selector === '.file-quality') return { textContent: '1080p' }; if (selector === '.episode-number') return { dataset: { episodeStart: '1', episodeEnd: '1' } }; return null; } } ] }; const files = fileManager.collectFileData(fileListEl); assert.strictEqual(files.length, 1); assert.strictEqual(files[0].filePath, '/test/file.mp4'); assert.strictEqual(files[0].quality, '1080p'); }); }); // ============ ModalManager Tests ============ describe('ModalManager', () => { let modalManager; beforeEach(() => { modalManager = new ModalManager(); }); test('should create instance with null modal', () => { assert.strictEqual(modalManager.getModal(), null); }); test('should create modal element', () => { modalManager.createVideoPreviewModal(); const modal = modalManager.getModal(); assert.ok(modal !== null); assert.strictEqual(modal.id, 'video-preview-modal'); }); test('should open video preview modal', async () => { modalManager.createVideoPreviewModal(); await modalManager.openVideoPreview('/test/file.mp4'); const modal = modalManager.getModal(); assert.strictEqual(modal.style.display, 'block'); }); test('should close video preview modal', () => { modalManager.createVideoPreviewModal(); modalManager.closeVideoPreview(); const modal = modalManager.getModal(); assert.strictEqual(modal.style.display, 'none'); }); test('should return modal element', () => { const modal = modalManager.getModal(); assert.ok(modal === null || modal.id === 'video-preview-modal'); }); }); // ============ ProgressManager Tests ============ describe('ProgressManager', () => { let progressManager; let progressContainer; beforeEach(() => { progressContainer = { style: {} }; progressManager = new ProgressManager(progressContainer, {}, {}); }); test('should create instance with elements', () => { assert.strictEqual(progressManager.progressContainer, progressContainer); }); test('should show progress with default message', () => { progressManager.showProgress(); assert.strictEqual(progressContainer.style.display, 'block'); }); test('should show progress with custom message', () => { progressManager.showProgress('Custom message'); assert.strictEqual(progressContainer.style.display, 'block'); }); test('should hide progress', () => { progressManager.showProgress(); progressManager.hideProgress(); assert.strictEqual(progressContainer.style.display, 'none'); }); test('should update progress with current/total', () => { progressManager.updateProgress(5, 10, 'test.mp4'); }); test('should update progress text', () => { progressManager.updateProgressText('Testing'); }); test('should update progress count', () => { progressManager.updateProgressCount(3, 10); }); test('should show directory feedback', () => { progressManager.showDirectoryFeedback('extras'); }); }); // ============ FileListManager Tests ============ describe('FileListManager', () => { let fileListManager; let fileListEl; beforeEach(() => { fileListEl = global.document.getElementById('file-list'); fileListManager = new FileListManager(fileListEl); }); test('should create instance with file list element', () => { assert.strictEqual(fileListManager.fileListEl, fileListEl); }); test('should display empty message when no files', () => { fileListManager.displayFiles([]); assert.ok(fileListEl.innerHTML.includes('No media files found')); }); test('should display folders first', () => { const files = [ { name: 'Folder', path: '/folder', isFolder: true }, { name: 'file.mp4', path: '/file.mp4', isFolder: false } ]; fileListManager.displayFiles(files); assert.ok(fileListEl.innerHTML.includes('📁')); }); test('should display media files with episode numbers', () => { const files = [ { name: 'file1.mp4', path: '/file1.mp4', isFolder: false }, { name: 'file2.mp4', path: '/file2.mp4', isFolder: false } ]; fileListManager.displayFiles(files); assert.ok(fileListEl.innerHTML.includes('1')); }); test('should update episode numbers after reordering', () => { fileListEl.innerHTML = `
2
1
`; fileListManager.updateEpisodeNumbers(); }); test('should return only media file items', () => { fileListEl.innerHTML = `
Folder
File 1
File 2
`; const items = fileListManager.getMediaFileItems(); assert.strictEqual(items.length, 2); }); test('should return only folder items', () => { fileListEl.innerHTML = `
Folder 1
Folder 2
File
`; const items = fileListManager.getFolderItems(); assert.strictEqual(items.length, 2); }); test('should clear file list', () => { fileListEl.innerHTML = '
Test
'; fileListManager.clear(); assert.strictEqual(fileListEl.innerHTML, ''); }); }); // ============ UIManager Integration Tests (Separate File) ============ // Note: UIManager tests are in a separate file due to complex dependencies