diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index b8bc796..8207c61 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -1,4 +1,5 @@ -import { execFile } from "node:child_process"; +import { execFile as execFileOriginal } from "node:child_process"; +import { ExecFileFn } from "./types.ts"; // This could be done dynamically by running `ffmpeg -formats` and parsing the output export const properties = { @@ -691,8 +692,8 @@ export async function convert( fileType: string, convertTo: string, targetPath: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars options?: unknown, + execFile: ExecFileFn = execFileOriginal, // to make it mockable ): Promise { let extraArgs: string[] = []; let message = "Done"; diff --git a/tests/converters/ffmpeg.test.ts b/tests/converters/ffmpeg.test.ts new file mode 100644 index 0000000..2bb375a --- /dev/null +++ b/tests/converters/ffmpeg.test.ts @@ -0,0 +1,157 @@ +import { beforeEach, expect, test } from "bun:test"; +import { convert } from "../../src/converters/ffmpeg.ts"; + +let calls: string[][] = []; + +function mockExecFile( + _cmd: string, + args: string[], + callback: (err: Error | null, stdout: string, stderr: string) => void, +) { + calls.push(args); + if (args.includes("fail.mov")) { + callback(new Error("mock failure"), "", "Fake stderr: fail"); + } else { + callback(null, "Fake stdout", ""); + } +} + +beforeEach(() => { + calls = []; + delete process.env.FFMPEG_ARGS; +}); + +test("converts a normal file", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + const result = await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(result).toBe("Done"); + expect(calls[0]).toEqual(expect.arrayContaining(["-i", "in.mp4", "out.avi"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("adds resize for ico output", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + const result = await convert("in.png", "png", "ico", "out.ico", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(result).toBe("Done: resized to 256x256"); + expect(calls[0]).toEqual( + expect.arrayContaining(["-filter:v", expect.stringContaining("scale=")]), + ); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("uses libaom-av1 for av1.mp4", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "av1.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libaom-av1"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("uses libx264 for h264.mp4", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("uses libx265 for h265.mp4", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx265"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("uses libx266 for h266.mp4", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "h266.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx266"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("respects FFMPEG_ARGS", async () => { + process.env.FFMPEG_ARGS = "-hide_banner -y"; + + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]?.slice(0, 2)).toEqual(["-hide_banner", "-y"]); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("fails on exec error", async () => { + const originalConsoleError = console.error; + + let loggedMessage = ""; + console.error = (msg) => { + loggedMessage = msg; + }; + + expect(convert("fail.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile)).rejects.toThrow( + "mock failure", + ); + + console.error = originalConsoleError; + + expect(loggedMessage).toBe("stderr: Fake stderr: fail"); +});