- 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
284 lines
8.8 KiB
JavaScript
284 lines
8.8 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import EpisodeManager from '../utils/renderer/EpisodeManager';
|
|
|
|
describe('EpisodeManager', () => {
|
|
let em;
|
|
let fileListEl;
|
|
|
|
beforeEach(() => {
|
|
em = new EpisodeManager();
|
|
fileListEl = document.createElement('div');
|
|
fileListEl.id = 'file-list';
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('initializes with isUpdatingEpisodeNumbers false', () => {
|
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateEpisodeNumbers', () => {
|
|
it('updates episode numbers sequentially', () => {
|
|
const item1 = document.createElement('div');
|
|
item1.className = 'file-item';
|
|
const ep1 = document.createElement('span');
|
|
ep1.className = 'episode-number';
|
|
ep1.dataset.episodeStart = '';
|
|
ep1.dataset.episodeEnd = '';
|
|
item1.appendChild(ep1);
|
|
|
|
const item2 = document.createElement('div');
|
|
item2.className = 'file-item';
|
|
const ep2 = document.createElement('span');
|
|
ep2.className = 'episode-number';
|
|
ep2.dataset.episodeStart = '';
|
|
ep2.dataset.episodeEnd = '';
|
|
item2.appendChild(ep2);
|
|
|
|
fileListEl.appendChild(item1);
|
|
fileListEl.appendChild(item2);
|
|
|
|
em.updateEpisodeNumbers(fileListEl);
|
|
expect(ep1.textContent).toBe('1');
|
|
expect(ep2.textContent).toBe('2');
|
|
});
|
|
|
|
it('skips re-entrant calls', () => {
|
|
em.setIsUpdatingEpisodeNumbers(true);
|
|
em.updateEpisodeNumbers(fileListEl);
|
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(true);
|
|
});
|
|
|
|
it('respects stored episode ranges', () => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
const ep = document.createElement('span');
|
|
ep.className = 'episode-number';
|
|
ep.dataset.episodeStart = '5';
|
|
ep.dataset.episodeEnd = '7';
|
|
item.appendChild(ep);
|
|
fileListEl.appendChild(item);
|
|
|
|
em.updateEpisodeNumbers(fileListEl);
|
|
expect(ep.textContent).toBe('5-7');
|
|
});
|
|
});
|
|
|
|
describe('setIsUpdatingEpisodeNumbers', () => {
|
|
it('sets the flag', () => {
|
|
em.setIsUpdatingEpisodeNumbers(true);
|
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(true);
|
|
em.setIsUpdatingEpisodeNumbers(false);
|
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('highlightEpisodes', () => {
|
|
it('highlights episodes in range', () => {
|
|
const container = document.createElement('div');
|
|
container.id = 'episodes-container';
|
|
|
|
const ep1 = document.createElement('div');
|
|
ep1.className = 'episode-item';
|
|
ep1.textContent = 'E1 Pilot';
|
|
|
|
const ep2 = document.createElement('div');
|
|
ep2.className = 'episode-item';
|
|
ep2.textContent = 'E2 Second';
|
|
|
|
const ep3 = document.createElement('div');
|
|
ep3.className = 'episode-item';
|
|
ep3.textContent = 'E3 Third';
|
|
|
|
container.appendChild(ep1);
|
|
container.appendChild(ep2);
|
|
container.appendChild(ep3);
|
|
|
|
em.highlightEpisodes(1, 2, container);
|
|
expect(ep1.classList.contains('highlighted-episode')).toBe(true);
|
|
expect(ep2.classList.contains('highlighted-episode')).toBe(true);
|
|
expect(ep3.classList.contains('highlighted-episode')).toBe(false);
|
|
});
|
|
|
|
it('handles null container', () => {
|
|
expect(() => em.highlightEpisodes(1, 2, null)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('removeEpisodeHighlights', () => {
|
|
it('removes highlights', () => {
|
|
const container = document.createElement('div');
|
|
const ep = document.createElement('div');
|
|
ep.className = 'episode-item highlighted-episode';
|
|
container.appendChild(ep);
|
|
|
|
em.removeEpisodeHighlights(container);
|
|
expect(ep.classList.contains('highlighted-episode')).toBe(false);
|
|
});
|
|
|
|
it('handles null container', () => {
|
|
expect(() => em.removeEpisodeHighlights(null)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getEpisodeRange', () => {
|
|
it('returns range from data attributes', () => {
|
|
const epEl = document.createElement('span');
|
|
epEl.dataset.episodeStart = '3';
|
|
epEl.dataset.episodeEnd = '5';
|
|
const range = em.getEpisodeRange(epEl);
|
|
expect(range).toEqual({ start: 3, end: 5 });
|
|
});
|
|
|
|
it('defaults to 1 when no data', () => {
|
|
const epEl = document.createElement('span');
|
|
const range = em.getEpisodeRange(epEl);
|
|
expect(range).toEqual({ start: 1, end: 1 });
|
|
});
|
|
});
|
|
|
|
describe('calculateTotalEpisodeCount', () => {
|
|
it('sums episode ranges', () => {
|
|
const item1 = document.createElement('div');
|
|
item1.className = 'file-item';
|
|
const ep1 = document.createElement('span');
|
|
ep1.className = 'episode-number';
|
|
ep1.dataset.episodeStart = '1';
|
|
ep1.dataset.episodeEnd = '1';
|
|
item1.appendChild(ep1);
|
|
|
|
const item2 = document.createElement('div');
|
|
item2.className = 'file-item';
|
|
const ep2 = document.createElement('span');
|
|
ep2.className = 'episode-number';
|
|
ep2.dataset.episodeStart = '2';
|
|
ep2.dataset.episodeEnd = '3';
|
|
item2.appendChild(ep2);
|
|
|
|
fileListEl.appendChild(item1);
|
|
fileListEl.appendChild(item2);
|
|
|
|
expect(em.calculateTotalEpisodeCount(fileListEl)).toBe(3);
|
|
});
|
|
|
|
it('returns 0 for empty list', () => {
|
|
expect(em.calculateTotalEpisodeCount(fileListEl)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('getLastEpisodeEnd', () => {
|
|
it('returns highest episode end', () => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
const ep = document.createElement('span');
|
|
ep.className = 'episode-number';
|
|
ep.dataset.episodeEnd = '13';
|
|
item.appendChild(ep);
|
|
fileListEl.appendChild(item);
|
|
|
|
expect(em.getLastEpisodeEnd(fileListEl)).toBe(13);
|
|
});
|
|
|
|
it('returns 0 for empty list', () => {
|
|
expect(em.getLastEpisodeEnd(fileListEl)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('handleEpisodeArrowClick', () => {
|
|
it('increases range with right arrow', () => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
item.dataset.filePath = '/path/file.mp4';
|
|
const ep = document.createElement('span');
|
|
ep.className = 'episode-number';
|
|
ep.dataset.episodeStart = '1';
|
|
ep.dataset.episodeEnd = '1';
|
|
item.appendChild(ep);
|
|
|
|
const arrow = document.createElement('button');
|
|
arrow.className = 'episode-arrow-right';
|
|
item.appendChild(arrow);
|
|
|
|
fileListEl.appendChild(item);
|
|
|
|
em.handleEpisodeArrowClick(arrow, 'right', fileListEl);
|
|
expect(ep.dataset.episodeEnd).toBe('2');
|
|
});
|
|
|
|
it('decreases range with left arrow', () => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
item.dataset.filePath = '/path/file.mp4';
|
|
const ep = document.createElement('span');
|
|
ep.className = 'episode-number';
|
|
ep.dataset.episodeStart = '3';
|
|
ep.dataset.episodeEnd = '3';
|
|
item.appendChild(ep);
|
|
|
|
const arrow = document.createElement('button');
|
|
arrow.className = 'episode-arrow-left';
|
|
item.appendChild(arrow);
|
|
|
|
fileListEl.appendChild(item);
|
|
|
|
em.handleEpisodeArrowClick(arrow, 'left', fileListEl);
|
|
expect(ep.dataset.episodeStart).toBe('2');
|
|
});
|
|
|
|
it('does not decrease below 1', () => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
item.dataset.filePath = '/path/file.mp4';
|
|
const ep = document.createElement('span');
|
|
ep.className = 'episode-number';
|
|
ep.dataset.episodeStart = '1';
|
|
ep.dataset.episodeEnd = '1';
|
|
item.appendChild(ep);
|
|
|
|
const arrow = document.createElement('button');
|
|
arrow.className = 'episode-arrow-left';
|
|
item.appendChild(arrow);
|
|
|
|
fileListEl.appendChild(item);
|
|
|
|
em.handleEpisodeArrowClick(arrow, 'left', fileListEl);
|
|
expect(ep.dataset.episodeStart).toBe('1');
|
|
});
|
|
|
|
it('returns early when no episode element', () => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
|
|
const arrow = document.createElement('button');
|
|
arrow.className = 'episode-arrow-right';
|
|
item.appendChild(arrow);
|
|
|
|
fileListEl.appendChild(item);
|
|
|
|
expect(() => em.handleEpisodeArrowClick(arrow, 'right', fileListEl)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('makeEpisodeRangeEditable', () => {
|
|
it('makes element editable', () => {
|
|
const el = document.createElement('span');
|
|
el.textContent = '1';
|
|
el.dataset.episodeStart = '1';
|
|
el.dataset.episodeEnd = '1';
|
|
|
|
em.makeEpisodeRangeEditable(el);
|
|
expect(el.contentEditable).toBe('true');
|
|
expect(el.classList.contains('editing')).toBe(true);
|
|
});
|
|
|
|
it('returns early if already editable', () => {
|
|
const el = document.createElement('span');
|
|
el.contentEditable = 'true';
|
|
const focusSpy = vi.spyOn(el, 'focus');
|
|
|
|
em.makeEpisodeRangeEditable(el);
|
|
expect(focusSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|