MovieMapper/tests/FileListManager.test.js
Jarian Cottingham 9b6e90ce0e test: add Vitest suite with 86% coverage threshold
- 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
2026-07-06 04:54:40 +00:00

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');
});
});
});