/** * 全面 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); }); });