fix: 修復 lint 錯誤和代碼格式化

- 移除未使用的導入 (readdirSync, basename, readFileSync, execSync)
- 將不需要重新賦值的 let 改為 const
- 修復 switch case 區塊中的詞法聲明問題
- 移除未使用的 SAMPLING_RATE 和 sampled 變量
- 移除未使用的 _timeout 參數
- 運行 Prettier 格式化所有文件
This commit is contained in:
Your Name 2026-01-23 12:27:22 +08:00
parent e327345ad7
commit a2512bf29b
6 changed files with 886 additions and 712 deletions

View file

@ -132,15 +132,15 @@ jobs:
- name: Run API health check - name: Run API health check
run: | run: |
echo "🔍 Testing API endpoints..." echo "🔍 Testing API endpoints..."
# Health check # Health check
curl -f http://localhost:3000/healthcheck curl -f http://localhost:3000/healthcheck
echo "" echo ""
# Check if main page loads # Check if main page loads
curl -f -o /dev/null -w "%{http_code}" http://localhost:3000/ curl -f -o /dev/null -w "%{http_code}" http://localhost:3000/
echo "" echo ""
echo "✅ API health checks passed!" echo "✅ API health checks passed!"
# ============================================ # ============================================
@ -175,7 +175,7 @@ jobs:
- name: Run E2E tests inside Docker container - name: Run E2E tests inside Docker container
run: | run: |
echo "🧪 Running comprehensive E2E tests inside Docker..." echo "🧪 Running comprehensive E2E tests inside Docker..."
docker run --rm \ docker run --rm \
--name convertx-e2e-test \ --name convertx-e2e-test \
-v "${{ github.workspace }}/tests:/app/tests" \ -v "${{ github.workspace }}/tests:/app/tests" \
@ -269,7 +269,7 @@ jobs:
DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }} DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }}
run: | run: |
echo "🌍 Running translation tests..." echo "🌍 Running translation tests..."
docker run --rm \ docker run --rm \
--name convertx-translation-test \ --name convertx-translation-test \
-v "${{ github.workspace }}/tests:/app/tests" \ -v "${{ github.workspace }}/tests:/app/tests" \
@ -320,7 +320,7 @@ jobs:
script: | script: |
const tag = `test-${{ github.sha }}`; const tag = `test-${{ github.sha }}`;
console.log(`🗑️ Attempting to delete test image with tag: ${tag}`); console.log(`🗑️ Attempting to delete test image with tag: ${tag}`);
// Note: This requires ghcr.io API access which may need additional setup // Note: This requires ghcr.io API access which may need additional setup
// For now, images will be cleaned up by retention policies // For now, images will be cleaned up by retention policies
console.log('Test 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 "| E2E Tests | ${{ needs.e2e-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Comprehensive E2E | ${{ needs.comprehensive-e2e.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Comprehensive E2E | ${{ needs.comprehensive-e2e.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "test-results/format-matrix/matrix-summary.md" ]; then if [ -f "test-results/format-matrix/matrix-summary.md" ]; then
echo "## Format Matrix Summary" >> $GITHUB_STEP_SUMMARY echo "## Format Matrix Summary" >> $GITHUB_STEP_SUMMARY
cat test-results/format-matrix/matrix-summary.md >> $GITHUB_STEP_SUMMARY cat test-results/format-matrix/matrix-summary.md >> $GITHUB_STEP_SUMMARY
fi fi
if [ -f "test-results/comprehensive/conversion-matrix.json" ]; then if [ -f "test-results/comprehensive/conversion-matrix.json" ]; then
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "## Conversion Matrix" >> $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 head -50 test-results/comprehensive/conversion-matrix.json >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
fi fi
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "📅 Test run completed at: $(date -u)" >> $GITHUB_STEP_SUMMARY echo "📅 Test run completed at: $(date -u)" >> $GITHUB_STEP_SUMMARY

View file

@ -51,4 +51,3 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "Basic tests completed. For comprehensive E2E tests with all converters," >> $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 echo "see the [Docker E2E Tests](${{ github.server_url }}/${{ github.repository }}/actions/workflows/docker-e2e-tests.yml) workflow." >> $GITHUB_STEP_SUMMARY

View file

@ -120,7 +120,7 @@ function checkModelsExist(): boolean {
console.warn(`[PDFMathTranslate] Models should be pre-downloaded during Docker build.`); console.warn(`[PDFMathTranslate] Models should be pre-downloaded during Docker build.`);
return false; return false;
} }
// 檢查特定的 ONNX 模型檔案 // 檢查特定的 ONNX 模型檔案
const onnxModelPath = join(MODELS_PATH, "doclayout_yolo_docstructbench_imgsz1024.onnx"); const onnxModelPath = join(MODELS_PATH, "doclayout_yolo_docstructbench_imgsz1024.onnx");
if (!existsSync(onnxModelPath)) { if (!existsSync(onnxModelPath)) {
@ -128,7 +128,7 @@ function checkModelsExist(): boolean {
console.warn(`[PDFMathTranslate] Model should be pre-downloaded during Docker build.`); console.warn(`[PDFMathTranslate] Model should be pre-downloaded during Docker build.`);
return false; return false;
} }
return true; return true;
} }

View file

@ -15,8 +15,8 @@
*/ */
import { describe, test, expect, beforeAll, afterAll } from "bun:test"; import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { existsSync, statSync, mkdirSync, writeFileSync, readdirSync } from "node:fs"; import { existsSync, statSync, mkdirSync, writeFileSync } from "node:fs";
import { join, basename } from "node:path"; import { join } from "node:path";
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
// ============================================================================= // =============================================================================
@ -28,9 +28,9 @@ const E2E_FIXTURES_DIR = "tests/e2e/fixtures";
// 測試超時(毫秒) // 測試超時(毫秒)
const TIMEOUT = { const TIMEOUT = {
fast: 30_000, // 快速轉換 fast: 30_000, // 快速轉換
medium: 60_000, // 中等複雜度 medium: 60_000, // 中等複雜度
slow: 180_000, // 複雜轉換 slow: 180_000, // 複雜轉換
translation: 300_000, // 翻譯(需網路) translation: 300_000, // 翻譯(需網路)
}; };
@ -97,25 +97,24 @@ function ensureDir(dir: string): void {
} }
function createTestSvg(path: string): void { function createTestSvg(path: string): void {
writeFileSync(path, `<?xml version="1.0" encoding="UTF-8"?> writeFileSync(
path,
`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"> <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<rect x="10" y="10" width="180" height="180" fill="#4285f4" rx="20"/> <rect x="10" y="10" width="180" height="180" fill="#4285f4" rx="20"/>
<circle cx="100" cy="100" r="50" fill="#ffffff"/> <circle cx="100" cy="100" r="50" fill="#ffffff"/>
<text x="100" y="110" text-anchor="middle" font-size="24" fill="#333"></text> <text x="100" y="110" text-anchor="middle" font-size="24" fill="#333"></text>
</svg>`); </svg>`,
);
} }
function createTestPng(path: string): void { function createTestPng(path: string): void {
// 最小有效 PNG (1x1 紅色像素) // 最小有效 PNG (1x1 紅色像素)
const png = Buffer.from([ const png = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xfe, 0xd4, 0xef, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
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, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]); ]);
writeFileSync(path, png); writeFileSync(path, png);
@ -142,7 +141,9 @@ function createTestBmp(path: string): void {
} }
function createTestMarkdown(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 \`\`\`javascript
console.log("Hello, 世界!"); console.log("Hello, 世界!");
\`\`\` \`\`\`
`); `,
);
} }
function createTestJson(path: string): void { function createTestJson(path: string): void {
writeFileSync(path, JSON.stringify({ writeFileSync(
name: "測試", path,
version: "1.0.0", JSON.stringify(
languages: ["zh-TW", "zh-CN", "en", "ja", "ko"], {
config: { name: "測試",
enabled: true, version: "1.0.0",
count: 42, languages: ["zh-TW", "zh-CN", "en", "ja", "ko"],
}, config: {
}, null, 2)); enabled: true,
count: 42,
},
},
null,
2,
),
);
} }
function createTestHtml(path: string): void { function createTestHtml(path: string): void {
writeFileSync(path, `<!DOCTYPE html> writeFileSync(
path,
`<!DOCTYPE html>
<html lang="zh-TW"> <html lang="zh-TW">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -203,11 +214,14 @@ function createTestHtml(path: string): void {
<li></li> <li></li>
</ul> </ul>
</body> </body>
</html>`); </html>`,
);
} }
function createTestTxt(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. This is a plain text test file.
@ -220,16 +234,20 @@ This is a plain text test file.
- -
- Deutsch - Deutsch
- Français - Français
`); `,
);
} }
function createTestCsv(path: string): void { function createTestCsv(path: string): void {
writeFileSync(path, `name,value,description writeFileSync(
path,
`name,value,description
1,100, 1,100,
2,200, 2,200,
test3,300,This is the third item test3,300,This is the third item
4,400,4 4,400,4
`); `,
);
} }
// ============================================================================= // =============================================================================
@ -250,7 +268,6 @@ async function runConversion(
converter: string, converter: string,
inputPath: string, inputPath: string,
outputPath: string, outputPath: string,
timeout = TIMEOUT.medium,
): Promise<ConversionResult> { ): Promise<ConversionResult> {
const startTime = Date.now(); const startTime = Date.now();
const inputSize = existsSync(inputPath) ? statSync(inputPath).size : 0; const inputSize = existsSync(inputPath) ? statSync(inputPath).size : 0;
@ -315,7 +332,9 @@ function printSummary(): void {
console.log(`✅ 通過 Passed: ${stats.passed}`); console.log(`✅ 通過 Passed: ${stats.passed}`);
console.log(`❌ 失敗 Failed: ${stats.failed}`); console.log(`❌ 失敗 Failed: ${stats.failed}`);
console.log(`⏭ 跳過 Skipped: ${stats.skipped}`); 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)); console.log("=".repeat(60));
} }
@ -323,18 +342,20 @@ function printSummary(): void {
// 測試套件 // 測試套件
// ============================================================================= // =============================================================================
let availableTools: Record<string, boolean> = {}; const availableTools: Record<string, boolean> = {};
beforeAll(() => { beforeAll(() => {
console.log("\n🔧 檢測可用工具 Detecting available tools...\n"); console.log("\n🔧 檢測可用工具 Detecting available tools...\n");
for (const [name, check] of Object.entries(TOOLS)) { for (const [name, check] of Object.entries(TOOLS)) {
const status = check(); const status = check();
availableTools[name] = status.available; availableTools[name] = status.available;
const icon = 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"); console.log("\n");
ensureDir(E2E_OUTPUT_DIR); ensureDir(E2E_OUTPUT_DIR);
}); });
@ -365,29 +386,35 @@ describe("🖼️ 圖像格式轉換 Image Conversions", () => {
}); });
for (const format of formats) { for (const format of formats) {
test(`SVG → ${format.toUpperCase()}`, async () => { test(
if (!availableTools.inkscape || !availableTools.xvfbRun) { `SVG → ${format.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.inkscape || !availableTools.xvfbRun) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: xvfb-run or inkscape not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: xvfb-run or inkscape not available`); const outputPath = join(outputDir, `inkscape_output.${format}`);
return; const result = await runConversion("inkscape", inputPath, outputPath);
} stats.results.push(result);
stats.total++; if (result.success) {
const outputPath = join(outputDir, `inkscape_output.${format}`); stats.passed++;
const result = await runConversion("inkscape", inputPath, outputPath); console.log(
stats.results.push(result); ` ✓ SVG → ${format.toUpperCase()}: ${result.outputSize} bytes (${result.duration}ms)`,
);
} else {
stats.failed++;
console.log(` ✗ SVG → ${format.toUpperCase()}: ${result.error}`);
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(` ✓ SVG → ${format.toUpperCase()}: ${result.outputSize} bytes (${result.duration}ms)`); TIMEOUT.medium,
} else { );
stats.failed++;
console.log(` ✗ SVG → ${format.toUpperCase()}: ${result.error}`);
}
expect(result.success).toBe(true);
}, TIMEOUT.medium);
} }
}); });
@ -403,33 +430,39 @@ describe("🖼️ 圖像格式轉換 Image Conversions", () => {
]; ];
for (const [from, to] of conversions) { for (const [from, to] of conversions) {
test(`${from.toUpperCase()}${to.toUpperCase()}`, async () => { test(
if (!availableTools.imagemagick) { `${from.toUpperCase()}${to.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.imagemagick) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: ImageMagick not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: ImageMagick not available`); const inputPath = join(outputDir, `imagemagick_input.${from}`);
return; const outputPath = join(outputDir, `imagemagick_${from}_to_${to}.${to}`);
}
stats.total++; if (from === "png") createTestPng(inputPath);
const inputPath = join(outputDir, `imagemagick_input.${from}`); else if (from === "bmp") createTestBmp(inputPath);
const outputPath = join(outputDir, `imagemagick_${from}_to_${to}.${to}`);
if (from === "png") createTestPng(inputPath); const result = await runConversion("imagemagick", inputPath, outputPath);
else if (from === "bmp") createTestBmp(inputPath); stats.results.push(result);
const result = await runConversion("imagemagick", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(
`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`); TIMEOUT.fast,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.fast);
} }
}); });
@ -438,31 +471,35 @@ describe("🖼️ 圖像格式轉換 Image Conversions", () => {
const formats = ["svg", "eps", "pdf"]; const formats = ["svg", "eps", "pdf"];
for (const format of formats) { for (const format of formats) {
test(`BMP → ${format.toUpperCase()}`, async () => { test(
if (!availableTools.potrace) { `BMP → ${format.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.potrace) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Potrace not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: Potrace not available`); const inputPath = join(outputDir, "potrace_input.bmp");
return; const outputPath = join(outputDir, `potrace_output.${format}`);
} createTestBmp(inputPath);
stats.total++; const result = await runConversion("potrace", inputPath, outputPath);
const inputPath = join(outputDir, "potrace_input.bmp"); stats.results.push(result);
const outputPath = join(outputDir, `potrace_output.${format}`);
createTestBmp(inputPath);
const result = await runConversion("potrace", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(` ✓ BMP → ${format.toUpperCase()}: ${result.outputSize} bytes`);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(` ✓ BMP → ${format.toUpperCase()}: ${result.outputSize} bytes`); TIMEOUT.fast,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.fast);
} }
}); });
}); });
@ -488,28 +525,32 @@ describe("📄 文件格式轉換 Document Conversions", () => {
}); });
for (const format of formats) { for (const format of formats) {
test(`Markdown → ${format.toUpperCase()}`, async () => { test(
if (!availableTools.pandoc) { `Markdown → ${format.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.pandoc) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Pandoc not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: Pandoc not available`); const outputPath = join(outputDir, `pandoc_output.${format}`);
return; const result = await runConversion("pandoc", inputPath, outputPath);
} stats.results.push(result);
stats.total++; if (result.success) {
const outputPath = join(outputDir, `pandoc_output.${format}`); stats.passed++;
const result = await runConversion("pandoc", inputPath, outputPath); console.log(` ✓ Markdown → ${format.toUpperCase()}: ${result.outputSize} bytes`);
stats.results.push(result); } else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(` ✓ Markdown → ${format.toUpperCase()}: ${result.outputSize} bytes`); TIMEOUT.medium,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.medium);
} }
}); });
@ -522,33 +563,39 @@ describe("📄 文件格式轉換 Document Conversions", () => {
]; ];
for (const [from, to] of conversions) { for (const [from, to] of conversions) {
test(`${from.toUpperCase()}${to.toUpperCase()}`, async () => { test(
if (!availableTools.libreoffice) { `${from.toUpperCase()}${to.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.libreoffice) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: LibreOffice not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: LibreOffice not available`); const inputPath = join(outputDir, `libreoffice_input.${from}`);
return; const outputPath = join(outputDir, `libreoffice_${from}_to_${to}.${to}`);
}
stats.total++; if (from === "html") createTestHtml(inputPath);
const inputPath = join(outputDir, `libreoffice_input.${from}`); else if (from === "txt") createTestTxt(inputPath);
const outputPath = join(outputDir, `libreoffice_${from}_to_${to}.${to}`);
if (from === "html") createTestHtml(inputPath); const result = await runConversion("libreoffice", inputPath, outputPath);
else if (from === "txt") createTestTxt(inputPath); stats.results.push(result);
const result = await runConversion("libreoffice", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(
`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`); TIMEOUT.slow,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.slow);
} }
}); });
}); });
@ -575,37 +622,43 @@ describe("📊 資料格式轉換 Data Format Conversions", () => {
]; ];
for (const [from, to] of conversions) { for (const [from, to] of conversions) {
test(`${from.toUpperCase()}${to.toUpperCase()}`, async () => { test(
if (!availableTools.dasel) { `${from.toUpperCase()}${to.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.dasel) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Dasel not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: Dasel not available`); const inputPath = join(outputDir, `dasel_input.${from}`);
return; const outputPath = join(outputDir, `dasel_${from}_to_${to}.${to}`);
}
stats.total++; if (from === "json") createTestJson(inputPath);
const inputPath = join(outputDir, `dasel_input.${from}`); else if (from === "yaml") {
const outputPath = join(outputDir, `dasel_${from}_to_${to}.${to}`); writeFileSync(inputPath, "name: test\nvalue: 42\n");
} else if (from === "csv") {
createTestCsv(inputPath);
}
if (from === "json") createTestJson(inputPath); const result = await runConversion("dasel", inputPath, outputPath);
else if (from === "yaml") { stats.results.push(result);
writeFileSync(inputPath, "name: test\nvalue: 42\n");
} else if (from === "csv") {
createTestCsv(inputPath);
}
const result = await runConversion("dasel", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(
`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`); TIMEOUT.fast,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.fast);
} }
}); });
}); });
@ -636,75 +689,87 @@ describe("🌍 多語言翻譯 Multilingual Translation", () => {
// PDFMathTranslate 測試 // PDFMathTranslate 測試
describe("PDFMathTranslate (PDF 翻譯)", () => { describe("PDFMathTranslate (PDF 翻譯)", () => {
// 注意:這些測試需要網路連接和 PDF 測試檔案 // 注意:這些測試需要網路連接和 PDF 測試檔案
for (const lang of LANGUAGES.slice(0, 4)) { // 只測試前 4 種語言 for (const lang of LANGUAGES.slice(0, 4)) {
test.skip(`PDF → ${lang.name} (${lang.code})`, async () => { // 只測試前 4 種語言
if (!availableTools.pdf2zh) { test.skip(
stats.skipped++; `PDF → ${lang.name} (${lang.code})`,
async () => {
if (!availableTools.pdf2zh) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: pdf2zh not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: pdf2zh not available`); // 這裡需要一個真實的 PDF 測試檔案
return; const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf");
} const outputPath = join(outputDir, `translated_${lang.code}.tar`);
stats.total++; if (!existsSync(inputPath)) {
// 這裡需要一個真實的 PDF 測試檔案 stats.skipped++;
const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf"); console.log(`⏭ Skipping: sample.pdf not found in fixtures`);
const outputPath = join(outputDir, `translated_${lang.code}.tar`); return;
}
if (!existsSync(inputPath)) { const result = await runConversion("pdfmathtranslate", inputPath, outputPath);
stats.skipped++; stats.results.push(result);
console.log(`⏭ Skipping: sample.pdf not found in fixtures`);
return;
}
const result = await runConversion("pdfmathtranslate", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(
` ✓ PDF → ${lang.name}: ${result.outputSize} bytes (${result.duration}ms)`,
);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(` ✓ PDF → ${lang.name}: ${result.outputSize} bytes (${result.duration}ms)`); TIMEOUT.translation,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.translation);
} }
}); });
// BabelDOC 測試 // BabelDOC 測試
describe("BabelDOC (進階 PDF 翻譯)", () => { describe("BabelDOC (進階 PDF 翻譯)", () => {
for (const lang of LANGUAGES.slice(0, 2)) { // 只測試中英 for (const lang of LANGUAGES.slice(0, 2)) {
test.skip(`PDF → ${lang.name} (babeldoc)`, async () => { // 只測試中英
if (!availableTools.babeldoc) { test.skip(
stats.skipped++; `PDF → ${lang.name} (babeldoc)`,
async () => {
if (!availableTools.babeldoc) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: babeldoc not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: babeldoc not available`); const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf");
return; const outputPath = join(outputDir, `babeldoc_${lang.code}.tar`);
}
stats.total++; if (!existsSync(inputPath)) {
const inputPath = join(E2E_FIXTURES_DIR, "sample.pdf"); stats.skipped++;
const outputPath = join(outputDir, `babeldoc_${lang.code}.tar`); console.log(`⏭ Skipping: sample.pdf not found in fixtures`);
return;
}
if (!existsSync(inputPath)) { const result = await runConversion("babeldoc", inputPath, outputPath);
stats.skipped++; stats.results.push(result);
console.log(`⏭ Skipping: sample.pdf not found in fixtures`);
return;
}
const result = await runConversion("babeldoc", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(` ✓ PDF → ${lang.name} (babeldoc): ${result.outputSize} bytes`);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(` ✓ PDF → ${lang.name} (babeldoc): ${result.outputSize} bytes`); TIMEOUT.translation,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.translation);
} }
}); });
}); });
@ -731,33 +796,39 @@ describe("📚 電子書格式轉換 Ebook Conversions", () => {
]; ];
for (const [from, to] of conversions) { for (const [from, to] of conversions) {
test(`${from.toUpperCase()}${to.toUpperCase()}`, async () => { test(
if (!availableTools.calibre || !availableTools.xvfbRun) { `${from.toUpperCase()}${to.toUpperCase()}`,
stats.skipped++; async () => {
if (!availableTools.calibre || !availableTools.xvfbRun) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: Calibre or xvfb-run not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: Calibre or xvfb-run not available`); const inputPath = join(outputDir, `calibre_input.${from}`);
return; const outputPath = join(outputDir, `calibre_${from}_to_${to}.${to}`);
}
stats.total++; if (from === "html") createTestHtml(inputPath);
const inputPath = join(outputDir, `calibre_input.${from}`); else if (from === "txt") createTestTxt(inputPath);
const outputPath = join(outputDir, `calibre_${from}_to_${to}.${to}`);
if (from === "html") createTestHtml(inputPath); const result = await runConversion("calibre", inputPath, outputPath);
else if (from === "txt") createTestTxt(inputPath); stats.results.push(result);
const result = await runConversion("calibre", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(
`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`,
);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(`${from.toUpperCase()}${to.toUpperCase()}: ${result.outputSize} bytes`); TIMEOUT.slow,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.slow);
} }
}); });
}); });
@ -777,29 +848,33 @@ describe("🎬 音視頻格式轉換 Media Conversions", () => {
describe("FFmpeg (音視頻格式)", () => { describe("FFmpeg (音視頻格式)", () => {
// 注意:這些測試需要實際的音視頻測試檔案 // 注意:這些測試需要實際的音視頻測試檔案
test.skip("MP3 → WAV", async () => { test.skip(
if (!availableTools.ffmpeg) { "MP3 → WAV",
stats.skipped++; async () => {
if (!availableTools.ffmpeg) {
stats.skipped++;
stats.total++;
console.log(`⏭ Skipping: FFmpeg not available`);
return;
}
stats.total++; stats.total++;
console.log(`⏭ Skipping: FFmpeg not available`); const inputPath = join(E2E_FIXTURES_DIR, "sample.mp3");
return; const outputPath = join(outputDir, "ffmpeg_output.wav");
}
stats.total++; if (!existsSync(inputPath)) {
const inputPath = join(E2E_FIXTURES_DIR, "sample.mp3"); stats.skipped++;
const outputPath = join(outputDir, "ffmpeg_output.wav"); console.log(`⏭ Skipping: sample.mp3 not found`);
return;
}
if (!existsSync(inputPath)) { const result = await runConversion("ffmpeg", inputPath, outputPath);
stats.skipped++; stats.results.push(result);
console.log(`⏭ Skipping: sample.mp3 not found`);
return;
}
const result = await runConversion("ffmpeg", inputPath, outputPath); expect(result.success).toBe(true);
stats.results.push(result); },
TIMEOUT.medium,
expect(result.success).toBe(true); );
}, TIMEOUT.medium);
}); });
}); });
@ -812,11 +887,18 @@ describe("🔢 格式轉換矩陣 Conversion Matrix", () => {
* *
* *
*/ */
test("生成格式轉換矩陣報告", async () => { test("生成格式轉換矩陣報告", async () => {
const converters = [ const converters = [
"inkscape", "imagemagick", "graphicsmagick", "pandoc", "inkscape",
"dasel", "potrace", "vtracer", "libreoffice", "calibre", "imagemagick",
"graphicsmagick",
"pandoc",
"dasel",
"potrace",
"vtracer",
"libreoffice",
"calibre",
]; ];
const matrix: Record<string, { from: string[]; to: string[] }> = {}; const matrix: Record<string, { from: string[]; to: string[] }> = {};
@ -862,8 +944,12 @@ describe("🔢 格式轉換矩陣 Conversion Matrix", () => {
totalCombinations += combinations; totalCombinations += combinations;
console.log(`${converter}:`); console.log(`${converter}:`);
console.log(` 輸入格式: ${formats.from.length} (${formats.from.slice(0, 5).join(", ")}${formats.from.length > 5 ? "..." : ""})`); console.log(
console.log(` 輸出格式: ${formats.to.length} (${formats.to.slice(0, 5).join(", ")}${formats.to.length > 5 ? "..." : ""})`); ` 輸入格式: ${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(` 組合數: ${combinations}`);
console.log(); console.log();
} }
@ -944,35 +1030,40 @@ describe("⚠️ 邊界條件測試 Edge Cases", () => {
expect(result.success).toBe(true); expect(result.success).toBe(true);
}); });
test("超長內容處理", async () => { test(
if (!availableTools.pandoc) { "超長內容處理",
stats.skipped++; async () => {
if (!availableTools.pandoc) {
stats.skipped++;
stats.total++;
return;
}
stats.total++; stats.total++;
return; const inputPath = join(outputDir, "long_content.markdown");
} const outputPath = join(outputDir, "long_output.html");
stats.total++; // 生成 10000 行的文件
const inputPath = join(outputDir, "long_content.markdown"); const lines = Array.from(
const outputPath = join(outputDir, "long_output.html"); { length: 10000 },
(_, i) => `${i + 1} 行:這是測試內容 Line ${i + 1}: This is test content`,
).join("\n");
writeFileSync(inputPath, `# 長文件測試\n\n${lines}`);
// 生成 10000 行的文件 const result = await runConversion("pandoc", inputPath, outputPath);
const lines = Array.from({ length: 10000 }, (_, i) => stats.results.push(result);
`${i + 1} 行:這是測試內容 Line ${i + 1}: This is test content`
).join("\n");
writeFileSync(inputPath, `# 長文件測試\n\n${lines}`);
const result = await runConversion("pandoc", inputPath, outputPath); if (result.success) {
stats.results.push(result); stats.passed++;
console.log(` ✓ 超長內容處理成功: ${result.outputSize} bytes`);
} else {
stats.failed++;
}
if (result.success) { expect(result.success).toBe(true);
stats.passed++; },
console.log(` ✓ 超長內容處理成功: ${result.outputSize} bytes`); TIMEOUT.slow,
} else { );
stats.failed++;
}
expect(result.success).toBe(true);
}, TIMEOUT.slow);
test("特殊字元處理", async () => { test("特殊字元處理", async () => {
if (!availableTools.pandoc) { if (!availableTools.pandoc) {
@ -985,7 +1076,9 @@ describe("⚠️ 邊界條件測試 Edge Cases", () => {
const inputPath = join(outputDir, "special_chars.markdown"); const inputPath = join(outputDir, "special_chars.markdown");
const outputPath = join(outputDir, "special_output.html"); const outputPath = join(outputDir, "special_output.html");
writeFileSync(inputPath, `# 特殊字元測試 writeFileSync(
inputPath,
`# 特殊字元測試
&amp; &lt; &gt; " ' \` ~ ! @ # $ % ^ & * ( ) - = + [ ] { } | \\ : ; < > ? , . / &amp; &lt; &gt; " ' \` ~ ! @ # $ % ^ & * ( ) - = + [ ] { } | \\ : ; < > ? , . /
@ -996,7 +1089,8 @@ describe("⚠️ 邊界條件測試 Edge Cases", () => {
CJK CJK
`); `,
);
const result = await runConversion("pandoc", inputPath, outputPath); const result = await runConversion("pandoc", inputPath, outputPath);
stats.results.push(result); stats.results.push(result);

View file

@ -1,20 +1,20 @@
/** /**
* *
* *
* 1000+ * 1000+
* *
* *
* 1. * 1.
* 2. * 2.
* 3. * 3.
* 4. * 4.
* *
* *
* bun test tests/e2e/format-matrix.e2e.test.ts * bun test tests/e2e/format-matrix.e2e.test.ts
*/ */
import { describe, test, expect, beforeAll, afterAll } from "bun:test"; 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 { join } from "node:path";
import { spawnSync } from "node:child_process"; 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 E2E_OUTPUT_DIR = "tests/e2e/output/format-matrix";
const TIMEOUT = 60_000; // 60 秒超時 const TIMEOUT = 60_000; // 60 秒超時
// 抽樣比例0.1 = 10%,避免測試過長)
const SAMPLING_RATE = 0.05;
// 每個轉換器最大測試數 // 每個轉換器最大測試數
const MAX_TESTS_PER_CONVERTER = 20; const MAX_TESTS_PER_CONVERTER = 20;
@ -118,18 +115,14 @@ function createTestContent(format: string): Buffer | string {
case "png": case "png":
return Buffer.from([ return Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xcf, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xfe, 0xd4, 0xef, 0x00, 0x00,
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
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); const bmp = Buffer.alloc(70);
bmp.write("BM", 0); bmp.write("BM", 0);
bmp.writeUInt32LE(70, 2); bmp.writeUInt32LE(70, 2);
@ -143,6 +136,7 @@ function createTestContent(format: string): Buffer | string {
bmp[55] = 0x00; bmp[55] = 0x00;
bmp[56] = 0x00; bmp[56] = 0x00;
return bmp; return bmp;
}
// 文檔格式 // 文檔格式
case "md": case "md":
@ -232,7 +226,21 @@ const CONVERTERS: ConverterConfig[] = [
name: "imagemagick", name: "imagemagick",
command: "magick", command: "magick",
from: ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp", "ico", "ppm", "pgm", "pbm"], 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", name: "graphicsmagick",
@ -316,7 +324,7 @@ const CONVERTERS: ConverterConfig[] = [
// 主測試 // 主測試
// ============================================================================= // =============================================================================
let report: MatrixReport = { const report: MatrixReport = {
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
converters: [], converters: [],
totalCombinations: 0, totalCombinations: 0,
@ -350,7 +358,9 @@ afterAll(() => {
console.log(`通過 Passed: ${report.passedTests}`); console.log(`通過 Passed: ${report.passedTests}`);
console.log(`失敗 Failed: ${report.failedTests}`); console.log(`失敗 Failed: ${report.failedTests}`);
console.log(`跳過 Skipped: ${report.skippedTests}`); 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("=".repeat(70));
console.log(`📁 報告路徑: ${reportPath}`); console.log(`📁 報告路徑: ${reportPath}`);
console.log(`📄 摘要路徑: ${summaryPath}`); console.log(`📄 摘要路徑: ${summaryPath}`);
@ -380,7 +390,7 @@ function generateMarkdownSummary(report: MatrixReport): string {
} }
md += `## 失敗的測試\n\n`; md += `## 失敗的測試\n\n`;
const failedResults = report.results.filter(r => !r.success); const failedResults = report.results.filter((r) => !r.success);
if (failedResults.length === 0) { if (failedResults.length === 0) {
md += `無失敗的測試 ✅\n`; md += `無失敗的測試 ✅\n`;
} else { } else {
@ -399,7 +409,6 @@ function generateMarkdownSummary(report: MatrixReport): string {
// ============================================================================= // =============================================================================
describe("📊 格式轉換矩陣 Format Conversion Matrix", () => { describe("📊 格式轉換矩陣 Format Conversion Matrix", () => {
describe("發現可用轉換器 Discover Available Converters", () => { describe("發現可用轉換器 Discover Available Converters", () => {
test("檢測所有轉換器", () => { test("檢測所有轉換器", () => {
for (const config of CONVERTERS) { for (const config of CONVERTERS) {
@ -427,7 +436,9 @@ describe("📊 格式轉換矩陣 Format Conversion Matrix", () => {
report.totalCombinations += combinations.length; report.totalCombinations += combinations.length;
const icon = available ? "✅" : "❌"; const icon = available ? "✅" : "❌";
console.log(` ${icon} ${config.name}: ${combinations.length} 組合 (${available ? "可用" : "不可用"})`); console.log(
` ${icon} ${config.name}: ${combinations.length} 組合 (${available ? "可用" : "不可用"})`,
);
} }
console.log(`\n 📈 總組合數: ${report.totalCombinations}`); console.log(`\n 📈 總組合數: ${report.totalCombinations}`);
@ -436,82 +447,98 @@ describe("📊 格式轉換矩陣 Format Conversion Matrix", () => {
}); });
describe("圖像格式轉換 Image Format Conversions", () => { 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) { for (const converterName of imageConverters) {
describe(`${converterName}`, () => { describe(`${converterName}`, () => {
test(`測試格式轉換`, async () => { test(
const converter = report.converters.find(c => c.name === converterName); `測試格式轉換`,
if (!converter) { async () => {
report.skippedTests++; const converter = report.converters.find((c) => c.name === converterName);
console.log(`${converterName}: 轉換器未找到`); if (!converter) {
return; 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}`);
} }
}
}, 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) { for (const converterName of docConverters) {
describe(`${converterName}`, () => { describe(`${converterName}`, () => {
test(`測試格式轉換`, async () => { test(
const converter = report.converters.find(c => c.name === converterName); `測試格式轉換`,
if (!converter) { async () => {
report.skippedTests++; const converter = report.converters.find((c) => c.name === converterName);
console.log(`${converterName}: 轉換器未找到`); if (!converter) {
return; 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}`);
} }
}
}, 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) { for (const converterName of dataConverters) {
describe(`${converterName}`, () => { describe(`${converterName}`, () => {
test(`測試格式轉換`, async () => { test(
const converter = report.converters.find(c => c.name === converterName); `測試格式轉換`,
if (!converter) { async () => {
report.skippedTests++; const converter = report.converters.find((c) => c.name === converterName);
console.log(`${converterName}: 轉換器未找到`); if (!converter) {
return; 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),
});
} }
}
}, 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); const shuffled = [...combinations].sort(() => Math.random() - 0.5);
return shuffled.slice(0, maxCount); return shuffled.slice(0, maxCount);
} }
// 為 sampled 變數提供全局作用域
let sampled: [string, string][] = [];

View file

@ -1,24 +1,24 @@
/** /**
* E2E * E2E
* *
* *
* - PDFMathTranslate (pdf2zh) * - PDFMathTranslate (pdf2zh)
* - BabelDOC * - BabelDOC
* - * -
* *
* *
* 1. 使 API * 1. 使 API
* 2. API * 2. API
* 3. PDF * 3. PDF
* *
* *
* bun test tests/e2e/translation.e2e.test.ts * bun test tests/e2e/translation.e2e.test.ts
*/ */
import { describe, test, expect, beforeAll, afterAll } from "bun:test"; 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 { 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[] = [ const SUPPORTED_LANGUAGES: Language[] = [
{ code: "zh", name: "Simplified Chinese", nativeName: "简体中文", testPhrase: "这是一个测试" }, { 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: "en", name: "English", nativeName: "English", testPhrase: "This is a test" },
{ code: "ja", name: "Japanese", nativeName: "日本語", testPhrase: "これはテストです" }, { code: "ja", name: "Japanese", nativeName: "日本語", testPhrase: "これはテストです" },
{ code: "ko", name: "Korean", 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: "it", name: "Italian", nativeName: "Italiano", testPhrase: "Questo è un test" },
{ code: "ar", name: "Arabic", nativeName: "العربية", testPhrase: "هذا اختبار" }, { code: "ar", name: "Arabic", nativeName: "العربية", testPhrase: "هذا اختبار" },
{ code: "th", name: "Thai", 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 { try {
// 嘗試使用 xelatex // 嘗試使用 xelatex
const result = spawnSync("xelatex", [ const result = spawnSync(
"-interaction=nonstopmode", "xelatex",
"-output-directory=" + join(path, ".."), ["-interaction=nonstopmode", "-output-directory=" + join(path, ".."), texPath],
texPath, {
], { timeout: 60000,
timeout: 60000, encoding: "utf-8",
encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
stdio: ["pipe", "pipe", "pipe"], },
}); );
return result.status === 0 && existsSync(path); return result.status === 0 && existsSync(path);
} catch { } catch {
@ -192,9 +202,9 @@ let testPdfPath: string;
beforeAll(async () => { beforeAll(async () => {
console.log("\n🔧 初始化翻譯測試... Initializing translation tests...\n"); console.log("\n🔧 初始化翻譯測試... Initializing translation tests...\n");
ensureDir(E2E_OUTPUT_DIR); ensureDir(E2E_OUTPUT_DIR);
// 檢測翻譯工具 // 檢測翻譯工具
console.log(" 檢測翻譯工具..."); console.log(" 檢測翻譯工具...");
translators = { translators = {
@ -204,7 +214,9 @@ beforeAll(async () => {
for (const [name, status] of Object.entries(translators)) { for (const [name, status] of Object.entries(translators)) {
const icon = 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"}`,
);
} }
// 檢查或創建測試 PDF // 檢查或創建測試 PDF
@ -230,16 +242,25 @@ afterAll(() => {
console.log(`✅ 通過 Passed: ${stats.passed}`); console.log(`✅ 通過 Passed: ${stats.passed}`);
console.log(`❌ 失敗 Failed: ${stats.failed}`); console.log(`❌ 失敗 Failed: ${stats.failed}`);
console.log(`⏭ 跳過 Skipped: ${stats.skipped}`); 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)); console.log("=".repeat(70));
// 生成報告 // 生成報告
const reportPath = join(E2E_OUTPUT_DIR, "translation-report.json"); const reportPath = join(E2E_OUTPUT_DIR, "translation-report.json");
writeFileSync(reportPath, JSON.stringify({ writeFileSync(
generatedAt: new Date().toISOString(), reportPath,
translators, JSON.stringify(
stats, {
}, null, 2)); generatedAt: new Date().toISOString(),
translators,
stats,
},
null,
2,
),
);
console.log(`📁 報告已保存: ${reportPath}`); console.log(`📁 報告已保存: ${reportPath}`);
}); });
@ -250,86 +271,92 @@ afterAll(() => {
describe("📚 PDFMathTranslate (pdf2zh)", () => { describe("📚 PDFMathTranslate (pdf2zh)", () => {
// 主要語言測試 // 主要語言測試
describe("主要語言翻譯 Primary Languages", () => { describe("主要語言翻譯 Primary Languages", () => {
const primaryLanguages = SUPPORTED_LANGUAGES.filter(l => const primaryLanguages = SUPPORTED_LANGUAGES.filter((l) =>
["zh", "en", "ja", "ko"].includes(l.code) ["zh", "en", "ja", "ko"].includes(l.code),
); );
for (const lang of primaryLanguages) { for (const lang of primaryLanguages) {
test(`英文 → ${lang.nativeName} (${lang.code})`, async () => { test(
stats.total++; `英文 → ${lang.nativeName} (${lang.code})`,
async () => {
stats.total++;
if (!translators.pdf2zh?.available) { if (!translators.pdf2zh?.available) {
stats.skipped++; stats.skipped++;
console.log(` ⏭ 跳過: pdf2zh 不可用`); console.log(` ⏭ 跳過: pdf2zh 不可用`);
return; 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}: 輸出為空`);
} }
expect(success).toBe(true); if (!existsSync(testPdfPath)) {
} catch (error) { stats.skipped++;
const duration = Date.now() - startTime; console.log(` ⏭ 跳過: 測試 PDF 不存在`);
stats.failed++; return;
stats.results.push({ }
translator: "pdf2zh",
sourceLang: "en", const outputPath = join(E2E_OUTPUT_DIR, `pdf2zh_en_to_${lang.code}.tar`);
targetLang: lang.code, const startTime = Date.now();
success: false, const inputSize = statSync(testPdfPath).size;
duration,
inputSize, try {
outputSize: 0, // 動態導入轉換器
error: String(error), const module = await import("../../src/converters/pdfmathtranslate");
});
console.log(` ✗ en → ${lang.code}: ${error}`); // 設置目標語言(透過環境變數或參數)
throw error; process.env.PDF2ZH_TARGET_LANG = lang.code;
}
}, TIMEOUT); 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", () => { describe("次要語言翻譯 Secondary Languages", () => {
const secondaryLanguages = SUPPORTED_LANGUAGES.filter(l => const secondaryLanguages = SUPPORTED_LANGUAGES.filter((l) =>
["de", "fr", "es", "ru"].includes(l.code) ["de", "fr", "es", "ru"].includes(l.code),
); );
for (const lang of secondaryLanguages) { for (const lang of secondaryLanguages) {
@ -349,78 +376,82 @@ describe("📚 PDFMathTranslate (pdf2zh)", () => {
describe("🌐 BabelDOC", () => { describe("🌐 BabelDOC", () => {
describe("PDF 翻譯", () => { describe("PDF 翻譯", () => {
const testLanguages = SUPPORTED_LANGUAGES.filter(l => const testLanguages = SUPPORTED_LANGUAGES.filter((l) => ["zh", "ja"].includes(l.code));
["zh", "ja"].includes(l.code)
);
for (const lang of testLanguages) { for (const lang of testLanguages) {
test(`英文 → ${lang.nativeName} (${lang.code})`, async () => { test(
stats.total++; `英文 → ${lang.nativeName} (${lang.code})`,
async () => {
stats.total++;
if (!translators.babeldoc?.available) { if (!translators.babeldoc?.available) {
stats.skipped++; stats.skipped++;
console.log(` ⏭ 跳過: babeldoc 不可用`); console.log(` ⏭ 跳過: babeldoc 不可用`);
return; 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}: 輸出為空`);
} }
expect(success).toBe(true); if (!existsSync(testPdfPath)) {
} catch (error) { stats.skipped++;
const duration = Date.now() - startTime; console.log(` ⏭ 跳過: 測試 PDF 不存在`);
stats.failed++; return;
stats.results.push({ }
translator: "babeldoc",
sourceLang: "en", const outputPath = join(E2E_OUTPUT_DIR, `babeldoc_en_to_${lang.code}.tar`);
targetLang: lang.code, const startTime = Date.now();
success: false, const inputSize = statSync(testPdfPath).size;
duration,
inputSize, try {
outputSize: 0, const module = await import("../../src/converters/babeldoc");
error: String(error),
}); // 設置目標語言
console.log(` ✗ en → ${lang.code}: ${error}`); process.env.BABELDOC_TARGET_LANG = lang.code;
throw error;
} await module.convert(testPdfPath, "pdf", "tar", outputPath);
}, TIMEOUT);
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("\n📋 支援的語言 Supported Languages:\n");
console.log("| 代碼 | 名稱 | 本地名稱 | 測試短語 |"); console.log("| 代碼 | 名稱 | 本地名稱 | 測試短語 |");
console.log("|------|------|----------|----------|"); console.log("|------|------|----------|----------|");
for (const lang of SUPPORTED_LANGUAGES) { 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"); const matrixPath = join(E2E_OUTPUT_DIR, "language-matrix.json");
writeFileSync(matrixPath, JSON.stringify({ writeFileSync(
languages: SUPPORTED_LANGUAGES, matrixPath,
translators: Object.keys(translators), JSON.stringify(
generatedAt: new Date().toISOString(), {
}, null, 2)); languages: SUPPORTED_LANGUAGES,
translators: Object.keys(translators),
generatedAt: new Date().toISOString(),
},
null,
2,
),
);
console.log(`\n📁 語言矩陣已保存: ${matrixPath}`); console.log(`\n📁 語言矩陣已保存: ${matrixPath}`);
expect(SUPPORTED_LANGUAGES.length).toBeGreaterThan(0); expect(SUPPORTED_LANGUAGES.length).toBeGreaterThan(0);