Refactor renderer.js into modular class-based architecture
- Split renderer.js into 9 manager classes following SOLID principles: * AppState - State management * EpisodeManager - Episode numbering and range editing * TagManager - File tagging system * SearchManager - TheTVDB search integration * FileManager - Directory and file operations * ModalManager - Video preview modals * ProgressManager - Progress display * FileListManager - File list rendering and drag/drop * UIManager - Main UI coordinator - Add comprehensive test suite (test-renderer-classes-comprehensive.js) with 100+ tests covering all business logic - Fix drag/drop handler 'this' binding issues - Add CSS for highlighted episodes and improved file list styling - Restore all original functionality including episode cascading, tagging system, TVDB search, and Jellyfin naming
This commit is contained in:
parent
759139a6f8
commit
4d7d476a44
@ -169,6 +169,13 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode-item.highlighted-episode {
|
||||||
|
background-color: #e94560;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* Main Content - Files */
|
/* Main Content - Files */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -238,12 +245,14 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-list {
|
#file-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-list > p {
|
#file-list > p {
|
||||||
|
|||||||
736
test-renderer-classes-comprehensive.js
Normal file
736
test-renderer-classes-comprehensive.js
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
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 = `
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="1" data-episode-end="1">1</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="2" data-episode-end="2">2</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="1" data-episode-end="3">1-3</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="1" data-episode-end="3">1-3</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="4" data-episode-end="5">4-5</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const count = episodeManager.calculateTotalEpisodeCount(fileListEl);
|
||||||
|
assert.strictEqual(count, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get last episode end', () => {
|
||||||
|
fileListEl.innerHTML = `
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="1" data-episode-end="3">1-3</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="4" data-episode-end="5">4-5</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = '<div>Test</div>';
|
||||||
|
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 = `
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="2" data-episode-end="2">2</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-item">
|
||||||
|
<div class="episode-number" data-episode-start="1" data-episode-end="1">1</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
fileListManager.updateEpisodeNumbers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return only media file items', () => {
|
||||||
|
fileListEl.innerHTML = `
|
||||||
|
<div class="file-item folder-item">Folder</div>
|
||||||
|
<div class="file-item">File 1</div>
|
||||||
|
<div class="file-item">File 2</div>
|
||||||
|
`;
|
||||||
|
const items = fileListManager.getMediaFileItems();
|
||||||
|
assert.strictEqual(items.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return only folder items', () => {
|
||||||
|
fileListEl.innerHTML = `
|
||||||
|
<div class="file-item folder-item">Folder 1</div>
|
||||||
|
<div class="file-item folder-item">Folder 2</div>
|
||||||
|
<div class="file-item">File</div>
|
||||||
|
`;
|
||||||
|
const items = fileListManager.getFolderItems();
|
||||||
|
assert.strictEqual(items.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear file list', () => {
|
||||||
|
fileListEl.innerHTML = '<div>Test</div>';
|
||||||
|
fileListManager.clear();
|
||||||
|
assert.strictEqual(fileListEl.innerHTML, '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============ UIManager Integration Tests (Separate File) ============
|
||||||
|
// Note: UIManager tests are in a separate file due to complex dependencies
|
||||||
@ -280,6 +280,22 @@ class EpisodeManager {
|
|||||||
|
|
||||||
return lastEpisodeEnd;
|
return lastEpisodeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get updating episode numbers flag
|
||||||
|
* @returns {boolean} Updating state
|
||||||
|
*/
|
||||||
|
getIsUpdatingEpisodeNumbers() {
|
||||||
|
return this.isUpdatingEpisodeNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set updating episode numbers flag
|
||||||
|
* @param {boolean} updating - Updating state
|
||||||
|
*/
|
||||||
|
setIsUpdatingEpisodeNumbers(updating) {
|
||||||
|
this.isUpdatingEpisodeNumbers = updating;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = EpisodeManager;
|
module.exports = EpisodeManager;
|
||||||
@ -120,10 +120,11 @@ class FileListManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_handleDragStart(e) {
|
_handleDragStart(e) {
|
||||||
this.draggedItem = this;
|
const fileItem = this;
|
||||||
this.element.classList.add('dragging');
|
this.draggedItem = fileItem;
|
||||||
|
fileItem.classList.add('dragging');
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', this.dataset.index);
|
e.dataTransfer.setData('text/plain', fileItem.dataset.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,7 +133,8 @@ class FileListManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_handleDragEnd(e) {
|
_handleDragEnd(e) {
|
||||||
this.element.classList.remove('dragging');
|
const fileItem = this;
|
||||||
|
fileItem.classList.remove('dragging');
|
||||||
// Remove drag-over class from all items
|
// Remove drag-over class from all items
|
||||||
document.querySelectorAll('.file-item').forEach(item => {
|
document.querySelectorAll('.file-item').forEach(item => {
|
||||||
item.classList.remove('drag-over');
|
item.classList.remove('drag-over');
|
||||||
@ -146,12 +148,13 @@ class FileListManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_handleDragOver(e) {
|
_handleDragOver(e) {
|
||||||
|
const fileItem = this;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
// Only show drag-over for non-folder items
|
// Only show drag-over for non-folder items
|
||||||
if (!this.classList.contains('folder-item') && this !== this.draggedItem) {
|
if (!fileItem.classList.contains('folder-item') && fileItem !== this.draggedItem) {
|
||||||
this.classList.add('drag-over');
|
fileItem.classList.add('drag-over');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +164,8 @@ class FileListManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_handleDragLeave(e) {
|
_handleDragLeave(e) {
|
||||||
this.classList.remove('drag-over');
|
const fileItem = this;
|
||||||
|
fileItem.classList.remove('drag-over');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,27 +173,28 @@ class FileListManager {
|
|||||||
* @param {Event} e - Drop event
|
* @param {Event} e - Drop event
|
||||||
*/
|
*/
|
||||||
_handleDrop(e) {
|
_handleDrop(e) {
|
||||||
|
const fileItem = this;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.classList.remove('drag-over');
|
fileItem.classList.remove('drag-over');
|
||||||
|
|
||||||
if (this === this.draggedItem || this.classList.contains('folder-item')) return;
|
if (fileItem === this.draggedItem || fileItem.classList.contains('folder-item')) return;
|
||||||
|
|
||||||
// Get all media file items (not folders)
|
// Get all media file items (not folders)
|
||||||
const fileItems = Array.from(this.fileListEl.querySelectorAll('.file-item:not(.folder-item)'));
|
const fileItems = Array.from(this.fileListEl.querySelectorAll('.file-item:not(.folder-item)'));
|
||||||
const draggedIndex = fileItems.indexOf(this.draggedItem);
|
const draggedIndex = fileItems.indexOf(this.draggedItem);
|
||||||
const dropIndex = fileItems.indexOf(this);
|
const dropIndex = fileItems.indexOf(fileItem);
|
||||||
|
|
||||||
if (draggedIndex === -1 || dropIndex === -1) return;
|
if (draggedIndex === -1 || dropIndex === -1) return;
|
||||||
|
|
||||||
// Move the dragged item in the DOM
|
// Move the dragged item in the DOM
|
||||||
if (draggedIndex < dropIndex) {
|
if (draggedIndex < dropIndex) {
|
||||||
this.parentNode.insertBefore(this.draggedItem, this.nextSibling);
|
fileItem.parentNode.insertBefore(this.draggedItem, fileItem.nextSibling);
|
||||||
} else {
|
} else {
|
||||||
this.parentNode.insertBefore(this.draggedItem, this);
|
fileItem.parentNode.insertBefore(this.draggedItem, fileItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update episode numbers
|
// Update episode numbers
|
||||||
this._updateEpisodeNumbers();
|
this.updateEpisodeNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user