convertor/tests/e2e/comprehensive.e2e.test.ts
Your Name e327345ad7 feat: add comprehensive E2E tests with Docker workflow
- Add comprehensive.e2e.test.ts: Full E2E tests covering 25+ converters
- Add format-matrix.e2e.test.ts: Format conversion matrix (70,000+ combinations)
- Add translation.e2e.test.ts: Multi-language translation tests (14 languages)
- Add docker-e2e-tests.yml: GitHub Actions workflow for Docker E2E tests
- Update run-bun-test.yml: Improved basic test workflow
- Add run-e2e-tests.sh: Local test runner script
- Add test scripts to package.json

Tests cover:
- Image formats (Inkscape, ImageMagick, Potrace, etc.)
- Document formats (Pandoc, LibreOffice, Calibre)
- Data formats (Dasel: JSON/YAML/TOML/XML/CSV)
- Translation (PDFMathTranslate, BabelDOC)
- Edge cases (Unicode, long content, special characters)
2026-01-23 12:20:52 +08:00

1013 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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