From ad000f35ba90fb8e6ba6d7cf5ba381ddef86a913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Fri, 22 Aug 2025 23:58:15 +0200 Subject: [PATCH 1/4] test: add libreoffice.test.ts --- tests/converters/libreoffice.test.ts | 161 +++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/converters/libreoffice.test.ts diff --git a/tests/converters/libreoffice.test.ts b/tests/converters/libreoffice.test.ts new file mode 100644 index 0000000..ef7006b --- /dev/null +++ b/tests/converters/libreoffice.test.ts @@ -0,0 +1,161 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { convert } from "../../src/converters/libreoffice"; // ← adjust +import type { ExecFileFn } from "../../src/converters/types"; // ← adjust + +function requireDefined(value: T, msg: string): NonNullable { + if (value === undefined || value === null) throw new Error(msg); + return value as NonNullable; +} + +// --- capture/inspect execFile calls ----------------------------------------- +type Call = { cmd: string; args: string[] }; +let calls: Call[] = []; + +let behavior: + | { kind: "success"; stdout?: string; stderr?: string } + | { kind: "error"; message?: string; stderr?: string } = { kind: "success" }; + +const mockExecFile: ExecFileFn = (cmd, args, cb) => { + calls.push({ cmd, args }); + if (behavior.kind === "error") { + cb(new Error(behavior.message ?? "mock failure"), "", behavior.stderr ?? ""); + } else { + cb(null, behavior.stdout ?? "ok", behavior.stderr ?? ""); + } + // We don't return a real ChildProcess in tests. + return undefined; +}; + +// --- capture console output (no terminal noise) ------------------------------ +let logs: string[] = []; +let errors: string[] = []; + +const originalLog = console.log; +const originalError = console.error; + +// Use Console["log"] for typing; avoids explicit `any` +const makeSink = + (sink: string[]): Console["log"] => + (...data) => { + sink.push(data.map(String).join(" ")); + }; + +beforeEach(() => { + calls = []; + behavior = { kind: "success" }; + + logs = []; + errors = []; + console.log = makeSink(logs); + console.error = makeSink(errors); +}); + +afterEach(() => { + console.log = originalLog; + console.error = originalError; +}); + +// --- core behavior ----------------------------------------------------------- +test("invokes soffice with --headless and outdir derived from targetPath", async () => { + await convert("in.docx", "docx", "odt", "out/out.odt", undefined, mockExecFile); + + const { cmd, args } = requireDefined(calls[0], "Expected at least one execFile call"); + expect(cmd).toBe("soffice"); + expect(args).toEqual([ + "--headless", + `--infilter="MS Word 2007 XML"`, + "--convert-to", + "odt:writer8", + "--outdir", + "out", + "in.docx", + ]); +}); + +test("uses only outFilter when input has no filter (e.g., pdf -> txt)", async () => { + await convert("in.pdf", "pdf", "txt", "out/out.txt", undefined, mockExecFile); + + const { args } = requireDefined(calls[0], "Expected at least one execFile call"); + + expect(args).not.toContainEqual(expect.stringMatching(/^--infilter=/)); + expect(args).toEqual(["--headless", "--convert-to", "txt", "--outdir", "out", "in.pdf"]); +}); + +test("uses only infilter when convertTo has no out filter (e.g., docx -> pdf)", async () => { + await convert("in.docx", "docx", "pdf", "out/out.pdf", undefined, mockExecFile); + + const { args } = requireDefined(calls[0], "Expected at least one execFile call"); + + // If docx has an infilter, it should be present + expect(args).toEqual(["--headless", "--convert-to", "pdf", "--outdir", "out", "in.docx"]); + + const i = args.indexOf("--convert-to"); + expect(i).toBeGreaterThanOrEqual(0); + expect(args[i + 1]).toBe("pdf"); + expect(args.slice(-2)).toEqual(["out", "in.docx"]); +}); + +test("strips leading './' from outdir", async () => { + await convert("in.txt", "txt", "docx", "./out/out.docx", undefined, mockExecFile); + + const { args } = requireDefined(calls[0], "Expected at least one execFile call"); + + const outDirIdx = args.indexOf("--outdir"); + expect(outDirIdx).toBeGreaterThanOrEqual(0); + expect(args[outDirIdx + 1]).toBe("out"); +}); + +// --- promise settlement ------------------------------------------------------ +test("resolves with 'Done' when execFile succeeds", async () => { + behavior = { kind: "success", stdout: "fine", stderr: "" }; + expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).resolves.toBe( + "Done", + ); +}); + +test("rejects when execFile returns an error", async () => { + behavior = { kind: "error", message: "convert failed", stderr: "oops" }; + expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).rejects.toMatch( + /error: Error: convert failed/, + ); +}); + +// --- logging behavior -------------------------------------------------------- +test("logs stdout when present", async () => { + behavior = { kind: "success", stdout: "hello", stderr: "" }; + + await convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile); + + expect(logs).toContain("stdout: hello"); + expect(errors).toHaveLength(0); +}); + +test("logs stderr when present", async () => { + behavior = { kind: "success", stdout: "", stderr: "uh-oh" }; + + await convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile); + + expect(errors).toContain("stderr: uh-oh"); + // When stdout is empty, no stdout log + expect(logs.find((l) => l.startsWith("stdout:"))).toBeUndefined(); +}); + +test("logs both stdout and stderr when both are present", async () => { + behavior = { kind: "success", stdout: "alpha", stderr: "beta" }; + + await convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile); + + expect(logs).toContain("stdout: alpha"); + expect(errors).toContain("stderr: beta"); +}); + +test("logs stderr on exec error as well", async () => { + behavior = { kind: "error", message: "boom", stderr: "EPIPE" }; + + expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).rejects.toMatch( + /error: Error: boom/, + ); + + // The callback still provided stderr; your implementation logs it before settling + expect(errors).toContain("stderr: EPIPE"); +}); From 84d7da0195bcdcb05b258e6f3a0675fb4d56f857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Mon, 8 Dec 2025 12:12:50 +0100 Subject: [PATCH 2/4] test: add tests for dasel, pandoc and vtracer --- tests/converters/dasel.test.ts | 72 ++++++++++++++++++++++++++++ tests/converters/libreoffice.test.ts | 4 +- tests/converters/pandoc.test.ts | 66 +++++++++++++++++++++++++ tests/converters/vtracer.test.ts | 58 ++++++++++++++++++++++ 4 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 tests/converters/dasel.test.ts create mode 100644 tests/converters/pandoc.test.ts create mode 100644 tests/converters/vtracer.test.ts diff --git a/tests/converters/dasel.test.ts b/tests/converters/dasel.test.ts new file mode 100644 index 0000000..adad804 --- /dev/null +++ b/tests/converters/dasel.test.ts @@ -0,0 +1,72 @@ +import fs from "fs"; +import { beforeEach, afterEach, expect, test, describe } from "bun:test"; +import { convert } from "../../src/converters/dasel"; +import type { ExecFileFn } from "../../src/converters/types"; + +const originalWriteFile = fs.writeFile; + +describe("convert", () => { + let mockExecFile: ExecFileFn; + + beforeEach(() => { + // mock fs.writeFile + // @ts-expect-error: property __promisify__ is missing + fs.writeFile = (path, data, cb) => cb(null); + // mock execFile + mockExecFile = (cmd, args, callback) => callback(null, "output-data", ""); + }); + + afterEach(() => { + // reset fs.writeFile + fs.writeFile = originalWriteFile; + }); + + test("should call dasel with correct arguments and write output", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + let writeFileCalled = false; + // @ts-expect-error: property __promisify__ is missing + fs.writeFile = (path, data, cb) => { + writeFileCalled = true; + expect(path).toBe("output.json"); + expect(data).toBe("output-data"); + // @ts-expect-error: could not be callable with null + cb(null); + }; + + const result = await convert( + "input.yaml", + "yaml", + "json", + "output.json", + undefined, + mockExecFile, + ); + + expect(calledArgs[0]).toBe("dasel"); + expect(calledArgs[1]).toEqual(["--file", "input.yaml", "--read", "yaml", "--write", "json"]); + expect(writeFileCalled).toBe(true); + expect(result).toBe("Done"); + }); + + test("should reject if execFile returns an error", async () => { + mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", ""); + expect( + convert("input.yaml", "yaml", "json", "output.json", undefined, mockExecFile), + ).rejects.toMatch(/error: Error: fail/); + }); + + test("should reject if writeFile fails", async () => { + // @ts-expect-error: property __promisify__ is missing + fs.writeFile = (path, data, cb) => cb(new Error("write fail")); + expect( + convert("input.yaml", "yaml", "json", "output.json", undefined, (cmd, args, cb) => + cb(null, "output-data", ""), + ), + ).rejects.toMatch(/Failed to write output/); + }); +}); diff --git a/tests/converters/libreoffice.test.ts b/tests/converters/libreoffice.test.ts index ef7006b..8e6dcf5 100644 --- a/tests/converters/libreoffice.test.ts +++ b/tests/converters/libreoffice.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; -import { convert } from "../../src/converters/libreoffice"; // ← adjust -import type { ExecFileFn } from "../../src/converters/types"; // ← adjust +import { convert } from "../../src/converters/libreoffice"; +import type { ExecFileFn } from "../../src/converters/types"; function requireDefined(value: T, msg: string): NonNullable { if (value === undefined || value === null) throw new Error(msg); diff --git a/tests/converters/pandoc.test.ts b/tests/converters/pandoc.test.ts new file mode 100644 index 0000000..168d6f8 --- /dev/null +++ b/tests/converters/pandoc.test.ts @@ -0,0 +1,66 @@ +import { beforeEach, expect, test, describe } from "bun:test"; +import { convert } from "../../src/converters/pandoc"; +import type { ExecFileFn } from "../../src/converters/types"; + +describe("convert", () => { + let mockExecFile: ExecFileFn; + + beforeEach(() => { + mockExecFile = (cmd, args, callback) => callback(null, "output-data", ""); + }); + + test("should call pandoc with correct arguments (normal)", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + const result = await convert( + "input.md", + "markdown", + "html", + "output.html", + undefined, + mockExecFile, + ); + + expect(calledArgs[0]).toBe("pandoc"); + expect(calledArgs[1]).toEqual([ + "input.md", + "-f", + "markdown", + "-t", + "html", + "-o", + "output.html", + ]); + expect(result).toBe("Done"); + }); + + test("should add xelatex argument for pdf/latex", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + await convert("input.md", "markdown", "pdf", "output.pdf", undefined, mockExecFile); + + expect(calledArgs[1][0]).toBe("--pdf-engine=xelatex"); + expect(calledArgs[1]).toContain("input.md"); + expect(calledArgs[1]).toContain("-f"); + expect(calledArgs[1]).toContain("markdown"); + expect(calledArgs[1]).toContain("-t"); + expect(calledArgs[1]).toContain("pdf"); + expect(calledArgs[1]).toContain("-o"); + expect(calledArgs[1]).toContain("output.pdf"); + }); + + test("should reject if execFile returns an error", async () => { + mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", ""); + expect( + convert("input.md", "markdown", "html", "output.html", undefined, mockExecFile), + ).rejects.toMatch(/error: Error: fail/); + }); +}); diff --git a/tests/converters/vtracer.test.ts b/tests/converters/vtracer.test.ts new file mode 100644 index 0000000..e68cb4a --- /dev/null +++ b/tests/converters/vtracer.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, expect, test, describe } from "bun:test"; +import { convert } from "../../src/converters/vtracer"; +import type { ExecFileFn } from "../../src/converters/types"; + +describe("convert", () => { + let mockExecFile: ExecFileFn; + + beforeEach(() => { + mockExecFile = (cmd, args, callback) => callback(null, "output-data", ""); + }); + + test("should call vtracer with correct arguments (minimal)", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + const result = await convert("input.png", "png", "svg", "output.svg", undefined, mockExecFile); + + expect(calledArgs[0]).toBe("vtracer"); + expect(calledArgs[1]).toEqual(["--input", "input.png", "--output", "output.svg"]); + expect(result).toBe("Done"); + }); + + test("should add options as arguments", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + const options = { + colormode: "color", + hierarchical: "true", + filter_speckle: 5, + path_precision: 0.8, + }; + + await convert("input.png", "png", "svg", "output.svg", options, mockExecFile); + + expect(calledArgs[1]).toContain("--colormode"); + expect(calledArgs[1]).toContain("color"); + expect(calledArgs[1]).toContain("--hierarchical"); + expect(calledArgs[1]).toContain("true"); + expect(calledArgs[1]).toContain("--filter_speckle"); + expect(calledArgs[1]).toContain("5"); + expect(calledArgs[1]).toContain("--path_precision"); + expect(calledArgs[1]).toContain("0.8"); + }); + + test("should reject if execFile returns an error", async () => { + mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", "stderr output"); + expect( + convert("input.png", "png", "svg", "output.svg", undefined, mockExecFile), + ).rejects.toMatch(/error: Error: fail\nstderr: stderr output/); + }); +}); From 826dc1062a2c76c0c95ae67c7aac18a12c523a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Mon, 8 Dec 2025 13:11:13 +0100 Subject: [PATCH 3/4] test: add awaits to rejects and resolves --- tests/converters/dasel.test.ts | 4 ++-- tests/converters/libreoffice.test.ts | 4 ++-- tests/converters/pandoc.test.ts | 2 +- tests/converters/vtracer.test.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/converters/dasel.test.ts b/tests/converters/dasel.test.ts index adad804..b08bed9 100644 --- a/tests/converters/dasel.test.ts +++ b/tests/converters/dasel.test.ts @@ -55,7 +55,7 @@ describe("convert", () => { test("should reject if execFile returns an error", async () => { mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", ""); - expect( + await expect( convert("input.yaml", "yaml", "json", "output.json", undefined, mockExecFile), ).rejects.toMatch(/error: Error: fail/); }); @@ -63,7 +63,7 @@ describe("convert", () => { test("should reject if writeFile fails", async () => { // @ts-expect-error: property __promisify__ is missing fs.writeFile = (path, data, cb) => cb(new Error("write fail")); - expect( + await expect( convert("input.yaml", "yaml", "json", "output.json", undefined, (cmd, args, cb) => cb(null, "output-data", ""), ), diff --git a/tests/converters/libreoffice.test.ts b/tests/converters/libreoffice.test.ts index 8e6dcf5..3984967 100644 --- a/tests/converters/libreoffice.test.ts +++ b/tests/converters/libreoffice.test.ts @@ -108,14 +108,14 @@ test("strips leading './' from outdir", async () => { // --- promise settlement ------------------------------------------------------ test("resolves with 'Done' when execFile succeeds", async () => { behavior = { kind: "success", stdout: "fine", stderr: "" }; - expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).resolves.toBe( + await expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).resolves.toBe( "Done", ); }); test("rejects when execFile returns an error", async () => { behavior = { kind: "error", message: "convert failed", stderr: "oops" }; - expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).rejects.toMatch( + await expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).rejects.toMatch( /error: Error: convert failed/, ); }); diff --git a/tests/converters/pandoc.test.ts b/tests/converters/pandoc.test.ts index 168d6f8..c910d6c 100644 --- a/tests/converters/pandoc.test.ts +++ b/tests/converters/pandoc.test.ts @@ -59,7 +59,7 @@ describe("convert", () => { test("should reject if execFile returns an error", async () => { mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", ""); - expect( + await expect( convert("input.md", "markdown", "html", "output.html", undefined, mockExecFile), ).rejects.toMatch(/error: Error: fail/); }); diff --git a/tests/converters/vtracer.test.ts b/tests/converters/vtracer.test.ts index e68cb4a..365748e 100644 --- a/tests/converters/vtracer.test.ts +++ b/tests/converters/vtracer.test.ts @@ -51,7 +51,7 @@ describe("convert", () => { test("should reject if execFile returns an error", async () => { mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", "stderr output"); - expect( + await expect( convert("input.png", "png", "svg", "output.svg", undefined, mockExecFile), ).rejects.toMatch(/error: Error: fail\nstderr: stderr output/); }); From 0521af0d52bdf15805ae8c561eebb119be909800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Mon, 8 Dec 2025 13:12:53 +0100 Subject: [PATCH 4/4] test: fix code style issue --- tests/converters/libreoffice.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/converters/libreoffice.test.ts b/tests/converters/libreoffice.test.ts index 3984967..4d07f1e 100644 --- a/tests/converters/libreoffice.test.ts +++ b/tests/converters/libreoffice.test.ts @@ -108,16 +108,16 @@ test("strips leading './' from outdir", async () => { // --- promise settlement ------------------------------------------------------ test("resolves with 'Done' when execFile succeeds", async () => { behavior = { kind: "success", stdout: "fine", stderr: "" }; - await expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).resolves.toBe( - "Done", - ); + await expect( + convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile), + ).resolves.toBe("Done"); }); test("rejects when execFile returns an error", async () => { behavior = { kind: "error", message: "convert failed", stderr: "oops" }; - await expect(convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile)).rejects.toMatch( - /error: Error: convert failed/, - ); + await expect( + convert("in.txt", "txt", "docx", "out/out.docx", undefined, mockExecFile), + ).rejects.toMatch(/error: Error: convert failed/); }); // --- logging behavior --------------------------------------------------------