fix: 修復 lint 錯誤和代碼格式化
- 移除未使用的導入 (readdirSync, basename, readFileSync, execSync) - 將不需要重新賦值的 let 改為 const - 修復 switch case 區塊中的詞法聲明問題 - 移除未使用的 SAMPLING_RATE 和 sampled 變量 - 移除未使用的 _timeout 參數 - 運行 Prettier 格式化所有文件
This commit is contained in:
parent
e327345ad7
commit
a2512bf29b
6 changed files with 886 additions and 712 deletions
1
.github/workflows/run-bun-test.yml
vendored
1
.github/workflows/run-bun-test.yml
vendored
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +342,7 @@ 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");
|
||||||
|
|
@ -332,7 +351,9 @@ beforeAll(() => {
|
||||||
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");
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -637,74 +690,86 @@ describe("🌍 多語言翻譯 Multilingual Translation", () => {
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -815,8 +890,15 @@ 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,
|
||||||
|
`# 特殊字元測試
|
||||||
|
|
||||||
& < > " ' \` ~ ! @ # $ % ^ & * ( ) - = + [ ] { } | \\ : ; < > ? , . /
|
& < > " ' \` ~ ! @ # $ % ^ & * ( ) - = + [ ] { } | \\ : ; < > ? , . /
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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][] = [];
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 {
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -436,16 +467,25 @@ describe("🔤 語言矩陣測試 Language Matrix", () => {
|
||||||
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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue