import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { convert, properties } from "../../src/converters/pdfmathtranslate"; import type { ExecFileException } from "node:child_process"; import { ExecFileFn } from "../../src/converters/types"; import { mkdirSync, existsSync, writeFileSync, rmSync, readFileSync } from "node:fs"; import { join } from "node:path"; // Skip common tests as PDFMathTranslate has different behavior (archive output) test.skip("dummy - required to trigger test detection", () => {}); describe("PDFMathTranslate converter properties", () => { test("should have correct input formats", () => { expect(properties.from.document).toContain("pdf"); }); test("should have correct output formats for multiple languages", () => { expect(properties.to.document).toContain("pdf-en"); expect(properties.to.document).toContain("pdf-zh"); expect(properties.to.document).toContain("pdf-zh-TW"); expect(properties.to.document).toContain("pdf-ja"); expect(properties.to.document).toContain("pdf-ko"); expect(properties.to.document).toContain("pdf-de"); expect(properties.to.document).toContain("pdf-fr"); }); test("should have archive output mode", () => { expect(properties.outputMode).toBe("archive"); }); }); describe("PDFMathTranslate converter - Chinese translation", () => { const testDir = "./test-output-pdfmathtranslate-zh"; const testInputFile = join(testDir, "input.pdf"); beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } // Create a dummy PDF file for testing writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content"); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); test("should call pdf2zh with correct language argument for zh", async () => { let pdf2zhArgs: string[] = []; let pdf2zhCalled = false; const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { pdf2zhCalled = true; pdf2zhArgs = args; // Simulate pdf2zh creating output files const outputDirIndex = args.indexOf("-o"); if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { const outputDir = args[outputDirIndex + 1]; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } // Create mock output files writeFileSync(join(outputDir, "input-mono.pdf"), "%PDF-1.4\n%Translated content"); writeFileSync(join(outputDir, "input-dual.pdf"), "%PDF-1.4\n%Dual content"); } callback(null, "Translation complete", ""); } else if (cmd === "tar") { // Simulate .tar creation (no compression) // Verify it's creating .tar not .tar.gz const tarArgs = args; expect(tarArgs[0]).toBe("-cf"); // -cf for tar (not -czf for gzip) callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile); expect(pdf2zhCalled).toBe(true); expect(pdf2zhArgs).toContain("-lo"); expect(pdf2zhArgs).toContain("zh"); expect(pdf2zhArgs).toContain("-o"); expect(pdf2zhArgs).toContain("-s"); // Translation service }); }); describe("PDFMathTranslate converter - English translation", () => { const testDir = "./test-output-pdfmathtranslate-en"; const testInputFile = join(testDir, "input.pdf"); beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content"); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); test("should call pdf2zh with correct language argument for en", async () => { let pdf2zhArgs: string[] = []; const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { pdf2zhArgs = args; const outputDirIndex = args.indexOf("-o"); if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { const outputDir = args[outputDirIndex + 1]; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } writeFileSync(join(outputDir, "input-mono.pdf"), "%PDF-1.4\n%Translated"); } callback(null, "Translation complete", ""); } else if (cmd === "tar") { callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await convert(testInputFile, "pdf", "pdf-en", targetPath, undefined, mockExecFile); expect(pdf2zhArgs).toContain("-lo"); expect(pdf2zhArgs).toContain("en"); }); }); describe("PDFMathTranslate converter - Japanese translation", () => { const testDir = "./test-output-pdfmathtranslate-ja"; const testInputFile = join(testDir, "input.pdf"); beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content"); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); test("should call pdf2zh with correct language argument for ja", async () => { let pdf2zhArgs: string[] = []; const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { pdf2zhArgs = args; const outputDirIndex = args.indexOf("-o"); if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { const outputDir = args[outputDirIndex + 1]; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } writeFileSync(join(outputDir, "input-mono.pdf"), "%PDF-1.4\n%Translated"); } callback(null, "Translation complete", ""); } else if (cmd === "tar") { callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await convert(testInputFile, "pdf", "pdf-ja", targetPath, undefined, mockExecFile); expect(pdf2zhArgs).toContain("-lo"); expect(pdf2zhArgs).toContain("ja"); }); }); describe("PDFMathTranslate converter - Traditional Chinese", () => { const testDir = "./test-output-pdfmathtranslate-zhtw"; const testInputFile = join(testDir, "input.pdf"); beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content"); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); test("should call pdf2zh with correct language argument for zh-TW", async () => { let pdf2zhArgs: string[] = []; const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { pdf2zhArgs = args; const outputDirIndex = args.indexOf("-o"); if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { const outputDir = args[outputDirIndex + 1]; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } writeFileSync(join(outputDir, "input-mono.pdf"), "%PDF-1.4\n%Translated"); } callback(null, "Translation complete", ""); } else if (cmd === "tar") { callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await convert(testInputFile, "pdf", "pdf-zh-TW", targetPath, undefined, mockExecFile); expect(pdf2zhArgs).toContain("-lo"); expect(pdf2zhArgs).toContain("zh-TW"); }); }); describe("PDFMathTranslate converter - Output structure", () => { const testDir = "./test-output-pdfmathtranslate-structure"; const testInputFile = join(testDir, "input.pdf"); beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content"); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); test("should create archive with original.pdf and translated-.pdf", async () => { let tarSourceDir = ""; let archiveContents: string[] = []; const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { const outputDirIndex = args.indexOf("-o"); if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { const outputDir = args[outputDirIndex + 1]; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } writeFileSync(join(outputDir, "input-mono.pdf"), "%PDF-1.4\n%Translated"); } callback(null, "Translation complete", ""); } else if (cmd === "tar") { // Capture the source directory for the archive const cIndex = args.indexOf("-C"); if (cIndex !== -1 && args[cIndex + 1]) { tarSourceDir = args[cIndex + 1]; // Read the files in the archive source directory if (existsSync(tarSourceDir)) { const { readdirSync } = require("node:fs"); archiveContents = readdirSync(tarSourceDir); } } callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await convert(testInputFile, "pdf", "pdf-ko", targetPath, undefined, mockExecFile); // Verify archive contains expected files expect(archiveContents).toContain("original.pdf"); expect(archiveContents).toContain("translated-ko.pdf"); }); test("should only use .tar format, not .tar.gz", async () => { let tarArgs: string[] = []; const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { const outputDirIndex = args.indexOf("-o"); if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { const outputDir = args[outputDirIndex + 1]; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } writeFileSync(join(outputDir, "input-mono.pdf"), "%PDF-1.4\n%Translated"); } callback(null, "Translation complete", ""); } else if (cmd === "tar") { tarArgs = args; callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await convert(testInputFile, "pdf", "pdf-de", targetPath, undefined, mockExecFile); // Verify tar is called with -cf (not -czf for gzip compression) expect(tarArgs[0]).toBe("-cf"); expect(tarArgs[0]).not.toBe("-czf"); // Verify output path ends with .tar not .tar.gz const outputTar = tarArgs[1]; expect(outputTar).toMatch(/\.tar$/); expect(outputTar).not.toMatch(/\.tar\.gz$/); expect(outputTar).not.toMatch(/\.tgz$/); }); }); describe("PDFMathTranslate converter - Error handling", () => { const testDir = "./test-output-pdfmathtranslate-error"; const testInputFile = join(testDir, "input.pdf"); beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content"); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); test("should reject with error when pdf2zh fails", async () => { const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { const error = new Error("Translation failed") as ExecFileException; callback(error, "", "Error: Translation service unavailable"); } }; const targetPath = join(testDir, "output.tar"); await expect( convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile) ).rejects.toMatch(/pdf2zh error/); }); test("should reject when no output PDF is generated", async () => { const mockExecFile: ExecFileFn = ( cmd: string, args: string[], callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, options?: any, ) => { if (cmd === "pdf2zh") { // Don't create any output files callback(null, "Complete", ""); } else if (cmd === "tar") { callback(null, "Archive created", ""); } }; const targetPath = join(testDir, "output.tar"); await expect( convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile) ).rejects.toMatch(/No.*PDF.*found/); }); });