- Add Vitest + jsdom test framework with V8 coverage - 118 tests across 7 test files covering AppState, ProgressManager, EpisodeManager, FileListManager, fileUtils, main.js logic, renderer escapeHtml, preload module polyfill - Fix async bug in extractFileDuration (await in non-async callback) - CI enforces 85% coverage threshold on testable modules - Exclude Electron IPC wrappers (FileManager, SearchManager, TagManager, ModalManager, UIManager) from coverage as they require Electron runtime
197 lines
6.9 KiB
JavaScript
197 lines
6.9 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import FileListManager from '../utils/renderer/FileListManager';
|
|
|
|
describe('FileListManager', () => {
|
|
let fileListEl;
|
|
let flm;
|
|
let folderClickSpy;
|
|
|
|
beforeEach(() => {
|
|
fileListEl = document.createElement('div');
|
|
fileListEl.id = 'file-list';
|
|
folderClickSpy = vi.fn();
|
|
flm = new FileListManager(fileListEl, folderClickSpy);
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('initializes with null draggedItem', () => {
|
|
expect(flm.draggedItem).toBeNull();
|
|
});
|
|
|
|
it('accepts null folder click callback', () => {
|
|
const manager = new FileListManager(fileListEl, null);
|
|
expect(manager.onFolderClick).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('displayFiles', () => {
|
|
it('shows empty message when no files', () => {
|
|
flm.displayFiles([]);
|
|
expect(fileListEl.textContent).toContain('No media files found');
|
|
});
|
|
|
|
it('displays folders and media files', () => {
|
|
const files = [
|
|
{ isFolder: true, path: '/path/folder', name: 'Extras' },
|
|
{ isFolder: false, path: '/path/file.mp4', name: 'episode.mp4', duration: '45:00', quality: '1080p', fps: '30fps' },
|
|
];
|
|
flm.displayFiles(files);
|
|
const items = fileListEl.querySelectorAll('.file-item');
|
|
expect(items.length).toBe(2);
|
|
expect(items[0].classList.contains('folder-item')).toBe(true);
|
|
});
|
|
|
|
it('sorts folders before files', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/file.mp4', name: 'episode.mp4', duration: '45:00' },
|
|
{ isFolder: true, path: '/path/folder', name: 'Extras' },
|
|
];
|
|
flm.displayFiles(files);
|
|
const items = fileListEl.querySelectorAll('.file-item');
|
|
expect(items[0].classList.contains('folder-item')).toBe(true);
|
|
});
|
|
|
|
it('creates folder element with click handler', () => {
|
|
const files = [{ isFolder: true, path: '/path/folder', name: 'Extras' }];
|
|
flm.displayFiles(files);
|
|
const folderItem = fileListEl.querySelector('.folder-item');
|
|
expect(folderItem).not.toBeNull();
|
|
});
|
|
|
|
it('creates media file with episode number', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/file.mp4', name: 'episode.mp4', duration: '45:00', quality: '1080p', fps: '30fps' },
|
|
];
|
|
flm.displayFiles(files);
|
|
const episodeEl = fileListEl.querySelector('.episode-number');
|
|
expect(episodeEl).not.toBeNull();
|
|
expect(episodeEl.textContent).toBe('1');
|
|
});
|
|
|
|
it('marks problematic files with warning icon', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/bad.mp4', name: 'bad.mp4', duration: '00:00', isProblematic: true },
|
|
];
|
|
flm.displayFiles(files);
|
|
const warnLabel = fileListEl.querySelector('.problematic-label');
|
|
expect(warnLabel).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('updateEpisodeNumbers', () => {
|
|
it('updates episode numbers sequentially', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
|
{ isFolder: false, path: '/path/f2.mp4', name: 'f2.mp4' },
|
|
{ isFolder: false, path: '/path/f3.mp4', name: 'f3.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
flm.updateEpisodeNumbers();
|
|
const episodeEls = fileListEl.querySelectorAll('.episode-number');
|
|
expect(episodeEls[0].textContent).toBe('1');
|
|
expect(episodeEls[1].textContent).toBe('2');
|
|
expect(episodeEls[2].textContent).toBe('3');
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('clears the file list', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
flm.clear();
|
|
expect(fileListEl.innerHTML).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('getMediaFileItems', () => {
|
|
it('returns only media file items', () => {
|
|
const files = [
|
|
{ isFolder: true, path: '/path/folder', name: 'Folder' },
|
|
{ isFolder: false, path: '/path/file.mp4', name: 'file.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
const mediaItems = flm.getMediaFileItems();
|
|
expect(mediaItems.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('getFolderItems', () => {
|
|
it('returns only folder items', () => {
|
|
const files = [
|
|
{ isFolder: true, path: '/path/folder', name: 'Folder' },
|
|
{ isFolder: false, path: '/path/file.mp4', name: 'file.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
const folderItems = flm.getFolderItems();
|
|
expect(folderItems.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('highlightFilesMatchingEpisode', () => {
|
|
it('highlights matching episodes', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
|
{ isFolder: false, path: '/path/f2.mp4', name: 'f2.mp4' },
|
|
{ isFolder: false, path: '/path/f3.mp4', name: 'f3.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
flm.highlightFilesMatchingEpisode(2, 2);
|
|
const items = flm.getMediaFileItems();
|
|
expect(items[0].classList.contains('hovered-file')).toBe(false);
|
|
expect(items[1].classList.contains('hovered-file')).toBe(true);
|
|
expect(items[2].classList.contains('hovered-file')).toBe(false);
|
|
});
|
|
|
|
it('highlights overlapping ranges', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
|
{ isFolder: false, path: '/path/f2.mp4', name: 'f2.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
// Update first file to have range 1-2
|
|
const firstEp = flm.getMediaFileItems()[0].querySelector('.episode-number');
|
|
firstEp.dataset.episodeEnd = '2';
|
|
|
|
flm.highlightFilesMatchingEpisode(1, 1);
|
|
const items = flm.getMediaFileItems();
|
|
// File 1 (range 1-2) overlaps with hover range 1-1
|
|
expect(items[0].classList.contains('hovered-file')).toBe(true);
|
|
// File 2 (range 2-2) does NOT overlap with hover range 1-1
|
|
expect(items[1].classList.contains('hovered-file')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('removeFileHighlights', () => {
|
|
it('removes all highlights', () => {
|
|
const files = [
|
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
|
];
|
|
flm.displayFiles(files);
|
|
flm.highlightFilesMatchingEpisode(1, 1);
|
|
flm.removeFileHighlights();
|
|
const items = flm.getMediaFileItems();
|
|
expect(items[0].classList.contains('hovered-file')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('setFolderClickCallback', () => {
|
|
it('updates the callback', () => {
|
|
const newCallback = vi.fn();
|
|
flm.setFolderClickCallback(newCallback);
|
|
expect(flm.onFolderClick).toBe(newCallback);
|
|
});
|
|
});
|
|
|
|
describe('folder click handling', () => {
|
|
it('calls folder click callback when folder is clicked', () => {
|
|
const files = [{ isFolder: true, path: '/path/folder', name: 'Extras' }];
|
|
flm.displayFiles(files);
|
|
const folderItem = fileListEl.querySelector('.folder-item');
|
|
folderItem.click();
|
|
expect(folderClickSpy).toHaveBeenCalledWith('/path/folder', 'Extras');
|
|
});
|
|
});
|
|
});
|