MovieMapper/tests/EpisodeManager.test.js
Jarian 56351b277e 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

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