/**
* 全面 E2E 測試
*
* 測試涵蓋:
* - 25+ 轉換器
* - 1000+ 格式組合
* - 多語言翻譯(中、英、日、韓、德、法等)
* - 批次轉換
* - 錯誤處理
*
* 執行方式:
* bun test tests/e2e/comprehensive.e2e.test.ts
*
* 環境要求:Docker 環境或已安裝所有轉換工具的系統
*/
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { existsSync, statSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
// =============================================================================
// 測試配置
// =============================================================================
const E2E_OUTPUT_DIR = "tests/e2e/output/comprehensive";
const E2E_FIXTURES_DIR = "tests/e2e/fixtures";
// 測試超時(毫秒)
const TIMEOUT = {
fast: 30_000, // 快速轉換
medium: 60_000, // 中等複雜度
slow: 180_000, // 複雜轉換
translation: 300_000, // 翻譯(需網路)
};
// =============================================================================
// 工具檢測
// =============================================================================
interface ToolStatus {
name: string;
available: boolean;
version?: string;
}
function checkTool(command: string, versionFlag = "--version"): ToolStatus {
try {
const result = spawnSync(command, [versionFlag], {
timeout: 5000,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return {
name: command,
available: result.status === 0,
version: result.stdout?.split("\n")[0]?.trim(),
};
} catch {
return { name: command, available: false };
}
}
const TOOLS = {
inkscape: () => checkTool("inkscape"),
imagemagick: () => checkTool("magick") || checkTool("convert"),
graphicsmagick: () => checkTool("gm", "version"),
libreoffice: () => checkTool("soffice"),
ffmpeg: () => checkTool("ffmpeg", "-version"),
pandoc: () => checkTool("pandoc"),
calibre: () => checkTool("ebook-convert"),
potrace: () => checkTool("potrace", "-v"),
vips: () => checkTool("vips"),
resvg: () => checkTool("resvg"),
dasel: () => checkTool("dasel"),
vtracer: () => checkTool("vtracer"),
assimp: () => checkTool("assimp", "version"),
pdf2zh: () => checkTool("pdf2zh"),
babeldoc: () => checkTool("babeldoc"),
mineru: () => checkTool("mineru"),
markitdown: () => checkTool("markitdown"),
xelatex: () => checkTool("xelatex"),
dvisvgm: () => checkTool("dvisvgm"),
heifConvert: () => checkTool("heif-convert"),
cjxl: () => checkTool("cjxl"),
xvfbRun: () => checkTool("xvfb-run"),
};
// =============================================================================
// 測試 Fixtures 生成
// =============================================================================
function ensureDir(dir: string): void {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
function createTestSvg(path: string): void {
writeFileSync(
path,
`
`,
);
}
function createTestPng(path: string): void {
// 最小有效 PNG (1x1 紅色像素)
const png = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xfe, 0xd4, 0xef, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]);
writeFileSync(path, png);
}
function createTestBmp(path: string): void {
// 最小有效 BMP (1x1 紅色像素)
const bmp = Buffer.alloc(70);
// BMP Header
bmp.write("BM", 0);
bmp.writeUInt32LE(70, 2); // File size
bmp.writeUInt32LE(54, 10); // Pixel data offset
// DIB Header
bmp.writeUInt32LE(40, 14); // DIB header size
bmp.writeInt32LE(1, 18); // Width
bmp.writeInt32LE(1, 22); // Height
bmp.writeUInt16LE(1, 26); // Color planes
bmp.writeUInt16LE(24, 28); // Bits per pixel
// Pixel data (BGR)
bmp[54] = 0x00; // Blue
bmp[55] = 0x00; // Green
bmp[56] = 0xff; // Red
writeFileSync(path, bmp);
}
function createTestMarkdown(path: string): void {
writeFileSync(
path,
`# 測試文件 Test Document
這是一個**多語言**測試文件。
## 中文內容
繁體中文測試:台灣、香港、澳門
簡體中文测试:北京、上海、广州
## English Content
This is an English paragraph for testing.
## 日本語コンテンツ
これは日本語のテスト段落です。
## 한국어 콘텐츠
이것은 한국어 테스트 단락입니다.
### 表格 Table
| 語言 | Language | 問候 |
|------|----------|------|
| 中文 | Chinese | 你好 |
| 英文 | English | Hello |
| 日文 | Japanese | こんにちは |
| 韓文 | Korean | 안녕하세요 |
### 代碼 Code
\`\`\`javascript
console.log("Hello, 世界!");
\`\`\`
`,
);
}
function createTestJson(path: string): void {
writeFileSync(
path,
JSON.stringify(
{
name: "測試",
version: "1.0.0",
languages: ["zh-TW", "zh-CN", "en", "ja", "ko"],
config: {
enabled: true,
count: 42,
},
},
null,
2,
),
);
}
function createTestHtml(path: string): void {
writeFileSync(
path,
`
測試頁面
標題 Title
這是測試內容。This is test content.
`,
);
}
function createTestTxt(path: string): void {
writeFileSync(
path,
`測試文字檔案 Test Text File
這是一個純文字測試檔案。
This is a plain text test file.
多語言支援:
- 繁體中文
- 简体中文
- English
- 日本語
- 한국어
- Deutsch
- Français
`,
);
}
function createTestCsv(path: string): void {
writeFileSync(
path,
`name,value,description
測試1,100,這是第一項
測試2,200,這是第二項
test3,300,This is the third item
テスト4,400,これは4番目の項目です
`,
);
}
// =============================================================================
// 測試執行輔助
// =============================================================================
interface ConversionResult {
success: boolean;
inputPath: string;
outputPath: string;
inputSize: number;
outputSize: number;
duration: number;
error?: string;
}
async function runConversion(
converter: string,
inputPath: string,
outputPath: string,
): Promise {
const startTime = Date.now();
const inputSize = existsSync(inputPath) ? statSync(inputPath).size : 0;
try {
// 動態導入轉換器
const module = await import(`../../src/converters/${converter}`);
const inputType = inputPath.split(".").pop() || "";
const outputType = outputPath.split(".").pop() || "";
await module.convert(inputPath, inputType, outputType, outputPath);
const duration = Date.now() - startTime;
const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0;
return {
success: existsSync(outputPath) && outputSize > 0,
inputPath,
outputPath,
inputSize,
outputSize,
duration,
};
} catch (error) {
return {
success: false,
inputPath,
outputPath,
inputSize,
outputSize: 0,
duration: Date.now() - startTime,
error: String(error),
};
}
}
// =============================================================================
// 測試報告
// =============================================================================
interface TestStats {
total: number;
passed: number;
failed: number;
skipped: number;
results: ConversionResult[];
}
const stats: TestStats = {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
results: [],
};
function printSummary(): void {
console.log("\n" + "=".repeat(60));
console.log("📊 E2E 測試摘要 Test Summary");
console.log("=".repeat(60));
console.log(`總測試數 Total: ${stats.total}`);
console.log(`✅ 通過 Passed: ${stats.passed}`);
console.log(`❌ 失敗 Failed: ${stats.failed}`);
console.log(`⏭ 跳過 Skipped: ${stats.skipped}`);
console.log(
`成功率 Success Rate: ${((stats.passed / (stats.total - stats.skipped)) * 100).toFixed(1)}%`,
);
console.log("=".repeat(60));
}
// =============================================================================
// 測試套件
// =============================================================================
const availableTools: Record = {};
beforeAll(() => {
console.log("\n🔧 檢測可用工具 Detecting available tools...\n");
for (const [name, check] of Object.entries(TOOLS)) {
const status = check();
availableTools[name] = status.available;
const icon = status.available ? "✅" : "❌";
console.log(
` ${icon} ${name}: ${status.available ? status.version || "available" : "not found"}`,
);
}
console.log("\n");
ensureDir(E2E_OUTPUT_DIR);
});
afterAll(() => {
printSummary();
console.log(`\n📁 測試輸出目錄: ${E2E_OUTPUT_DIR}`);
});
// =============================================================================
// 圖像格式轉換測試
// =============================================================================
describe("🖼️ 圖像格式轉換 Image Conversions", () => {
const outputDir = join(E2E_OUTPUT_DIR, "images");
beforeAll(() => {
ensureDir(outputDir);
});
// Inkscape 轉換
describe("Inkscape (SVG ↔ 其他格式)", () => {
const formats = ["png", "pdf", "eps", "ps", "emf"];
const inputPath = join(outputDir, "test.svg");
beforeAll(() => {
createTestSvg(inputPath);
});
for (const format of formats) {
test(
`SVG → ${format.toUpperCase()}`,
async () => {
if (!availableTools.inkscape || !availableTools.xvfbRun) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: xvfb-run or inkscape not available`);
return;
}
stats.total++;
const outputPath = join(outputDir, `inkscape_output.${format}`);
const result = await runConversion("inkscape", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(
` ✓ SVG → ${format.toUpperCase()}: ${result.outputSize} bytes (${result.duration}ms)`,
);
} else {
stats.failed++;
console.log(` ✗ SVG → ${format.toUpperCase()}: ${result.error}`);
}
expect(result.success).toBe(true);
},
TIMEOUT.medium,
);
}
});
// ImageMagick 轉換
describe("ImageMagick (點陣圖格式)", () => {
const conversions = [
["png", "jpg"],
["png", "gif"],
["png", "webp"],
["png", "bmp"],
["png", "tiff"],
["bmp", "png"],
];
for (const [from, to] of conversions) {
test(
`${from.toUpperCase()} → ${to.toUpperCase()}`,
async () => {
if (!availableTools.imagemagick) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: ImageMagick not available`);
return;
}
stats.total++;
const inputPath = join(outputDir, `imagemagick_input.${from}`);
const outputPath = join(outputDir, `imagemagick_${from}_to_${to}.${to}`);
if (from === "png") createTestPng(inputPath);
else if (from === "bmp") createTestBmp(inputPath);
const result = await runConversion("imagemagick", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(
` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.fast,
);
}
});
// Potrace (點陣圖 → 向量)
describe("Potrace (點陣圖 → 向量)", () => {
const formats = ["svg", "eps", "pdf"];
for (const format of formats) {
test(
`BMP → ${format.toUpperCase()}`,
async () => {
if (!availableTools.potrace) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Potrace not available`);
return;
}
stats.total++;
const inputPath = join(outputDir, "potrace_input.bmp");
const outputPath = join(outputDir, `potrace_output.${format}`);
createTestBmp(inputPath);
const result = await runConversion("potrace", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(` ✓ BMP → ${format.toUpperCase()}: ${result.outputSize} bytes`);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.fast,
);
}
});
});
// =============================================================================
// 文件格式轉換測試
// =============================================================================
describe("📄 文件格式轉換 Document Conversions", () => {
const outputDir = join(E2E_OUTPUT_DIR, "documents");
beforeAll(() => {
ensureDir(outputDir);
});
// Pandoc 轉換
describe("Pandoc (Markdown ↔ 其他格式)", () => {
const formats = ["html", "docx", "rst", "latex", "epub", "odt"];
const inputPath = join(outputDir, "test.markdown");
beforeAll(() => {
createTestMarkdown(inputPath);
});
for (const format of formats) {
test(
`Markdown → ${format.toUpperCase()}`,
async () => {
if (!availableTools.pandoc) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Pandoc not available`);
return;
}
stats.total++;
const outputPath = join(outputDir, `pandoc_output.${format}`);
const result = await runConversion("pandoc", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(` ✓ Markdown → ${format.toUpperCase()}: ${result.outputSize} bytes`);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.medium,
);
}
});
// LibreOffice 轉換
describe("LibreOffice (Office 格式)", () => {
const conversions = [
["html", "pdf"],
["html", "docx"],
["txt", "pdf"],
];
for (const [from, to] of conversions) {
test(
`${from.toUpperCase()} → ${to.toUpperCase()}`,
async () => {
if (!availableTools.libreoffice) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: LibreOffice not available`);
return;
}
stats.total++;
const inputPath = join(outputDir, `libreoffice_input.${from}`);
const outputPath = join(outputDir, `libreoffice_${from}_to_${to}.${to}`);
if (from === "html") createTestHtml(inputPath);
else if (from === "txt") createTestTxt(inputPath);
const result = await runConversion("libreoffice", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(
` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.slow,
);
}
});
});
// =============================================================================
// 資料格式轉換測試
// =============================================================================
describe("📊 資料格式轉換 Data Format Conversions", () => {
const outputDir = join(E2E_OUTPUT_DIR, "data");
beforeAll(() => {
ensureDir(outputDir);
});
// Dasel 轉換
describe("Dasel (結構化資料格式)", () => {
const conversions = [
["json", "yaml"],
["json", "toml"],
["json", "xml"],
["yaml", "json"],
["csv", "json"],
];
for (const [from, to] of conversions) {
test(
`${from.toUpperCase()} → ${to.toUpperCase()}`,
async () => {
if (!availableTools.dasel) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Dasel not available`);
return;
}
stats.total++;
const inputPath = join(outputDir, `dasel_input.${from}`);
const outputPath = join(outputDir, `dasel_${from}_to_${to}.${to}`);
if (from === "json") createTestJson(inputPath);
else if (from === "yaml") {
writeFileSync(inputPath, "name: test\nvalue: 42\n");
} else if (from === "csv") {
createTestCsv(inputPath);
}
const result = await runConversion("dasel", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(
` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.fast,
);
}
});
});
// =============================================================================
// 多語言翻譯測試
// =============================================================================
describe("🌍 多語言翻譯 Multilingual Translation", () => {
const outputDir = join(E2E_OUTPUT_DIR, "translation");
beforeAll(() => {
ensureDir(outputDir);
});
// 支援的語言
const LANGUAGES = [
{ code: "zh", name: "簡體中文" },
{ code: "zh-TW", name: "繁體中文" },
{ code: "ja", name: "日本語" },
{ code: "ko", name: "한국어" },
{ code: "de", name: "Deutsch" },
{ code: "fr", name: "Français" },
{ code: "es", name: "Español" },
{ code: "ru", name: "Русский" },
];
// PDFMathTranslate 測試
describe("PDFMathTranslate (PDF 翻譯)", () => {
// 注意:這些測試需要網路連接和 PDF 測試檔案
for (const lang of LANGUAGES.slice(0, 4)) {
// 只測試前 4 種語言
test.skip(
`PDF → ${lang.name} (${lang.code})`,
async () => {
if (!availableTools.pdf2zh) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: pdf2zh not available`);
return;
}
stats.total++;
// 這裡需要一個真實的 PDF 測試檔案
const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf");
const outputPath = join(outputDir, `translated_${lang.code}.tar`);
if (!existsSync(inputPath)) {
stats.skipped++;
console.log(`⏭ Skipping: sample.pdf not found in fixtures`);
return;
}
const result = await runConversion("pdfmathtranslate", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(
` ✓ PDF → ${lang.name}: ${result.outputSize} bytes (${result.duration}ms)`,
);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.translation,
);
}
});
// BabelDOC 測試
describe("BabelDOC (進階 PDF 翻譯)", () => {
for (const lang of LANGUAGES.slice(0, 2)) {
// 只測試中英
test.skip(
`PDF → ${lang.name} (babeldoc)`,
async () => {
if (!availableTools.babeldoc) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: babeldoc not available`);
return;
}
stats.total++;
const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf");
const outputPath = join(outputDir, `babeldoc_${lang.code}.tar`);
if (!existsSync(inputPath)) {
stats.skipped++;
console.log(`⏭ Skipping: sample.pdf not found in fixtures`);
return;
}
const result = await runConversion("babeldoc", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(` ✓ PDF → ${lang.name} (babeldoc): ${result.outputSize} bytes`);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.translation,
);
}
});
});
// =============================================================================
// 電子書格式轉換測試
// =============================================================================
describe("📚 電子書格式轉換 Ebook Conversions", () => {
const outputDir = join(E2E_OUTPUT_DIR, "ebooks");
beforeAll(() => {
ensureDir(outputDir);
});
// Calibre 轉換
describe("Calibre (電子書格式)", () => {
// 使用 HTML 作為輸入源
const conversions = [
["html", "epub"],
["html", "mobi"],
["html", "pdf"],
["txt", "epub"],
];
for (const [from, to] of conversions) {
test(
`${from.toUpperCase()} → ${to.toUpperCase()}`,
async () => {
if (!availableTools.calibre || !availableTools.xvfbRun) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Calibre or xvfb-run not available`);
return;
}
stats.total++;
const inputPath = join(outputDir, `calibre_input.${from}`);
const outputPath = join(outputDir, `calibre_${from}_to_${to}.${to}`);
if (from === "html") createTestHtml(inputPath);
else if (from === "txt") createTestTxt(inputPath);
const result = await runConversion("calibre", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(
` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.slow,
);
}
});
});
// =============================================================================
// 音視頻格式轉換測試
// =============================================================================
describe("🎬 音視頻格式轉換 Media Conversions", () => {
const outputDir = join(E2E_OUTPUT_DIR, "media");
beforeAll(() => {
ensureDir(outputDir);
});
// FFmpeg 音頻轉換
describe("FFmpeg (音視頻格式)", () => {
// 注意:這些測試需要實際的音視頻測試檔案
test.skip(
"MP3 → WAV",
async () => {
if (!availableTools.ffmpeg) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: FFmpeg not available`);
return;
}
stats.total++;
const inputPath = join(E2E_FIXTURES_DIR, "sample.mp3");
const outputPath = join(outputDir, "ffmpeg_output.wav");
if (!existsSync(inputPath)) {
stats.skipped++;
console.log(`⏭ Skipping: sample.mp3 not found`);
return;
}
const result = await runConversion("ffmpeg", inputPath, outputPath);
stats.results.push(result);
expect(result.success).toBe(true);
},
TIMEOUT.medium,
);
});
});
// =============================================================================
// 格式轉換矩陣測試
// =============================================================================
describe("🔢 格式轉換矩陣 Conversion Matrix", () => {
/**
* 這個測試會生成一個轉換矩陣報告
* 顯示所有支援的格式轉換組合
*/
test("生成格式轉換矩陣報告", async () => {
const converters = [
"inkscape",
"imagemagick",
"graphicsmagick",
"pandoc",
"dasel",
"potrace",
"vtracer",
"libreoffice",
"calibre",
];
const matrix: Record = {};
for (const converter of converters) {
try {
const module = await import(`../../src/converters/${converter}`);
const props = module.properties;
if (props) {
const fromFormats: string[] = [];
const toFormats: string[] = [];
for (const category of Object.values(props.from || {})) {
fromFormats.push(...(category as string[]));
}
for (const category of Object.values(props.to || {})) {
toFormats.push(...(category as string[]));
}
matrix[converter] = {
from: [...new Set(fromFormats)],
to: [...new Set(toFormats)],
};
}
} catch {
// 忽略無法載入的轉換器
}
}
// 計算總格式數
let totalFrom = 0;
let totalTo = 0;
let totalCombinations = 0;
console.log("\n📊 格式轉換矩陣 Conversion Matrix\n");
console.log("-".repeat(60));
for (const [converter, formats] of Object.entries(matrix)) {
const combinations = formats.from.length * formats.to.length;
totalFrom += formats.from.length;
totalTo += formats.to.length;
totalCombinations += combinations;
console.log(`${converter}:`);
console.log(
` 輸入格式: ${formats.from.length} (${formats.from.slice(0, 5).join(", ")}${formats.from.length > 5 ? "..." : ""})`,
);
console.log(
` 輸出格式: ${formats.to.length} (${formats.to.slice(0, 5).join(", ")}${formats.to.length > 5 ? "..." : ""})`,
);
console.log(` 組合數: ${combinations}`);
console.log();
}
console.log("-".repeat(60));
console.log(`總計:`);
console.log(` 轉換器數量: ${Object.keys(matrix).length}`);
console.log(` 輸入格式總數: ${totalFrom}`);
console.log(` 輸出格式總數: ${totalTo}`);
console.log(` 理論轉換組合: ${totalCombinations}`);
console.log("-".repeat(60));
// 保存矩陣報告
const reportPath = join(E2E_OUTPUT_DIR, "conversion-matrix.json");
writeFileSync(reportPath, JSON.stringify(matrix, null, 2));
console.log(`\n📁 矩陣報告已保存: ${reportPath}`);
expect(Object.keys(matrix).length).toBeGreaterThan(0);
});
});
// =============================================================================
// 邊界條件測試
// =============================================================================
describe("⚠️ 邊界條件測試 Edge Cases", () => {
const outputDir = join(E2E_OUTPUT_DIR, "edge-cases");
beforeAll(() => {
ensureDir(outputDir);
});
test("空檔案處理", async () => {
stats.total++;
const inputPath = join(outputDir, "empty.txt");
const outputPath = join(outputDir, "empty_output.html");
writeFileSync(inputPath, "");
// 某些轉換器可能可以處理空檔案
try {
const result = await runConversion("pandoc", inputPath, outputPath);
if (result.success) {
stats.passed++;
console.log(" ✓ 空檔案處理成功");
} else {
stats.passed++; // 失敗也是預期行為
console.log(" ✓ 空檔案正確拒絕");
}
} catch {
stats.passed++;
console.log(" ✓ 空檔案正確拒絕");
}
});
test("Unicode 檔名處理", async () => {
if (!availableTools.pandoc) {
stats.skipped++;
stats.total++;
return;
}
stats.total++;
const inputPath = join(outputDir, "測試文件_テスト_테스트.markdown");
const outputPath = join(outputDir, "unicode_output.html");
createTestMarkdown(inputPath);
const result = await runConversion("pandoc", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(" ✓ Unicode 檔名處理成功");
} else {
stats.failed++;
}
expect(result.success).toBe(true);
});
test(
"超長內容處理",
async () => {
if (!availableTools.pandoc) {
stats.skipped++;
stats.total++;
return;
}
stats.total++;
const inputPath = join(outputDir, "long_content.markdown");
const outputPath = join(outputDir, "long_output.html");
// 生成 10000 行的文件
const lines = Array.from(
{ length: 10000 },
(_, i) => `第 ${i + 1} 行:這是測試內容 Line ${i + 1}: This is test content`,
).join("\n");
writeFileSync(inputPath, `# 長文件測試\n\n${lines}`);
const result = await runConversion("pandoc", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(` ✓ 超長內容處理成功: ${result.outputSize} bytes`);
} else {
stats.failed++;
}
expect(result.success).toBe(true);
},
TIMEOUT.slow,
);
test("特殊字元處理", async () => {
if (!availableTools.pandoc) {
stats.skipped++;
stats.total++;
return;
}
stats.total++;
const inputPath = join(outputDir, "special_chars.markdown");
const outputPath = join(outputDir, "special_output.html");
writeFileSync(
inputPath,
`# 特殊字元測試
& < > " ' \` ~ ! @ # $ % ^ & * ( ) - = + [ ] { } | \\ : ; < > ? , . /
數學符號:∑ ∏ ∫ √ ∞ ≠ ≤ ≥ ± × ÷ π
表情符號:😀 🎉 🚀 ❤️ 🔥 💯 ✅ ❌
CJK 擴展:㊀ ㊁ ㊂ ㊃ ㊄
全形標點:「」『』【】〈〉《》
`,
);
const result = await runConversion("pandoc", inputPath, outputPath);
stats.results.push(result);
if (result.success) {
stats.passed++;
console.log(" ✓ 特殊字元處理成功");
} else {
stats.failed++;
}
expect(result.success).toBe(true);
});
});