From a2512bf29b8a9e22b0da10bfa71c9cf60d8270c7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 23 Jan 2026 12:27:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=20lint=20=E9=8C=AF?= =?UTF-8?q?=E8=AA=A4=E5=92=8C=E4=BB=A3=E7=A2=BC=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除未使用的導入 (readdirSync, basename, readFileSync, execSync) - 將不需要重新賦值的 let 改為 const - 修復 switch case 區塊中的詞法聲明問題 - 移除未使用的 SAMPLING_RATE 和 sampled 變量 - 移除未使用的 _timeout 參數 - 運行 Prettier 格式化所有文件 --- .github/workflows/docker-e2e-tests.yml | 18 +- .github/workflows/run-bun-test.yml | 1 - src/converters/pdfmathtranslate.ts | 4 +- tests/e2e/comprehensive.e2e.test.ts | 682 ++++++++++++++----------- tests/e2e/format-matrix.e2e.test.ts | 517 ++++++++++--------- tests/e2e/translation.e2e.test.ts | 376 ++++++++------ 6 files changed, 886 insertions(+), 712 deletions(-) diff --git a/.github/workflows/docker-e2e-tests.yml b/.github/workflows/docker-e2e-tests.yml index 56d1e15..75ee175 100644 --- a/.github/workflows/docker-e2e-tests.yml +++ b/.github/workflows/docker-e2e-tests.yml @@ -132,15 +132,15 @@ jobs: - name: Run API health check run: | echo "🔍 Testing API endpoints..." - + # Health check curl -f http://localhost:3000/healthcheck echo "" - + # Check if main page loads curl -f -o /dev/null -w "%{http_code}" http://localhost:3000/ echo "" - + echo "✅ API health checks passed!" # ============================================ @@ -175,7 +175,7 @@ jobs: - name: Run E2E tests inside Docker container run: | echo "🧪 Running comprehensive E2E tests inside Docker..." - + docker run --rm \ --name convertx-e2e-test \ -v "${{ github.workspace }}/tests:/app/tests" \ @@ -269,7 +269,7 @@ jobs: DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }} run: | echo "🌍 Running translation tests..." - + docker run --rm \ --name convertx-translation-test \ -v "${{ github.workspace }}/tests:/app/tests" \ @@ -320,7 +320,7 @@ jobs: script: | const tag = `test-${{ github.sha }}`; console.log(`🗑️ Attempting to delete test image with tag: ${tag}`); - + // Note: This requires ghcr.io API access which may need additional setup // For now, images will be cleaned up by retention policies console.log('Test images will be cleaned up by retention policies'); @@ -353,12 +353,12 @@ jobs: echo "| E2E Tests | ${{ needs.e2e-tests.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Comprehensive E2E | ${{ needs.comprehensive-e2e.result }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + if [ -f "test-results/format-matrix/matrix-summary.md" ]; then echo "## Format Matrix Summary" >> $GITHUB_STEP_SUMMARY cat test-results/format-matrix/matrix-summary.md >> $GITHUB_STEP_SUMMARY fi - + if [ -f "test-results/comprehensive/conversion-matrix.json" ]; then echo "" >> $GITHUB_STEP_SUMMARY echo "## Conversion Matrix" >> $GITHUB_STEP_SUMMARY @@ -366,6 +366,6 @@ jobs: head -50 test-results/comprehensive/conversion-matrix.json >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY fi - + echo "" >> $GITHUB_STEP_SUMMARY echo "📅 Test run completed at: $(date -u)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/run-bun-test.yml b/.github/workflows/run-bun-test.yml index f75d769..74582cb 100644 --- a/.github/workflows/run-bun-test.yml +++ b/.github/workflows/run-bun-test.yml @@ -51,4 +51,3 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "Basic tests completed. For comprehensive E2E tests with all converters," >> $GITHUB_STEP_SUMMARY echo "see the [Docker E2E Tests](${{ github.server_url }}/${{ github.repository }}/actions/workflows/docker-e2e-tests.yml) workflow." >> $GITHUB_STEP_SUMMARY - diff --git a/src/converters/pdfmathtranslate.ts b/src/converters/pdfmathtranslate.ts index 168e837..2a05b49 100644 --- a/src/converters/pdfmathtranslate.ts +++ b/src/converters/pdfmathtranslate.ts @@ -120,7 +120,7 @@ function checkModelsExist(): boolean { console.warn(`[PDFMathTranslate] Models should be pre-downloaded during Docker build.`); return false; } - + // 檢查特定的 ONNX 模型檔案 const onnxModelPath = join(MODELS_PATH, "doclayout_yolo_docstructbench_imgsz1024.onnx"); if (!existsSync(onnxModelPath)) { @@ -128,7 +128,7 @@ function checkModelsExist(): boolean { console.warn(`[PDFMathTranslate] Model should be pre-downloaded during Docker build.`); return false; } - + return true; } diff --git a/tests/e2e/comprehensive.e2e.test.ts b/tests/e2e/comprehensive.e2e.test.ts index 24410e5..c5e131e 100644 --- a/tests/e2e/comprehensive.e2e.test.ts +++ b/tests/e2e/comprehensive.e2e.test.ts @@ -15,8 +15,8 @@ */ import { describe, test, expect, beforeAll, afterAll } from "bun:test"; -import { existsSync, statSync, mkdirSync, writeFileSync, readdirSync } from "node:fs"; -import { join, basename } from "node:path"; +import { existsSync, statSync, mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; import { spawnSync } from "node:child_process"; // ============================================================================= @@ -28,9 +28,9 @@ const E2E_FIXTURES_DIR = "tests/e2e/fixtures"; // 測試超時(毫秒) const TIMEOUT = { - fast: 30_000, // 快速轉換 - medium: 60_000, // 中等複雜度 - slow: 180_000, // 複雜轉換 + fast: 30_000, // 快速轉換 + medium: 60_000, // 中等複雜度 + slow: 180_000, // 複雜轉換 translation: 300_000, // 翻譯(需網路) }; @@ -97,25 +97,24 @@ function ensureDir(dir: string): void { } function createTestSvg(path: string): void { - writeFileSync(path, ` + 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, + 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); @@ -142,7 +141,9 @@ function createTestBmp(path: string): void { } function createTestMarkdown(path: string): void { - writeFileSync(path, `# 測試文件 Test Document + writeFileSync( + path, + `# 測試文件 Test Document 這是一個**多語言**測試文件。 @@ -173,23 +174,33 @@ This is an English paragraph for testing. \`\`\`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)); + 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, ` + writeFileSync( + path, + ` @@ -203,11 +214,14 @@ function createTestHtml(path: string): void {
  • 項目二
  • -`); +`, + ); } function createTestTxt(path: string): void { - writeFileSync(path, `測試文字檔案 Test Text File + writeFileSync( + path, + `測試文字檔案 Test Text File 這是一個純文字測試檔案。 This is a plain text test file. @@ -220,16 +234,20 @@ This is a plain text test file. - 한국어 - Deutsch - Français -`); +`, + ); } function createTestCsv(path: string): void { - writeFileSync(path, `name,value,description + writeFileSync( + path, + `name,value,description 測試1,100,這是第一項 測試2,200,這是第二項 test3,300,This is the third item テスト4,400,これは4番目の項目です -`); +`, + ); } // ============================================================================= @@ -250,7 +268,6 @@ async function runConversion( converter: string, inputPath: string, outputPath: string, - timeout = TIMEOUT.medium, ): Promise { const startTime = Date.now(); const inputSize = existsSync(inputPath) ? statSync(inputPath).size : 0; @@ -315,7 +332,9 @@ function printSummary(): void { 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( + `成功率 Success Rate: ${((stats.passed / (stats.total - stats.skipped)) * 100).toFixed(1)}%`, + ); console.log("=".repeat(60)); } @@ -323,18 +342,20 @@ function printSummary(): void { // 測試套件 // ============================================================================= -let availableTools: Record = {}; +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( + ` ${icon} ${name}: ${status.available ? status.version || "available" : "not found"}`, + ); } - + console.log("\n"); ensureDir(E2E_OUTPUT_DIR); }); @@ -365,29 +386,35 @@ describe("🖼️ 圖像格式轉換 Image Conversions", () => { }); for (const format of formats) { - test(`SVG → ${format.toUpperCase()}`, async () => { - if (!availableTools.inkscape || !availableTools.xvfbRun) { - stats.skipped++; + 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++; - console.log(`⏭ Skipping: xvfb-run or inkscape not available`); - return; - } + const outputPath = join(outputDir, `inkscape_output.${format}`); + const result = await runConversion("inkscape", inputPath, outputPath); + stats.results.push(result); - 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}`); + } - 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); + expect(result.success).toBe(true); + }, + TIMEOUT.medium, + ); } }); @@ -403,33 +430,39 @@ describe("🖼️ 圖像格式轉換 Image Conversions", () => { ]; for (const [from, to] of conversions) { - test(`${from.toUpperCase()} → ${to.toUpperCase()}`, async () => { - if (!availableTools.imagemagick) { - stats.skipped++; + test( + `${from.toUpperCase()} → ${to.toUpperCase()}`, + async () => { + if (!availableTools.imagemagick) { + stats.skipped++; + stats.total++; + console.log(`⏭ Skipping: ImageMagick not available`); + return; + } + stats.total++; - console.log(`⏭ Skipping: ImageMagick not available`); - return; - } + const inputPath = join(outputDir, `imagemagick_input.${from}`); + const outputPath = join(outputDir, `imagemagick_${from}_to_${to}.${to}`); - 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); - if (from === "png") createTestPng(inputPath); - else if (from === "bmp") createTestBmp(inputPath); + const result = await runConversion("imagemagick", inputPath, outputPath); + stats.results.push(result); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.fast); + expect(result.success).toBe(true); + }, + TIMEOUT.fast, + ); } }); @@ -438,31 +471,35 @@ describe("🖼️ 圖像格式轉換 Image Conversions", () => { const formats = ["svg", "eps", "pdf"]; for (const format of formats) { - test(`BMP → ${format.toUpperCase()}`, async () => { - if (!availableTools.potrace) { - stats.skipped++; + test( + `BMP → ${format.toUpperCase()}`, + async () => { + if (!availableTools.potrace) { + stats.skipped++; + stats.total++; + console.log(`⏭ Skipping: Potrace not available`); + return; + } + stats.total++; - console.log(`⏭ Skipping: Potrace not available`); - return; - } + const inputPath = join(outputDir, "potrace_input.bmp"); + const outputPath = join(outputDir, `potrace_output.${format}`); + createTestBmp(inputPath); - 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); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ BMP → ${format.toUpperCase()}: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.fast); + expect(result.success).toBe(true); + }, + TIMEOUT.fast, + ); } }); }); @@ -488,28 +525,32 @@ describe("📄 文件格式轉換 Document Conversions", () => { }); for (const format of formats) { - test(`Markdown → ${format.toUpperCase()}`, async () => { - if (!availableTools.pandoc) { - stats.skipped++; + test( + `Markdown → ${format.toUpperCase()}`, + async () => { + if (!availableTools.pandoc) { + stats.skipped++; + stats.total++; + console.log(`⏭ Skipping: Pandoc not available`); + return; + } + stats.total++; - console.log(`⏭ Skipping: Pandoc not available`); - return; - } + const outputPath = join(outputDir, `pandoc_output.${format}`); + const result = await runConversion("pandoc", inputPath, outputPath); + stats.results.push(result); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ Markdown → ${format.toUpperCase()}: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.medium); + expect(result.success).toBe(true); + }, + TIMEOUT.medium, + ); } }); @@ -522,33 +563,39 @@ describe("📄 文件格式轉換 Document Conversions", () => { ]; for (const [from, to] of conversions) { - test(`${from.toUpperCase()} → ${to.toUpperCase()}`, async () => { - if (!availableTools.libreoffice) { - stats.skipped++; + test( + `${from.toUpperCase()} → ${to.toUpperCase()}`, + async () => { + if (!availableTools.libreoffice) { + stats.skipped++; + stats.total++; + console.log(`⏭ Skipping: LibreOffice not available`); + return; + } + stats.total++; - console.log(`⏭ Skipping: LibreOffice not available`); - return; - } + const inputPath = join(outputDir, `libreoffice_input.${from}`); + const outputPath = join(outputDir, `libreoffice_${from}_to_${to}.${to}`); - 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); - if (from === "html") createTestHtml(inputPath); - else if (from === "txt") createTestTxt(inputPath); + const result = await runConversion("libreoffice", inputPath, outputPath); + stats.results.push(result); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.slow); + expect(result.success).toBe(true); + }, + TIMEOUT.slow, + ); } }); }); @@ -575,37 +622,43 @@ describe("📊 資料格式轉換 Data Format Conversions", () => { ]; for (const [from, to] of conversions) { - test(`${from.toUpperCase()} → ${to.toUpperCase()}`, async () => { - if (!availableTools.dasel) { - stats.skipped++; + test( + `${from.toUpperCase()} → ${to.toUpperCase()}`, + async () => { + if (!availableTools.dasel) { + stats.skipped++; + stats.total++; + console.log(`⏭ Skipping: Dasel not available`); + return; + } + stats.total++; - console.log(`⏭ Skipping: Dasel not available`); - return; - } + const inputPath = join(outputDir, `dasel_input.${from}`); + const outputPath = join(outputDir, `dasel_${from}_to_${to}.${to}`); - 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); + } - 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); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.fast); + expect(result.success).toBe(true); + }, + TIMEOUT.fast, + ); } }); }); @@ -636,75 +689,87 @@ describe("🌍 多語言翻譯 Multilingual Translation", () => { // 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++; + + 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++; - console.log(`⏭ Skipping: pdf2zh not available`); - return; - } + // 這裡需要一個真實的 PDF 測試檔案 + const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf"); + const outputPath = join(outputDir, `translated_${lang.code}.tar`); - 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; + } - 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); - 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++; + } - 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); + 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++; + 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++; - console.log(`⏭ Skipping: babeldoc not available`); - return; - } + const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf"); + const outputPath = join(outputDir, `babeldoc_${lang.code}.tar`); - 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; + } - 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); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ PDF → ${lang.name} (babeldoc): ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.translation); + expect(result.success).toBe(true); + }, + TIMEOUT.translation, + ); } }); }); @@ -731,33 +796,39 @@ describe("📚 電子書格式轉換 Ebook Conversions", () => { ]; for (const [from, to] of conversions) { - test(`${from.toUpperCase()} → ${to.toUpperCase()}`, async () => { - if (!availableTools.calibre || !availableTools.xvfbRun) { - stats.skipped++; + 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++; - console.log(`⏭ Skipping: Calibre or xvfb-run not available`); - return; - } + const inputPath = join(outputDir, `calibre_input.${from}`); + const outputPath = join(outputDir, `calibre_${from}_to_${to}.${to}`); - 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); - if (from === "html") createTestHtml(inputPath); - else if (from === "txt") createTestTxt(inputPath); + const result = await runConversion("calibre", inputPath, outputPath); + stats.results.push(result); - 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++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ ${from.toUpperCase()} → ${to.toUpperCase()}: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.slow); + expect(result.success).toBe(true); + }, + TIMEOUT.slow, + ); } }); }); @@ -777,29 +848,33 @@ describe("🎬 音視頻格式轉換 Media Conversions", () => { describe("FFmpeg (音視頻格式)", () => { // 注意:這些測試需要實際的音視頻測試檔案 - test.skip("MP3 → WAV", async () => { - if (!availableTools.ffmpeg) { - stats.skipped++; + test.skip( + "MP3 → WAV", + async () => { + if (!availableTools.ffmpeg) { + stats.skipped++; + stats.total++; + console.log(`⏭ Skipping: FFmpeg not available`); + return; + } + stats.total++; - console.log(`⏭ Skipping: FFmpeg not available`); - return; - } + const inputPath = join(E2E_FIXTURES_DIR, "sample.mp3"); + const outputPath = join(outputDir, "ffmpeg_output.wav"); - 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; + } - if (!existsSync(inputPath)) { - stats.skipped++; - console.log(`⏭ Skipping: sample.mp3 not found`); - return; - } + const result = await runConversion("ffmpeg", inputPath, outputPath); + stats.results.push(result); - const result = await runConversion("ffmpeg", inputPath, outputPath); - stats.results.push(result); - - expect(result.success).toBe(true); - }, TIMEOUT.medium); + expect(result.success).toBe(true); + }, + TIMEOUT.medium, + ); }); }); @@ -812,11 +887,18 @@ describe("🔢 格式轉換矩陣 Conversion Matrix", () => { * 這個測試會生成一個轉換矩陣報告 * 顯示所有支援的格式轉換組合 */ - + test("生成格式轉換矩陣報告", async () => { const converters = [ - "inkscape", "imagemagick", "graphicsmagick", "pandoc", - "dasel", "potrace", "vtracer", "libreoffice", "calibre", + "inkscape", + "imagemagick", + "graphicsmagick", + "pandoc", + "dasel", + "potrace", + "vtracer", + "libreoffice", + "calibre", ]; const matrix: Record = {}; @@ -862,8 +944,12 @@ describe("🔢 格式轉換矩陣 Conversion Matrix", () => { 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( + ` 輸入格式: ${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(); } @@ -944,35 +1030,40 @@ describe("⚠️ 邊界條件測試 Edge Cases", () => { expect(result.success).toBe(true); }); - test("超長內容處理", async () => { - if (!availableTools.pandoc) { - stats.skipped++; + test( + "超長內容處理", + async () => { + if (!availableTools.pandoc) { + stats.skipped++; + stats.total++; + return; + } + stats.total++; - return; - } + const inputPath = join(outputDir, "long_content.markdown"); + const outputPath = join(outputDir, "long_output.html"); - 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}`); - // 生成 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); - const result = await runConversion("pandoc", inputPath, outputPath); - stats.results.push(result); + if (result.success) { + stats.passed++; + console.log(` ✓ 超長內容處理成功: ${result.outputSize} bytes`); + } else { + stats.failed++; + } - if (result.success) { - stats.passed++; - console.log(` ✓ 超長內容處理成功: ${result.outputSize} bytes`); - } else { - stats.failed++; - } - - expect(result.success).toBe(true); - }, TIMEOUT.slow); + expect(result.success).toBe(true); + }, + TIMEOUT.slow, + ); test("特殊字元處理", async () => { if (!availableTools.pandoc) { @@ -985,7 +1076,9 @@ describe("⚠️ 邊界條件測試 Edge Cases", () => { const inputPath = join(outputDir, "special_chars.markdown"); const outputPath = join(outputDir, "special_output.html"); - writeFileSync(inputPath, `# 特殊字元測試 + writeFileSync( + inputPath, + `# 特殊字元測試 & < > " ' \` ~ ! @ # $ % ^ & * ( ) - = + [ ] { } | \\ : ; < > ? , . / @@ -996,7 +1089,8 @@ describe("⚠️ 邊界條件測試 Edge Cases", () => { CJK 擴展:㊀ ㊁ ㊂ ㊃ ㊄ 全形標點:「」『』【】〈〉《》 -`); +`, + ); const result = await runConversion("pandoc", inputPath, outputPath); stats.results.push(result); diff --git a/tests/e2e/format-matrix.e2e.test.ts b/tests/e2e/format-matrix.e2e.test.ts index da6c6dd..72c3b7a 100644 --- a/tests/e2e/format-matrix.e2e.test.ts +++ b/tests/e2e/format-matrix.e2e.test.ts @@ -1,20 +1,20 @@ /** * 格式轉換矩陣測試 - * + * * 測試目標:驗證 1000+ 種格式轉換組合 - * + * * 這個測試會: * 1. 自動發現所有轉換器支援的格式 * 2. 生成所有可能的轉換組合 * 3. 執行抽樣測試(避免耗時過長) * 4. 生成詳細的測試報告 - * + * * 執行方式: * bun test tests/e2e/format-matrix.e2e.test.ts */ import { describe, test, expect, beforeAll, afterAll } from "bun:test"; -import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; @@ -25,9 +25,6 @@ import { spawnSync } from "node:child_process"; const E2E_OUTPUT_DIR = "tests/e2e/output/format-matrix"; const TIMEOUT = 60_000; // 60 秒超時 -// 抽樣比例(0.1 = 10%,避免測試過長) -const SAMPLING_RATE = 0.05; - // 每個轉換器最大測試數 const MAX_TESTS_PER_CONVERTER = 20; @@ -118,18 +115,14 @@ function createTestContent(format: string): Buffer | string { case "png": return 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, + 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, ]); - case "bmp": + case "bmp": { const bmp = Buffer.alloc(70); bmp.write("BM", 0); bmp.writeUInt32LE(70, 2); @@ -143,6 +136,7 @@ function createTestContent(format: string): Buffer | string { bmp[55] = 0x00; bmp[56] = 0x00; return bmp; + } // 文檔格式 case "md": @@ -232,7 +226,21 @@ const CONVERTERS: ConverterConfig[] = [ name: "imagemagick", command: "magick", from: ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp", "ico", "ppm", "pgm", "pbm"], - to: ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp", "ico", "ppm", "pgm", "pbm", "pdf"], + to: [ + "png", + "jpg", + "jpeg", + "gif", + "bmp", + "tiff", + "tif", + "webp", + "ico", + "ppm", + "pgm", + "pbm", + "pdf", + ], }, { name: "graphicsmagick", @@ -316,7 +324,7 @@ const CONVERTERS: ConverterConfig[] = [ // 主測試 // ============================================================================= -let report: MatrixReport = { +const report: MatrixReport = { generatedAt: new Date().toISOString(), converters: [], totalCombinations: 0, @@ -350,7 +358,9 @@ afterAll(() => { console.log(`通過 Passed: ${report.passedTests}`); console.log(`失敗 Failed: ${report.failedTests}`); console.log(`跳過 Skipped: ${report.skippedTests}`); - console.log(`成功率 Success Rate: ${((report.passedTests / (report.testedCombinations || 1)) * 100).toFixed(1)}%`); + console.log( + `成功率 Success Rate: ${((report.passedTests / (report.testedCombinations || 1)) * 100).toFixed(1)}%`, + ); console.log("=".repeat(70)); console.log(`📁 報告路徑: ${reportPath}`); console.log(`📄 摘要路徑: ${summaryPath}`); @@ -380,7 +390,7 @@ function generateMarkdownSummary(report: MatrixReport): string { } md += `## 失敗的測試\n\n`; - const failedResults = report.results.filter(r => !r.success); + const failedResults = report.results.filter((r) => !r.success); if (failedResults.length === 0) { md += `無失敗的測試 ✅\n`; } else { @@ -399,7 +409,6 @@ function generateMarkdownSummary(report: MatrixReport): string { // ============================================================================= describe("📊 格式轉換矩陣 Format Conversion Matrix", () => { - describe("發現可用轉換器 Discover Available Converters", () => { test("檢測所有轉換器", () => { for (const config of CONVERTERS) { @@ -427,7 +436,9 @@ describe("📊 格式轉換矩陣 Format Conversion Matrix", () => { report.totalCombinations += combinations.length; const icon = available ? "✅" : "❌"; - console.log(` ${icon} ${config.name}: ${combinations.length} 組合 (${available ? "可用" : "不可用"})`); + console.log( + ` ${icon} ${config.name}: ${combinations.length} 組合 (${available ? "可用" : "不可用"})`, + ); } console.log(`\n 📈 總組合數: ${report.totalCombinations}`); @@ -436,82 +447,98 @@ describe("📊 格式轉換矩陣 Format Conversion Matrix", () => { }); describe("圖像格式轉換 Image Format Conversions", () => { - const imageConverters = ["inkscape", "imagemagick", "graphicsmagick", "potrace", "vtracer", "resvg", "vips"]; + const imageConverters = [ + "inkscape", + "imagemagick", + "graphicsmagick", + "potrace", + "vtracer", + "resvg", + "vips", + ]; for (const converterName of imageConverters) { describe(`${converterName}`, () => { - test(`測試格式轉換`, async () => { - const converter = report.converters.find(c => c.name === converterName); - if (!converter) { - report.skippedTests++; - console.log(` ⏭ ${converterName}: 轉換器未找到`); - return; - } - - if (!converter.available) { - report.skippedTests += converter.combinations.length; - console.log(` ⏭ ${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`); - return; - } - - // 抽樣測試 - const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER); - console.log(` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`); - - for (const [from, to] of sampled) { - report.testedCombinations++; - const startTime = Date.now(); - - try { - // 建立測試檔案 - const inputDir = join(E2E_OUTPUT_DIR, converterName); - ensureDir(inputDir); - const inputPath = join(inputDir, `input.${from}`); - const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`); - - const content = createTestContent(from); - if (Buffer.isBuffer(content)) { - writeFileSync(inputPath, content); - } else { - writeFileSync(inputPath, content, "utf-8"); - } - - // 動態導入並執行轉換 - const module = await import(`../../src/converters/${converterName}`); - await module.convert(inputPath, from, to, outputPath); - - const duration = Date.now() - startTime; - const success = existsSync(outputPath); - - report.results.push({ - converter: converterName, - from, - to, - success, - duration, - }); - - if (success) { - report.passedTests++; - } else { - report.failedTests++; - console.log(` ❌ ${from} → ${to}: 輸出檔案不存在`); - } - } catch (error) { - const duration = Date.now() - startTime; - report.failedTests++; - report.results.push({ - converter: converterName, - from, - to, - success: false, - duration, - error: String(error), - }); - console.log(` ❌ ${from} → ${to}: ${error}`); + test( + `測試格式轉換`, + async () => { + const converter = report.converters.find((c) => c.name === converterName); + if (!converter) { + report.skippedTests++; + console.log(` ⏭ ${converterName}: 轉換器未找到`); + return; } - } - }, TIMEOUT * sampled.length); + + if (!converter.available) { + report.skippedTests += converter.combinations.length; + console.log( + ` ⏭ ${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`, + ); + return; + } + + // 抽樣測試 + const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER); + console.log( + ` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`, + ); + + for (const [from, to] of sampled) { + report.testedCombinations++; + const startTime = Date.now(); + + try { + // 建立測試檔案 + const inputDir = join(E2E_OUTPUT_DIR, converterName); + ensureDir(inputDir); + const inputPath = join(inputDir, `input.${from}`); + const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`); + + const content = createTestContent(from); + if (Buffer.isBuffer(content)) { + writeFileSync(inputPath, content); + } else { + writeFileSync(inputPath, content, "utf-8"); + } + + // 動態導入並執行轉換 + const module = await import(`../../src/converters/${converterName}`); + await module.convert(inputPath, from, to, outputPath); + + const duration = Date.now() - startTime; + const success = existsSync(outputPath); + + report.results.push({ + converter: converterName, + from, + to, + success, + duration, + }); + + if (success) { + report.passedTests++; + } else { + report.failedTests++; + console.log(` ❌ ${from} → ${to}: 輸出檔案不存在`); + } + } catch (error) { + const duration = Date.now() - startTime; + report.failedTests++; + report.results.push({ + converter: converterName, + from, + to, + success: false, + duration, + error: String(error), + }); + console.log(` ❌ ${from} → ${to}: ${error}`); + } + } + }, + TIMEOUT * sampled.length, + ); }); } }); @@ -521,85 +548,94 @@ describe("📊 格式轉換矩陣 Format Conversion Matrix", () => { for (const converterName of docConverters) { describe(`${converterName}`, () => { - test(`測試格式轉換`, async () => { - const converter = report.converters.find(c => c.name === converterName); - if (!converter) { - report.skippedTests++; - console.log(` ⏭ ${converterName}: 轉換器未找到`); - return; - } - - if (!converter.available) { - report.skippedTests += converter.combinations.length; - console.log(` ⏭ ${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`); - return; - } - - // 抽樣測試 - const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER); - console.log(` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`); - - for (const [from, to] of sampled) { - report.testedCombinations++; - const startTime = Date.now(); - - try { - const inputDir = join(E2E_OUTPUT_DIR, converterName); - ensureDir(inputDir); - const inputPath = join(inputDir, `input.${from}`); - const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`); - - // 跳過複雜的二進制輸入格式(需要真實檔案) - if (isComplexFormat(from)) { - report.skippedTests++; - continue; - } - - const content = createTestContent(from); - if (Buffer.isBuffer(content)) { - writeFileSync(inputPath, content); - } else { - writeFileSync(inputPath, content, "utf-8"); - } - - const module = await import(`../../src/converters/${converterName}`); - // 對於 Pandoc,正規化格式名稱 - const normalizedFrom = converterName === "pandoc" ? normalizeFormatForPandoc(from) : from; - const normalizedTo = converterName === "pandoc" ? normalizeFormatForPandoc(to) : to; - await module.convert(inputPath, normalizedFrom, normalizedTo, outputPath); - - const duration = Date.now() - startTime; - const success = existsSync(outputPath); - - report.results.push({ - converter: converterName, - from, - to, - success, - duration, - }); - - if (success) { - report.passedTests++; - } else { - report.failedTests++; - console.log(` ❌ ${from} → ${to}: 輸出檔案不存在`); - } - } catch (error) { - const duration = Date.now() - startTime; - report.failedTests++; - report.results.push({ - converter: converterName, - from, - to, - success: false, - duration, - error: String(error), - }); - console.log(` ❌ ${from} → ${to}: ${error}`); + test( + `測試格式轉換`, + async () => { + const converter = report.converters.find((c) => c.name === converterName); + if (!converter) { + report.skippedTests++; + console.log(` ⏭ ${converterName}: 轉換器未找到`); + return; } - } - }, TIMEOUT * sampled.length); + + if (!converter.available) { + report.skippedTests += converter.combinations.length; + console.log( + ` ⏭ ${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`, + ); + return; + } + + // 抽樣測試 + const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER); + console.log( + ` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`, + ); + + for (const [from, to] of sampled) { + report.testedCombinations++; + const startTime = Date.now(); + + try { + const inputDir = join(E2E_OUTPUT_DIR, converterName); + ensureDir(inputDir); + const inputPath = join(inputDir, `input.${from}`); + const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`); + + // 跳過複雜的二進制輸入格式(需要真實檔案) + if (isComplexFormat(from)) { + report.skippedTests++; + continue; + } + + const content = createTestContent(from); + if (Buffer.isBuffer(content)) { + writeFileSync(inputPath, content); + } else { + writeFileSync(inputPath, content, "utf-8"); + } + + const module = await import(`../../src/converters/${converterName}`); + // 對於 Pandoc,正規化格式名稱 + const normalizedFrom = + converterName === "pandoc" ? normalizeFormatForPandoc(from) : from; + const normalizedTo = converterName === "pandoc" ? normalizeFormatForPandoc(to) : to; + await module.convert(inputPath, normalizedFrom, normalizedTo, outputPath); + + const duration = Date.now() - startTime; + const success = existsSync(outputPath); + + report.results.push({ + converter: converterName, + from, + to, + success, + duration, + }); + + if (success) { + report.passedTests++; + } else { + report.failedTests++; + console.log(` ❌ ${from} → ${to}: 輸出檔案不存在`); + } + } catch (error) { + const duration = Date.now() - startTime; + report.failedTests++; + report.results.push({ + converter: converterName, + from, + to, + success: false, + duration, + error: String(error), + }); + console.log(` ❌ ${from} → ${to}: ${error}`); + } + } + }, + TIMEOUT * sampled.length, + ); }); } }); @@ -609,69 +645,77 @@ describe("📊 格式轉換矩陣 Format Conversion Matrix", () => { for (const converterName of dataConverters) { describe(`${converterName}`, () => { - test(`測試格式轉換`, async () => { - const converter = report.converters.find(c => c.name === converterName); - if (!converter) { - report.skippedTests++; - console.log(` ⏭ ${converterName}: 轉換器未找到`); - return; - } - - if (!converter.available) { - report.skippedTests += converter.combinations.length; - console.log(` ⏭ ${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`); - return; - } - - const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER); - console.log(` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`); - - for (const [from, to] of sampled) { - report.testedCombinations++; - const startTime = Date.now(); - - try { - const inputDir = join(E2E_OUTPUT_DIR, converterName); - ensureDir(inputDir); - const inputPath = join(inputDir, `input.${from}`); - const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`); - - const content = createTestContent(from); - writeFileSync(inputPath, content, "utf-8"); - - const module = await import(`../../src/converters/${converterName}`); - await module.convert(inputPath, from, to, outputPath); - - const duration = Date.now() - startTime; - const success = existsSync(outputPath); - - report.results.push({ - converter: converterName, - from, - to, - success, - duration, - }); - - if (success) { - report.passedTests++; - } else { - report.failedTests++; - } - } catch (error) { - const duration = Date.now() - startTime; - report.failedTests++; - report.results.push({ - converter: converterName, - from, - to, - success: false, - duration, - error: String(error), - }); + test( + `測試格式轉換`, + async () => { + const converter = report.converters.find((c) => c.name === converterName); + if (!converter) { + report.skippedTests++; + console.log(` ⏭ ${converterName}: 轉換器未找到`); + return; } - } - }, TIMEOUT * sampled.length); + + if (!converter.available) { + report.skippedTests += converter.combinations.length; + console.log( + ` ⏭ ${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`, + ); + return; + } + + const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER); + console.log( + ` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`, + ); + + for (const [from, to] of sampled) { + report.testedCombinations++; + const startTime = Date.now(); + + try { + const inputDir = join(E2E_OUTPUT_DIR, converterName); + ensureDir(inputDir); + const inputPath = join(inputDir, `input.${from}`); + const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`); + + const content = createTestContent(from); + writeFileSync(inputPath, content, "utf-8"); + + const module = await import(`../../src/converters/${converterName}`); + await module.convert(inputPath, from, to, outputPath); + + const duration = Date.now() - startTime; + const success = existsSync(outputPath); + + report.results.push({ + converter: converterName, + from, + to, + success, + duration, + }); + + if (success) { + report.passedTests++; + } else { + report.failedTests++; + } + } catch (error) { + const duration = Date.now() - startTime; + report.failedTests++; + report.results.push({ + converter: converterName, + from, + to, + success: false, + duration, + error: String(error), + }); + } + } + }, + TIMEOUT * sampled.length, + ); }); } }); @@ -693,6 +737,3 @@ function sampleCombinations( const shuffled = [...combinations].sort(() => Math.random() - 0.5); return shuffled.slice(0, maxCount); } - -// 為 sampled 變數提供全局作用域 -let sampled: [string, string][] = []; diff --git a/tests/e2e/translation.e2e.test.ts b/tests/e2e/translation.e2e.test.ts index 5770cd3..00f4784 100644 --- a/tests/e2e/translation.e2e.test.ts +++ b/tests/e2e/translation.e2e.test.ts @@ -1,24 +1,24 @@ /** * 多語言翻譯 E2E 測試 - * + * * 測試涵蓋: * - PDFMathTranslate (pdf2zh) * - BabelDOC * - 支援語言:中文(簡繁)、英文、日文、韓文、德文、法文等 - * + * * 注意:這些測試需要: * 1. 網路連接(使用翻譯 API) * 2. 設置翻譯服務 API 金鑰 * 3. PDF 測試檔案 - * + * * 執行方式: * bun test tests/e2e/translation.e2e.test.ts */ import { describe, test, expect, beforeAll, afterAll } from "bun:test"; -import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync, statSync } from "node:fs"; import { join } from "node:path"; -import { spawnSync, execSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; // ============================================================================= // 配置 @@ -41,7 +41,12 @@ interface Language { const SUPPORTED_LANGUAGES: Language[] = [ { code: "zh", name: "Simplified Chinese", nativeName: "简体中文", testPhrase: "这是一个测试" }, - { code: "zh-TW", name: "Traditional Chinese", nativeName: "繁體中文", testPhrase: "這是一個測試" }, + { + code: "zh-TW", + name: "Traditional Chinese", + nativeName: "繁體中文", + testPhrase: "這是一個測試", + }, { code: "en", name: "English", nativeName: "English", testPhrase: "This is a test" }, { code: "ja", name: "Japanese", nativeName: "日本語", testPhrase: "これはテストです" }, { code: "ko", name: "Korean", nativeName: "한국어", testPhrase: "이것은 테스트입니다" }, @@ -53,7 +58,12 @@ const SUPPORTED_LANGUAGES: Language[] = [ { code: "it", name: "Italian", nativeName: "Italiano", testPhrase: "Questo è un test" }, { code: "ar", name: "Arabic", nativeName: "العربية", testPhrase: "هذا اختبار" }, { code: "th", name: "Thai", nativeName: "ไทย", testPhrase: "นี่คือการทดสอบ" }, - { code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt", testPhrase: "Đây là một bài kiểm tra" }, + { + code: "vi", + name: "Vietnamese", + nativeName: "Tiếng Việt", + testPhrase: "Đây là một bài kiểm tra", + }, ]; // ============================================================================= @@ -133,15 +143,15 @@ This document is used for testing multilingual translation capabilities. try { // 嘗試使用 xelatex - const result = spawnSync("xelatex", [ - "-interaction=nonstopmode", - "-output-directory=" + join(path, ".."), - texPath, - ], { - timeout: 60000, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }); + const result = spawnSync( + "xelatex", + ["-interaction=nonstopmode", "-output-directory=" + join(path, ".."), texPath], + { + timeout: 60000, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }, + ); return result.status === 0 && existsSync(path); } catch { @@ -192,9 +202,9 @@ let testPdfPath: string; beforeAll(async () => { console.log("\n🔧 初始化翻譯測試... Initializing translation tests...\n"); - + ensureDir(E2E_OUTPUT_DIR); - + // 檢測翻譯工具 console.log(" 檢測翻譯工具..."); translators = { @@ -204,7 +214,9 @@ beforeAll(async () => { for (const [name, status] of Object.entries(translators)) { const icon = status.available ? "✅" : "❌"; - console.log(` ${icon} ${name}: ${status.available ? status.version || "available" : "not found"}`); + console.log( + ` ${icon} ${name}: ${status.available ? status.version || "available" : "not found"}`, + ); } // 檢查或創建測試 PDF @@ -230,16 +242,25 @@ afterAll(() => { 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 || 1)) * 100).toFixed(1)}%`); + console.log( + `成功率 Success Rate: ${((stats.passed / (stats.total - stats.skipped || 1)) * 100).toFixed(1)}%`, + ); console.log("=".repeat(70)); // 生成報告 const reportPath = join(E2E_OUTPUT_DIR, "translation-report.json"); - writeFileSync(reportPath, JSON.stringify({ - generatedAt: new Date().toISOString(), - translators, - stats, - }, null, 2)); + writeFileSync( + reportPath, + JSON.stringify( + { + generatedAt: new Date().toISOString(), + translators, + stats, + }, + null, + 2, + ), + ); console.log(`📁 報告已保存: ${reportPath}`); }); @@ -250,86 +271,92 @@ afterAll(() => { describe("📚 PDFMathTranslate (pdf2zh)", () => { // 主要語言測試 describe("主要語言翻譯 Primary Languages", () => { - const primaryLanguages = SUPPORTED_LANGUAGES.filter(l => - ["zh", "en", "ja", "ko"].includes(l.code) + const primaryLanguages = SUPPORTED_LANGUAGES.filter((l) => + ["zh", "en", "ja", "ko"].includes(l.code), ); for (const lang of primaryLanguages) { - test(`英文 → ${lang.nativeName} (${lang.code})`, async () => { - stats.total++; + test( + `英文 → ${lang.nativeName} (${lang.code})`, + async () => { + stats.total++; - if (!translators.pdf2zh?.available) { - stats.skipped++; - console.log(` ⏭ 跳過: pdf2zh 不可用`); - return; - } - - if (!existsSync(testPdfPath)) { - stats.skipped++; - console.log(` ⏭ 跳過: 測試 PDF 不存在`); - return; - } - - const outputPath = join(E2E_OUTPUT_DIR, `pdf2zh_en_to_${lang.code}.tar`); - const startTime = Date.now(); - const inputSize = statSync(testPdfPath).size; - - try { - // 動態導入轉換器 - const module = await import("../../src/converters/pdfmathtranslate"); - - // 設置目標語言(透過環境變數或參數) - process.env.PDF2ZH_TARGET_LANG = lang.code; - - await module.convert(testPdfPath, "pdf", "tar", outputPath); - - const duration = Date.now() - startTime; - const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0; - const success = outputSize > 0; - - stats.results.push({ - translator: "pdf2zh", - sourceLang: "en", - targetLang: lang.code, - success, - duration, - inputSize, - outputSize, - }); - - if (success) { - stats.passed++; - console.log(` ✓ en → ${lang.code}: ${outputSize} bytes (${(duration / 1000).toFixed(1)}s)`); - } else { - stats.failed++; - console.log(` ✗ en → ${lang.code}: 輸出為空`); + if (!translators.pdf2zh?.available) { + stats.skipped++; + console.log(` ⏭ 跳過: pdf2zh 不可用`); + return; } - expect(success).toBe(true); - } catch (error) { - const duration = Date.now() - startTime; - stats.failed++; - stats.results.push({ - translator: "pdf2zh", - sourceLang: "en", - targetLang: lang.code, - success: false, - duration, - inputSize, - outputSize: 0, - error: String(error), - }); - console.log(` ✗ en → ${lang.code}: ${error}`); - throw error; - } - }, TIMEOUT); + if (!existsSync(testPdfPath)) { + stats.skipped++; + console.log(` ⏭ 跳過: 測試 PDF 不存在`); + return; + } + + const outputPath = join(E2E_OUTPUT_DIR, `pdf2zh_en_to_${lang.code}.tar`); + const startTime = Date.now(); + const inputSize = statSync(testPdfPath).size; + + try { + // 動態導入轉換器 + const module = await import("../../src/converters/pdfmathtranslate"); + + // 設置目標語言(透過環境變數或參數) + process.env.PDF2ZH_TARGET_LANG = lang.code; + + await module.convert(testPdfPath, "pdf", "tar", outputPath); + + const duration = Date.now() - startTime; + const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0; + const success = outputSize > 0; + + stats.results.push({ + translator: "pdf2zh", + sourceLang: "en", + targetLang: lang.code, + success, + duration, + inputSize, + outputSize, + }); + + if (success) { + stats.passed++; + console.log( + ` ✓ en → ${lang.code}: ${outputSize} bytes (${(duration / 1000).toFixed(1)}s)`, + ); + } else { + stats.failed++; + console.log(` ✗ en → ${lang.code}: 輸出為空`); + } + + expect(success).toBe(true); + } catch (error) { + const duration = Date.now() - startTime; + stats.failed++; + stats.results.push({ + translator: "pdf2zh", + sourceLang: "en", + targetLang: lang.code, + success: false, + duration, + inputSize, + outputSize: 0, + error: String(error), + }); + console.log(` ✗ en → ${lang.code}: ${error}`); + throw error; + } + }, + TIMEOUT, + ); } }); // 次要語言測試(可選) describe("次要語言翻譯 Secondary Languages", () => { - const secondaryLanguages = SUPPORTED_LANGUAGES.filter(l => - ["de", "fr", "es", "ru"].includes(l.code) + const secondaryLanguages = SUPPORTED_LANGUAGES.filter((l) => + ["de", "fr", "es", "ru"].includes(l.code), ); for (const lang of secondaryLanguages) { @@ -349,78 +376,82 @@ describe("📚 PDFMathTranslate (pdf2zh)", () => { describe("🌐 BabelDOC", () => { describe("PDF 翻譯", () => { - const testLanguages = SUPPORTED_LANGUAGES.filter(l => - ["zh", "ja"].includes(l.code) - ); + const testLanguages = SUPPORTED_LANGUAGES.filter((l) => ["zh", "ja"].includes(l.code)); for (const lang of testLanguages) { - test(`英文 → ${lang.nativeName} (${lang.code})`, async () => { - stats.total++; + test( + `英文 → ${lang.nativeName} (${lang.code})`, + async () => { + stats.total++; - if (!translators.babeldoc?.available) { - stats.skipped++; - console.log(` ⏭ 跳過: babeldoc 不可用`); - return; - } - - if (!existsSync(testPdfPath)) { - stats.skipped++; - console.log(` ⏭ 跳過: 測試 PDF 不存在`); - return; - } - - const outputPath = join(E2E_OUTPUT_DIR, `babeldoc_en_to_${lang.code}.tar`); - const startTime = Date.now(); - const inputSize = statSync(testPdfPath).size; - - try { - const module = await import("../../src/converters/babeldoc"); - - // 設置目標語言 - process.env.BABELDOC_TARGET_LANG = lang.code; - - await module.convert(testPdfPath, "pdf", "tar", outputPath); - - const duration = Date.now() - startTime; - const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0; - const success = outputSize > 0; - - stats.results.push({ - translator: "babeldoc", - sourceLang: "en", - targetLang: lang.code, - success, - duration, - inputSize, - outputSize, - }); - - if (success) { - stats.passed++; - console.log(` ✓ en → ${lang.code}: ${outputSize} bytes (${(duration / 1000).toFixed(1)}s)`); - } else { - stats.failed++; - console.log(` ✗ en → ${lang.code}: 輸出為空`); + if (!translators.babeldoc?.available) { + stats.skipped++; + console.log(` ⏭ 跳過: babeldoc 不可用`); + return; } - expect(success).toBe(true); - } catch (error) { - const duration = Date.now() - startTime; - stats.failed++; - stats.results.push({ - translator: "babeldoc", - sourceLang: "en", - targetLang: lang.code, - success: false, - duration, - inputSize, - outputSize: 0, - error: String(error), - }); - console.log(` ✗ en → ${lang.code}: ${error}`); - throw error; - } - }, TIMEOUT); + if (!existsSync(testPdfPath)) { + stats.skipped++; + console.log(` ⏭ 跳過: 測試 PDF 不存在`); + return; + } + + const outputPath = join(E2E_OUTPUT_DIR, `babeldoc_en_to_${lang.code}.tar`); + const startTime = Date.now(); + const inputSize = statSync(testPdfPath).size; + + try { + const module = await import("../../src/converters/babeldoc"); + + // 設置目標語言 + process.env.BABELDOC_TARGET_LANG = lang.code; + + await module.convert(testPdfPath, "pdf", "tar", outputPath); + + const duration = Date.now() - startTime; + const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0; + const success = outputSize > 0; + + stats.results.push({ + translator: "babeldoc", + sourceLang: "en", + targetLang: lang.code, + success, + duration, + inputSize, + outputSize, + }); + + if (success) { + stats.passed++; + console.log( + ` ✓ en → ${lang.code}: ${outputSize} bytes (${(duration / 1000).toFixed(1)}s)`, + ); + } else { + stats.failed++; + console.log(` ✗ en → ${lang.code}: 輸出為空`); + } + + expect(success).toBe(true); + } catch (error) { + const duration = Date.now() - startTime; + stats.failed++; + stats.results.push({ + translator: "babeldoc", + sourceLang: "en", + targetLang: lang.code, + success: false, + duration, + inputSize, + outputSize: 0, + error: String(error), + }); + console.log(` ✗ en → ${lang.code}: ${error}`); + throw error; + } + }, + TIMEOUT, + ); } }); }); @@ -434,18 +465,27 @@ describe("🔤 語言矩陣測試 Language Matrix", () => { console.log("\n📋 支援的語言 Supported Languages:\n"); console.log("| 代碼 | 名稱 | 本地名稱 | 測試短語 |"); console.log("|------|------|----------|----------|"); - + for (const lang of SUPPORTED_LANGUAGES) { - console.log(`| ${lang.code.padEnd(6)} | ${lang.name.padEnd(20)} | ${lang.nativeName} | ${lang.testPhrase} |`); + console.log( + `| ${lang.code.padEnd(6)} | ${lang.name.padEnd(20)} | ${lang.nativeName} | ${lang.testPhrase} |`, + ); } // 生成語言矩陣報告 const matrixPath = join(E2E_OUTPUT_DIR, "language-matrix.json"); - writeFileSync(matrixPath, JSON.stringify({ - languages: SUPPORTED_LANGUAGES, - translators: Object.keys(translators), - generatedAt: new Date().toISOString(), - }, null, 2)); + writeFileSync( + matrixPath, + JSON.stringify( + { + languages: SUPPORTED_LANGUAGES, + translators: Object.keys(translators), + generatedAt: new Date().toISOString(), + }, + null, + 2, + ), + ); console.log(`\n📁 語言矩陣已保存: ${matrixPath}`); expect(SUPPORTED_LANGUAGES.length).toBeGreaterThan(0);