import { describe, it, expect, beforeEach, vi } from 'vitest'; import { registerVideoConverters, VIDEO_FORMATS } from '../../src/converters/video.js'; import { resolve, _resetForTests } from '../../src/converters/registry.js'; const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() })); vi.mock('node:child_process', () => ({ execFile: execFileMock })); beforeEach(() => { _resetForTests(); execFileMock.mockReset(); execFileMock.mockImplementation((file, args, options, callback) => callback(null, '', '')); }); describe('registerVideoConverters — registration', () => { it('registers every pair among the 5 video formats, and nothing for source === target', () => { registerVideoConverters(); for (const sourceFormat of VIDEO_FORMATS) { for (const targetFormat of VIDEO_FORMATS) { const entry = resolve(sourceFormat, targetFormat); if (sourceFormat === targetFormat) { expect(entry).toBeNull(); } else { expect(entry).not.toBeNull(); expect(entry.family).toBe('video'); } } } }); it('exposes exactly the 5 requested formats', () => { expect(VIDEO_FORMATS.sort()).toEqual(['avi', 'mkv', 'mov', 'mp4', 'webm'].sort()); }); }); describe('registerVideoConverters — subprocess invocation', () => { beforeEach(() => { registerVideoConverters(); }); it('calls ffmpeg with -i, the codec args, and the output path, omitting -vf when no quality is given', async () => { await resolve('mov', 'mp4').convert('/tmp/in.mov', '/tmp/out.mp4'); expect(execFileMock).toHaveBeenCalledTimes(1); const [file, args, options] = execFileMock.mock.calls[0]; expect(file).toBe('ffmpeg'); expect(args).toEqual([ '-y', '-i', '/tmp/in.mov', '-c:v', 'libx264', '-c:a', 'aac', '/tmp/out.mp4', ]); expect(options).toEqual({ timeout: undefined }); }); it('appends -vf scale=-2: when a resolution quality is given', async () => { await resolve('mov', 'mp4').convert('/tmp/in.mov', '/tmp/out.mp4', { quality: 720 }); const [, args] = execFileMock.mock.calls[0]; expect(args).toEqual([ '-y', '-i', '/tmp/in.mov', '-c:v', 'libx264', '-c:a', 'aac', '-vf', 'scale=-2:720', '/tmp/out.mp4', ]); }); it('uses libvpx-vp9 and libopus for a webm target', async () => { await resolve('mp4', 'webm').convert('/tmp/in.mp4', '/tmp/out.webm'); const [, args] = execFileMock.mock.calls[0]; expect(args).toEqual([ '-y', '-i', '/tmp/in.mp4', '-c:v', 'libvpx-vp9', '-c:a', 'libopus', '/tmp/out.webm', ]); }); it('uses mpeg4 and libmp3lame for an avi target', async () => { await resolve('mp4', 'avi').convert('/tmp/in.mp4', '/tmp/out.avi'); const [, args] = execFileMock.mock.calls[0]; expect(args).toEqual([ '-y', '-i', '/tmp/in.mp4', '-c:v', 'mpeg4', '-c:a', 'libmp3lame', '/tmp/out.avi', ]); }); it('uses libx264 and aac for an mkv target', async () => { await resolve('mp4', 'mkv').convert('/tmp/in.mp4', '/tmp/out.mkv'); const [, args] = execFileMock.mock.calls[0]; expect(args).toEqual([ '-y', '-i', '/tmp/in.mp4', '-c:v', 'libx264', '-c:a', 'aac', '/tmp/out.mkv', ]); }); it('forwards options.timeoutMs to execFile as its timeout', async () => { await resolve('mp4', 'webm').convert('/tmp/in.mp4', '/tmp/out.webm', { timeoutMs: 300000 }); const [, , options] = execFileMock.mock.calls[0]; expect(options).toEqual({ timeout: 300000 }); }); it('uses FFMPEG_PATH from the environment when set', async () => { const previous = process.env.FFMPEG_PATH; process.env.FFMPEG_PATH = '/opt/ffmpeg/bin/ffmpeg'; await resolve('mp4', 'webm').convert('/tmp/in.mp4', '/tmp/out.webm'); expect(execFileMock.mock.calls[0][0]).toBe('/opt/ffmpeg/bin/ffmpeg'); if (previous === undefined) delete process.env.FFMPEG_PATH; else process.env.FFMPEG_PATH = previous; }); it('propagates a rejection from execFile as a rejected promise', async () => { execFileMock.mockImplementation((file, args, options, callback) => callback(new Error('ffmpeg exited with code 1'), '', 'Unknown encoder') ); await expect(resolve('mp4', 'webm').convert('/tmp/in.mp4', '/tmp/out.webm')).rejects.toThrow( /exited with code 1/ ); }); });