Add unit tests for PDF Packager converter functionality
- Implement tests for converter properties, ensuring correct input and output formats. - Add tests for chip parsing, covering various image, pdf_image, and pdfa chip formats. - Include tests for the isArchiveOutput and getOutputFileName functions. - Mock execFile for testing the convert function, verifying command execution and output file creation. - Ensure all chips are parseable and validate the completeness of chip categories.
This commit is contained in:
parent
87bd318303
commit
944fe18b75
11 changed files with 2233 additions and 8 deletions
504
tests/converters/pdfpackager.test.ts
Normal file
504
tests/converters/pdfpackager.test.ts
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
||||
import {
|
||||
convert,
|
||||
properties,
|
||||
parseChip,
|
||||
isArchiveOutput,
|
||||
getOutputFileName,
|
||||
ALL_CHIPS,
|
||||
} from "../../src/converters/pdfpackager";
|
||||
import type { ExecFileException } from "node:child_process";
|
||||
import type { ExecFileFn } from "../../src/converters/types";
|
||||
import { mkdirSync, existsSync, writeFileSync, rmSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
// =============================================================================
|
||||
// 屬性測試
|
||||
// =============================================================================
|
||||
|
||||
describe("PDF Packager converter properties", () => {
|
||||
test("should have correct input format (pdf only)", () => {
|
||||
expect(properties.from.document).toContain("pdf");
|
||||
expect(properties.from.document).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("should have all chips as output formats", () => {
|
||||
expect(properties.to.document).toHaveLength(ALL_CHIPS.length);
|
||||
|
||||
// 驗證部分代表性 chips
|
||||
expect(properties.to.document).toContain("png-300");
|
||||
expect(properties.to.document).toContain("pdf-600-np-s");
|
||||
expect(properties.to.document).toContain("pdfa1b-i-300-p");
|
||||
expect(properties.to.document).toContain("pdfa2b-o-600");
|
||||
expect(properties.to.document).toContain("all-300");
|
||||
});
|
||||
|
||||
test("should have correct number of chips (102 total)", () => {
|
||||
// A) 圖片: 3格式 × 3dpi = 9
|
||||
// B) PDF: 3dpi × 6 variants = 18
|
||||
// C) PDF/A-1b: 2source × 3dpi × 6 = 36
|
||||
// D) PDF/A-2b: 2source × 3dpi × 6 = 36
|
||||
// E) all: 3
|
||||
// 總計: 9 + 18 + 36 + 36 + 3 = 102
|
||||
expect(ALL_CHIPS.length).toBe(102);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Chip 解析測試
|
||||
// =============================================================================
|
||||
|
||||
describe("parseChip function", () => {
|
||||
describe("images chips", () => {
|
||||
test("should parse png-300", () => {
|
||||
const result = parseChip("png-300");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("images");
|
||||
expect(result?.dpi).toBe(300);
|
||||
expect(result?.imageFormat).toBe("png");
|
||||
});
|
||||
|
||||
test("should parse jpg-150", () => {
|
||||
const result = parseChip("jpg-150");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("images");
|
||||
expect(result?.dpi).toBe(150);
|
||||
expect(result?.imageFormat).toBe("jpg");
|
||||
});
|
||||
|
||||
test("should parse jpeg-600", () => {
|
||||
const result = parseChip("jpeg-600");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("images");
|
||||
expect(result?.dpi).toBe(600);
|
||||
expect(result?.imageFormat).toBe("jpeg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pdf_image chips", () => {
|
||||
test("should parse pdf-300", () => {
|
||||
const result = parseChip("pdf-300");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdf_image");
|
||||
expect(result?.dpi).toBe(300);
|
||||
expect(result?.protect).toBeUndefined();
|
||||
expect(result?.sign).toBe(false);
|
||||
});
|
||||
|
||||
test("should parse pdf-300-p", () => {
|
||||
const result = parseChip("pdf-300-p");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdf_image");
|
||||
expect(result?.dpi).toBe(300);
|
||||
expect(result?.protect).toBe("p");
|
||||
expect(result?.sign).toBe(false);
|
||||
});
|
||||
|
||||
test("should parse pdf-600-np-s", () => {
|
||||
const result = parseChip("pdf-600-np-s");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdf_image");
|
||||
expect(result?.dpi).toBe(600);
|
||||
expect(result?.protect).toBe("np");
|
||||
expect(result?.sign).toBe(true);
|
||||
});
|
||||
|
||||
test("should parse pdf-150-s (sign without protect)", () => {
|
||||
const result = parseChip("pdf-150-s");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdf_image");
|
||||
expect(result?.dpi).toBe(150);
|
||||
expect(result?.protect).toBeUndefined();
|
||||
expect(result?.sign).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pdfa chips", () => {
|
||||
test("should parse pdfa1b-i-300", () => {
|
||||
const result = parseChip("pdfa1b-i-300");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdfa");
|
||||
expect(result?.pdfaLevel).toBe("1b");
|
||||
expect(result?.pdfaSource).toBe("i");
|
||||
expect(result?.dpi).toBe(300);
|
||||
});
|
||||
|
||||
test("should parse pdfa2b-o-600-np-s", () => {
|
||||
const result = parseChip("pdfa2b-o-600-np-s");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdfa");
|
||||
expect(result?.pdfaLevel).toBe("2b");
|
||||
expect(result?.pdfaSource).toBe("o");
|
||||
expect(result?.dpi).toBe(600);
|
||||
expect(result?.protect).toBe("np");
|
||||
expect(result?.sign).toBe(true);
|
||||
});
|
||||
|
||||
test("should parse pdfa1b-o-150-p", () => {
|
||||
const result = parseChip("pdfa1b-o-150-p");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("pdfa");
|
||||
expect(result?.pdfaLevel).toBe("1b");
|
||||
expect(result?.pdfaSource).toBe("o");
|
||||
expect(result?.dpi).toBe(150);
|
||||
expect(result?.protect).toBe("p");
|
||||
});
|
||||
});
|
||||
|
||||
describe("all chips", () => {
|
||||
test("should parse all-300", () => {
|
||||
const result = parseChip("all-300");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("all");
|
||||
expect(result?.dpi).toBe(300);
|
||||
});
|
||||
|
||||
test("should parse all-150", () => {
|
||||
const result = parseChip("all-150");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("all");
|
||||
expect(result?.dpi).toBe(150);
|
||||
});
|
||||
|
||||
test("should parse all-600", () => {
|
||||
const result = parseChip("all-600");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.kind).toBe("all");
|
||||
expect(result?.dpi).toBe(600);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid chips", () => {
|
||||
test("should return null for invalid chip", () => {
|
||||
expect(parseChip("invalid")).toBeNull();
|
||||
expect(parseChip("png-400")).toBeNull(); // invalid DPI
|
||||
expect(parseChip("pdf-300-x")).toBeNull(); // invalid protect
|
||||
expect(parseChip("pdfa3b-i-300")).toBeNull(); // invalid level
|
||||
expect(parseChip("all-300-p")).toBeNull(); // all doesn't support protect
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 輔助函式測試
|
||||
// =============================================================================
|
||||
|
||||
describe("isArchiveOutput function", () => {
|
||||
test("should return true for image chips", () => {
|
||||
expect(isArchiveOutput("png-300")).toBe(true);
|
||||
expect(isArchiveOutput("jpg-600")).toBe(true);
|
||||
expect(isArchiveOutput("jpeg-150")).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true for all chips", () => {
|
||||
expect(isArchiveOutput("all-150")).toBe(true);
|
||||
expect(isArchiveOutput("all-300")).toBe(true);
|
||||
expect(isArchiveOutput("all-600")).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for pdf chips", () => {
|
||||
expect(isArchiveOutput("pdf-300")).toBe(false);
|
||||
expect(isArchiveOutput("pdf-300-p")).toBe(false);
|
||||
expect(isArchiveOutput("pdf-600-np-s")).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false for pdfa chips", () => {
|
||||
expect(isArchiveOutput("pdfa1b-i-300")).toBe(false);
|
||||
expect(isArchiveOutput("pdfa2b-o-600-np-s")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOutputFileName function", () => {
|
||||
test("should return .tar for image chips", () => {
|
||||
expect(getOutputFileName("png-300")).toBe("pack_png-300.tar");
|
||||
expect(getOutputFileName("jpg-600")).toBe("pack_jpg-600.tar");
|
||||
});
|
||||
|
||||
test("should return .tar for all chips", () => {
|
||||
expect(getOutputFileName("all-300")).toBe("pack_all-300.tar");
|
||||
});
|
||||
|
||||
test("should return .pdf for pdf chips", () => {
|
||||
expect(getOutputFileName("pdf-300")).toBe("pack_pdf-300.pdf");
|
||||
expect(getOutputFileName("pdf-600-np-s")).toBe("pack_pdf-600-np-s.pdf");
|
||||
});
|
||||
|
||||
test("should return .pdf for pdfa chips", () => {
|
||||
expect(getOutputFileName("pdfa1b-i-300")).toBe("pack_pdfa1b-i-300.pdf");
|
||||
expect(getOutputFileName("pdfa2b-o-600-np-s")).toBe("pack_pdfa2b-o-600-np-s.pdf");
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 轉換測試(使用 mock execFile)
|
||||
// =============================================================================
|
||||
|
||||
describe("PDF Packager convert function", () => {
|
||||
const testDir = "./test-output-pdfpackager";
|
||||
|
||||
beforeEach(() => {
|
||||
if (!existsSync(testDir)) {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("should process png-300 chip correctly", async () => {
|
||||
const commands: Array<{ cmd: string; args: string[] }> = [];
|
||||
|
||||
const mockExecFile: ExecFileFn = (
|
||||
cmd: string,
|
||||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
commands.push({ cmd, args });
|
||||
|
||||
if (cmd === "pdftoppm") {
|
||||
// Simulate creating image files
|
||||
const outputPrefix = args[args.length - 1];
|
||||
const outputDir = join(outputPrefix, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(`${outputPrefix}-01.png`, "fake png content");
|
||||
writeFileSync(`${outputPrefix}-02.png`, "fake png content");
|
||||
callback(null, "", "");
|
||||
} else if (cmd === "tar") {
|
||||
// Simulate tar creation
|
||||
const outputTar = args[1]; // -cf <output>
|
||||
writeFileSync(outputTar, "fake tar content");
|
||||
callback(null, "", "");
|
||||
} else {
|
||||
callback(null, "", "");
|
||||
}
|
||||
};
|
||||
|
||||
const inputPdf = join(testDir, "input.pdf");
|
||||
writeFileSync(inputPdf, "fake pdf content");
|
||||
|
||||
const targetPath = join(testDir, "output");
|
||||
await convert(inputPdf, "pdf", "png-300", targetPath, undefined, mockExecFile);
|
||||
|
||||
// Verify pdftoppm was called with correct arguments
|
||||
const pdftoppmCall = commands.find((c) => c.cmd === "pdftoppm");
|
||||
expect(pdftoppmCall).toBeDefined();
|
||||
expect(pdftoppmCall?.args).toContain("-r");
|
||||
expect(pdftoppmCall?.args).toContain("300");
|
||||
expect(pdftoppmCall?.args).toContain("-png");
|
||||
|
||||
// Verify tar was called
|
||||
const tarCall = commands.find((c) => c.cmd === "tar");
|
||||
expect(tarCall).toBeDefined();
|
||||
expect(tarCall?.args).toContain("-cf");
|
||||
});
|
||||
|
||||
test("should process pdf-300-np chip correctly (with qpdf)", async () => {
|
||||
const commands: Array<{ cmd: string; args: string[] }> = [];
|
||||
|
||||
const mockExecFile: ExecFileFn = (
|
||||
cmd: string,
|
||||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
commands.push({ cmd, args });
|
||||
|
||||
if (cmd === "pdftoppm") {
|
||||
const outputPrefix = args[args.length - 1];
|
||||
const outputDir = join(outputPrefix, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(`${outputPrefix}-01.png`, "fake png");
|
||||
callback(null, "", "");
|
||||
} else if (cmd === "img2pdf") {
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1) {
|
||||
const outputPdf = args[outputIndex + 1];
|
||||
const outputDir = join(outputPdf, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(outputPdf, "fake pdf");
|
||||
}
|
||||
callback(null, "", "");
|
||||
} else if (cmd === "qpdf") {
|
||||
const outputPdf = args[args.length - 1];
|
||||
writeFileSync(outputPdf, "fake protected pdf");
|
||||
callback(null, "", "");
|
||||
} else {
|
||||
callback(null, "", "");
|
||||
}
|
||||
};
|
||||
|
||||
const inputPdf = join(testDir, "input.pdf");
|
||||
writeFileSync(inputPdf, "fake pdf content");
|
||||
|
||||
const targetPath = join(testDir, "output");
|
||||
await convert(inputPdf, "pdf", "pdf-300-np", targetPath, undefined, mockExecFile);
|
||||
|
||||
// Verify qpdf was called with no-print option
|
||||
const qpdfCall = commands.find((c) => c.cmd === "qpdf");
|
||||
expect(qpdfCall).toBeDefined();
|
||||
expect(qpdfCall?.args).toContain("--print=none");
|
||||
expect(qpdfCall?.args).toContain("--modify=none");
|
||||
});
|
||||
|
||||
test("should process pdfa2b-o-300 chip correctly (from original)", async () => {
|
||||
const commands: Array<{ cmd: string; args: string[] }> = [];
|
||||
|
||||
const mockExecFile: ExecFileFn = (
|
||||
cmd: string,
|
||||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
commands.push({ cmd, args });
|
||||
|
||||
if (cmd === "gs") {
|
||||
// Find output file from args
|
||||
const outputArg = args.find((a) => a.startsWith("-sOutputFile="));
|
||||
if (outputArg) {
|
||||
const outputPdf = outputArg.replace("-sOutputFile=", "");
|
||||
const outputDir = join(outputPdf, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(outputPdf, "fake pdfa pdf");
|
||||
}
|
||||
callback(null, "", "");
|
||||
} else {
|
||||
callback(null, "", "");
|
||||
}
|
||||
};
|
||||
|
||||
const inputPdf = join(testDir, "input.pdf");
|
||||
writeFileSync(inputPdf, "fake pdf content");
|
||||
|
||||
const targetPath = join(testDir, "output");
|
||||
await convert(inputPdf, "pdf", "pdfa2b-o-300", targetPath, undefined, mockExecFile);
|
||||
|
||||
// Verify gs was called with PDF/A-2 option
|
||||
const gsCall = commands.find((c) => c.cmd === "gs");
|
||||
expect(gsCall).toBeDefined();
|
||||
expect(gsCall?.args).toContain("-dPDFA=2");
|
||||
|
||||
// Verify pdftoppm was NOT called (source=o means from original)
|
||||
const pdftoppmCall = commands.find((c) => c.cmd === "pdftoppm");
|
||||
expect(pdftoppmCall).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should process pdf-300-s chip correctly (with signing)", async () => {
|
||||
const mockExecFile: ExecFileFn = (
|
||||
cmd: string,
|
||||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdftoppm") {
|
||||
const outputPrefix = args[args.length - 1];
|
||||
const outputDir = join(outputPrefix, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(`${outputPrefix}-01.png`, "fake png");
|
||||
callback(null, "", "");
|
||||
} else if (cmd === "img2pdf") {
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1) {
|
||||
const outputPdf = args[outputIndex + 1];
|
||||
const outputDir = join(outputPdf, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(outputPdf, "fake pdf");
|
||||
}
|
||||
callback(null, "", "");
|
||||
} else if (cmd === "python3") {
|
||||
// Mock Python signing script - creates signed.pdf
|
||||
const inputPdf = args[1];
|
||||
const outputPdf = args[2];
|
||||
const outputDir = join(outputPdf, "..");
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
// Copy input to output with "signed" marker
|
||||
if (existsSync(inputPdf)) {
|
||||
writeFileSync(outputPdf, "signed pdf content");
|
||||
}
|
||||
callback(null, "", "");
|
||||
} else {
|
||||
callback(null, "", "");
|
||||
}
|
||||
};
|
||||
|
||||
const inputPdf = join(testDir, "input.pdf");
|
||||
writeFileSync(inputPdf, "fake pdf content");
|
||||
|
||||
const targetPath = join(testDir, "output");
|
||||
await convert(inputPdf, "pdf", "pdf-300-s", targetPath, undefined, mockExecFile);
|
||||
|
||||
// Verify output file exists (outDir is dirname(targetPath) = testDir)
|
||||
const expectedOutput = join(testDir, "pack_pdf-300-s.pdf");
|
||||
expect(existsSync(expectedOutput)).toBe(true);
|
||||
|
||||
// Verify file content contains signed marker
|
||||
const content = readFileSync(expectedOutput, "utf-8");
|
||||
expect(content).toContain("signed");
|
||||
});
|
||||
|
||||
test("should throw error for invalid chip", async () => {
|
||||
const mockExecFile: ExecFileFn = (
|
||||
_cmd: string,
|
||||
_args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
callback(null, "", "");
|
||||
};
|
||||
|
||||
const inputPdf = join(testDir, "input.pdf");
|
||||
writeFileSync(inputPdf, "fake pdf content");
|
||||
|
||||
const targetPath = join(testDir, "output");
|
||||
|
||||
await expect(
|
||||
convert(inputPdf, "pdf", "invalid-chip", targetPath, undefined, mockExecFile),
|
||||
).rejects.toThrow("INVALID_CHIP");
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Chip 完整性測試
|
||||
// =============================================================================
|
||||
|
||||
describe("ALL_CHIPS completeness", () => {
|
||||
test("all chips should be parseable", () => {
|
||||
for (const chip of ALL_CHIPS) {
|
||||
const parsed = parseChip(chip);
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed?.rawChip).toBe(chip);
|
||||
}
|
||||
});
|
||||
|
||||
test("should contain all expected image chips", () => {
|
||||
const imageChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "images");
|
||||
expect(imageChips).toHaveLength(9); // 3 formats × 3 dpis
|
||||
});
|
||||
|
||||
test("should contain all expected pdf_image chips", () => {
|
||||
const pdfChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "pdf_image");
|
||||
expect(pdfChips).toHaveLength(18); // 3 dpis × 6 variants
|
||||
});
|
||||
|
||||
test("should contain all expected pdfa chips", () => {
|
||||
const pdfaChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "pdfa");
|
||||
expect(pdfaChips).toHaveLength(72); // 2 levels × 2 sources × 3 dpis × 6 variants
|
||||
});
|
||||
|
||||
test("should contain all expected all chips", () => {
|
||||
const allChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "all");
|
||||
expect(allChips).toHaveLength(3); // 3 dpis
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue