MovieMapper/tests/main.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

365 lines
11 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
const path = require('path');
describe('renderer.js - escapeHtml', () => {
let escapeHtml;
beforeEach(() => {
escapeHtml = (str) => {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
});
it('escapes angle brackets', () => {
expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
});
it('escapes ampersands', () => {
expect(escapeHtml('a & b')).toBe('a &amp; b');
});
it('escapes full HTML tag', () => {
const result = escapeHtml('<div onclick="alert(1)">');
expect(result).toContain('&lt;div');
expect(result).toContain('onclick=');
});
it('handles plain text unchanged', () => {
expect(escapeHtml('Hello World')).toBe('Hello World');
});
it('handles empty string', () => {
expect(escapeHtml('')).toBe('');
});
it('handles special characters', () => {
const result = escapeHtml('<>&');
expect(result).toBe('&lt;&gt;&amp;');
});
it('escapes XSS payload', () => {
const payload = '<img src=x onerror=alert(1)>';
const result = escapeHtml(payload);
expect(result).not.toContain('<img');
expect(result).toContain('&lt;img');
});
it('handles unicode characters', () => {
expect(escapeHtml('Hello \u4e16\u754c')).toBe('Hello \u4e16\u754c');
});
it('handles newlines and tabs', () => {
const result = escapeHtml('line1\nline2\ttab');
expect(result).toBe('line1\nline2\ttab');
});
});
describe('main.js - writeLog', () => {
let writeLog, logFilePath;
beforeEach(() => {
vi.clearAllMocks();
const fs = require('fs');
fs.writeFileSync = vi.fn();
fs.appendFileSync = vi.fn();
logFilePath = path.join(globalThis.__dirname, 'app-debug.log');
writeLog = (message) => {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
fs.appendFileSync(logFilePath, logEntry);
};
});
it('writes log entry with timestamp', () => {
const fs = require('fs');
writeLog('Test message');
expect(fs.appendFileSync).toHaveBeenCalled();
const callArg = fs.appendFileSync.mock.calls[0][1];
expect(callArg).toMatch(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
expect(callArg).toContain('Test message');
});
it('handles empty message', () => {
const fs = require('fs');
writeLog('');
expect(fs.appendFileSync).toHaveBeenCalled();
});
});
describe('main.js - writeAuditLog', () => {
let writeAuditLog;
beforeEach(() => {
vi.clearAllMocks();
const fs = require('fs');
fs.appendFileSync = vi.fn();
writeAuditLog = (directoryPath, action, details) => {
const auditFileName = '.audit';
const auditFilePath = path.join(directoryPath, auditFileName);
const timestamp = new Date().toISOString();
const auditEntry = {
timestamp: timestamp,
action: action,
details: details,
};
const entryString = JSON.stringify(auditEntry) + '\n';
fs.appendFileSync(auditFilePath, entryString);
};
});
it('writes audit entry as JSON', () => {
const fs = require('fs');
writeAuditLog('/path/to/dir', 'rename_file', { oldName: 'a.mp4', newName: 'b.mp4' });
expect(fs.appendFileSync).toHaveBeenCalledWith(
'/path/to/dir/.audit',
expect.stringContaining('"action":"rename_file"')
);
});
it('includes timestamp in audit entry', () => {
const fs = require('fs');
writeAuditLog('/path', 'move_file', {});
const callArg = fs.appendFileSync.mock.calls[0][1];
const parsed = JSON.parse(callArg.trim());
expect(parsed.timestamp).toMatch(/\d{4}-\d{2}-\d{2}T/);
});
it('writes to correct path', () => {
const fs = require('fs');
writeAuditLog('/custom/path', 'action', {});
expect(fs.appendFileSync).toHaveBeenCalledWith('/custom/path/.audit', expect.any(String));
});
});
describe('main.js - folder name mapping', () => {
it('maps extra to extras', () => {
const folderName = 'extra';
let actualFolderName;
if (folderName === 'extra') {
actualFolderName = 'extras';
} else if (folderName === 'behind-the-scenes') {
actualFolderName = 'behind the scenes';
} else {
actualFolderName = folderName;
}
expect(actualFolderName).toBe('extras');
});
it('maps behind-the-scenes to behind the scenes', () => {
const folderName = 'behind-the-scenes';
let actualFolderName;
if (folderName === 'extra') {
actualFolderName = 'extras';
} else if (folderName === 'behind-the-scenes') {
actualFolderName = 'behind the scenes';
} else {
actualFolderName = folderName;
}
expect(actualFolderName).toBe('behind the scenes');
});
it('keeps commentary as-is', () => {
const folderName = 'commentary';
let actualFolderName;
if (folderName === 'extra') {
actualFolderName = 'extras';
} else if (folderName === 'behind-the-scenes') {
actualFolderName = 'behind the scenes';
} else {
actualFolderName = folderName;
}
expect(actualFolderName).toBe('commentary');
});
it('keeps delete as-is', () => {
const folderName = 'delete';
let actualFolderName;
if (folderName === 'extra') {
actualFolderName = 'extras';
} else if (folderName === 'behind-the-scenes') {
actualFolderName = 'behind the scenes';
} else {
actualFolderName = folderName;
}
expect(actualFolderName).toBe('delete');
});
it('validates folder names', () => {
const validFolders = ['extra', 'behind-the-scenes', 'commentary', 'delete'];
expect(validFolders.includes('extra')).toBe(true);
expect(validFolders.includes('invalid')).toBe(false);
expect(validFolders.includes('')).toBe(false);
});
});
describe('main.js - episode naming logic', () => {
it('generates single episode filename', () => {
const showName = 'Test Show';
const seasonNumber = 1;
const episodeStart = 5;
const quality = '1080p';
const ext = '.mp4';
const seasonNum = String(seasonNumber).padStart(2, '0');
const epNum = String(episodeStart).padStart(2, '0');
let newFileName = `${showName} S${seasonNum}E${epNum}`;
if (quality && quality !== 'N/A' && quality !== '-') {
newFileName += ` - ${quality}`;
}
newFileName += ext;
expect(newFileName).toBe('Test Show S01E05 - 1080p.mp4');
});
it('generates range episode filename', () => {
const showName = 'Test Show';
const seasonNumber = 2;
const episodeStart = 1;
const episodeEnd = 3;
const ext = '.mkv';
const seasonNum = String(seasonNumber).padStart(2, '0');
const startEpNum = String(episodeStart).padStart(2, '0');
const endEpNum = String(episodeEnd).padStart(2, '0');
let newFileName = `${showName} S${seasonNum}E${startEpNum}-E${endEpNum}`;
newFileName += ext;
expect(newFileName).toBe('Test Show S02E01-E03.mkv');
});
it('skips quality when N/A', () => {
const showName = 'Show';
const seasonNumber = 1;
const episodeStart = 1;
const quality = 'N/A';
const ext = '.mp4';
const seasonNum = String(seasonNumber).padStart(2, '0');
const epNum = String(episodeStart).padStart(2, '0');
let newFileName = `${showName} S${seasonNum}E${epNum}`;
if (quality && quality !== 'N/A' && quality !== '-') {
newFileName += ` - ${quality}`;
}
newFileName += ext;
expect(newFileName).toBe('Show S01E01.mp4');
});
it('skips quality when dash', () => {
const showName = 'Show';
const quality = '-';
const ext = '.mp4';
const seasonNum = '01';
const epNum = '01';
let newFileName = `${showName} S${seasonNum}E${epNum}`;
if (quality && quality !== 'N/A' && quality !== '-') {
newFileName += ` - ${quality}`;
}
newFileName += ext;
expect(newFileName).toBe('Show S01E01.mp4');
});
it('handles show name with tvdbid bracket', () => {
const showFolderName = 'Test Show [tvdbid-12345]';
let showName = showFolderName.replace(/\s*\[tvdbid-[^\]]+\]/, '').trim();
expect(showName).toBe('Test Show');
});
it('handles show name with semicolons', () => {
const showFolderName = ';;;Test Show';
let showName = showFolderName.replace(/\s*\[tvdbid-[^\]]+\]/, '').trim();
showName = showName.replace(/^[;:]+/, '').trim();
expect(showName).toBe('Test Show');
});
it('extracts season number from Season XX format', () => {
const seasonFolder = 'Season 05';
const seasonMatch = seasonFolder.match(/(?:season\s*|s)(\d+)/i);
expect(parseInt(seasonMatch[1])).toBe(5);
});
it('extracts season number from SXX format', () => {
const seasonFolder = 'S03';
const seasonMatch = seasonFolder.match(/(?:season\s*|s)(\d+)/i);
expect(parseInt(seasonMatch[1])).toBe(3);
});
});
describe('main.js - rename-file handler logic', () => {
let fs;
beforeEach(() => {
vi.clearAllMocks();
fs = require('fs');
fs.existsSync = vi.fn(() => true);
fs.accessSync = vi.fn();
fs.renameSync = vi.fn();
});
it('returns error when original file does not exist', () => {
fs.existsSync = vi.fn(() => false);
const oldPath = '/path/to/file.mp4';
let result;
if (!fs.existsSync(oldPath)) {
result = { success: false, error: 'Original file does not exist' };
}
expect(result.success).toBe(false);
expect(result.error).toBe('Original file does not exist');
});
it('constructs new path correctly', () => {
const oldPath = '/path/to/old.mp4';
const newName = 'new.mp4';
const newPath = path.join(path.dirname(oldPath), newName);
expect(newPath).toBe('/path/to/new.mp4');
});
});
describe('main.js - move-file-to-folder handler logic', () => {
let fs;
beforeEach(() => {
vi.clearAllMocks();
fs = require('fs');
fs.existsSync = vi.fn(() => true);
});
it('returns error when file does not exist', () => {
fs.existsSync = vi.fn(() => false);
const filePath = '/nonexistent.mp4';
let result;
if (!fs.existsSync(filePath)) {
result = { success: false, error: 'File does not exist' };
}
expect(result.success).toBe(false);
expect(result.error).toBe('File does not exist');
});
it('returns error for invalid folder name', () => {
const validFolders = ['extra', 'behind-the-scenes', 'commentary', 'delete'];
const folderName = 'invalid-folder';
let result;
if (!validFolders.includes(folderName)) {
result = { success: false, error: 'Invalid folder name' };
}
expect(result.success).toBe(false);
});
it('allows all valid folder names', () => {
const validFolders = ['extra', 'behind-the-scenes', 'commentary', 'delete'];
validFolders.forEach((fn) => {
expect(validFolders.includes(fn)).toBe(true);
});
});
});