convertor/tests/e2e/helpers.ts
Your Name 840c215597 test(e2e): add End-to-End test suite
Added comprehensive E2E tests:

1. API E2E Tests (api.e2e.test.ts):
   - Healthcheck API testing
   - Static resources verification
   - Module loading tests
   - Converter properties validation
   - File type normalization tests
   - i18n module tests
   - Transfer module tests
   - Database structure tests

2. Converter E2E Tests (converters.e2e.test.ts):
   - Real Inkscape conversions (SVG → PNG/PDF/EPS)
   - Real Pandoc conversions (Markdown → HTML/DOCX/RST)
   - Dasel format conversions (JSON → YAML/TOML)
   - Batch conversion tests
   - Error handling tests
   - Auto-detection of available tools

3. Test Infrastructure:
   - helpers.ts: Tool detection, test file generation
   - fixtures/: Sample test files (SVG, MD, JSON)
   - .gitignore: Exclude output directory

Total: 34 new E2E tests
2026-01-23 00:14:31 +08:00

264 lines
6.3 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 測試輔助工具
*
* 提供 E2E 測試所需的共用功能:
* - 工具可用性偵測
* - 測試檔案生成
* - 輸出目錄管理
*/
import { execSync, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
/** E2E 測試輸出目錄 */
export const E2E_OUTPUT_DIR = "tests/e2e/output";
/** E2E 測試 fixtures 目錄 */
export const E2E_FIXTURES_DIR = "tests/e2e/fixtures";
/**
* 檢查命令是否可用
*/
export function isCommandAvailable(command: string): boolean {
try {
const result = spawnSync(process.platform === "win32" ? "where" : "which", [command], {
stdio: "pipe",
timeout: 5000,
});
return result.status === 0;
} catch {
return false;
}
}
/**
* 取得命令版本
*/
export function getCommandVersion(command: string, versionFlag = "--version"): string | null {
try {
const result = execSync(`${command} ${versionFlag}`, {
encoding: "utf-8",
timeout: 10000,
stdio: ["pipe", "pipe", "pipe"],
});
return result.trim().split("\n")[0] || null;
} catch {
return null;
}
}
/**
* 可用的轉換工具
*/
export interface AvailableTools {
inkscape: boolean;
imagemagick: boolean;
graphicsmagick: boolean;
libreoffice: boolean;
ffmpeg: boolean;
pandoc: boolean;
calibre: boolean;
potrace: boolean;
vips: boolean;
resvg: boolean;
}
/**
* 偵測所有可用的轉換工具
*/
export function detectAvailableTools(): AvailableTools {
return {
inkscape: isCommandAvailable("inkscape"),
imagemagick: isCommandAvailable("magick") || isCommandAvailable("convert"),
graphicsmagick: isCommandAvailable("gm"),
libreoffice: isCommandAvailable("soffice") || isCommandAvailable("libreoffice"),
ffmpeg: isCommandAvailable("ffmpeg"),
pandoc: isCommandAvailable("pandoc"),
calibre: isCommandAvailable("ebook-convert"),
potrace: isCommandAvailable("potrace"),
vips: isCommandAvailable("vips"),
resvg: isCommandAvailable("resvg"),
};
}
/**
* 清理並建立輸出目錄
*/
export function setupOutputDir(subDir?: string): string {
const outputDir = subDir ? join(E2E_OUTPUT_DIR, subDir) : E2E_OUTPUT_DIR;
if (existsSync(outputDir)) {
rmSync(outputDir, { recursive: true, force: true });
}
mkdirSync(outputDir, { recursive: true });
return outputDir;
}
/**
* 確保 fixtures 目錄存在
*/
export function ensureFixturesDir(): string {
if (!existsSync(E2E_FIXTURES_DIR)) {
mkdirSync(E2E_FIXTURES_DIR, { recursive: true });
}
return E2E_FIXTURES_DIR;
}
/**
* 建立測試用 SVG 檔案
*/
export function createTestSvg(outputPath: string): void {
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<rect x="10" y="10" width="80" height="80" fill="#4285f4" rx="10"/>
<circle cx="50" cy="50" r="25" fill="#ffffff"/>
<text x="50" y="55" text-anchor="middle" font-size="12" fill="#333">Test</text>
</svg>`;
writeFileSync(outputPath, svgContent);
}
/**
* 建立測試用 PNG 檔案1x1 像素)
*/
export function createTestPng(outputPath: string): void {
// 最小的有效 PNG 檔案 (1x1 紅色像素)
const pngBuffer = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 dimensions
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, // bit depth, color type, etc
0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, // IDAT chunk
0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, 0x00, // compressed data
0x01, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb4, // checksum
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, // IEND chunk
0xae, 0x42, 0x60, 0x82,
]);
writeFileSync(outputPath, pngBuffer);
}
/**
* 建立測試用純文字檔案
*/
export function createTestText(outputPath: string, content?: string): void {
const textContent =
content ||
`# Test Document
This is a test document for E2E testing.
## Features
- Feature 1
- Feature 2
- Feature 3
## Conclusion
End of test document.
`;
writeFileSync(outputPath, textContent);
}
/**
* 建立測試用 HTML 檔案
*/
export function createTestHtml(outputPath: string): void {
const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Document</title>
</head>
<body>
<h1>Test Document</h1>
<p>This is a test paragraph for E2E testing.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</body>
</html>`;
writeFileSync(outputPath, htmlContent);
}
/**
* 建立測試用 Markdown 檔案
*/
export function createTestMarkdown(outputPath: string): void {
const mdContent = `# Test Document
This is a **test** document for E2E testing.
## Code Example
\`\`\`javascript
console.log("Hello, World!");
\`\`\`
## List
1. First item
2. Second item
3. Third item
## Table
| Name | Value |
|------|-------|
| A | 1 |
| B | 2 |
`;
writeFileSync(outputPath, mdContent);
}
/**
* 建立測試用 JSON 檔案
*/
export function createTestJson(outputPath: string): void {
const jsonContent = {
name: "test",
version: "1.0.0",
description: "Test JSON for E2E testing",
data: {
items: [1, 2, 3],
nested: {
key: "value",
},
},
};
writeFileSync(outputPath, JSON.stringify(jsonContent, null, 2));
}
/**
* 等待檔案存在
*/
export async function waitForFile(filePath: string, timeoutMs = 30000): Promise<boolean> {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
if (existsSync(filePath)) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return false;
}
/**
* 印出測試環境資訊
*/
export function printTestEnvironment(): void {
console.log("\n=== E2E Test Environment ===");
console.log(`Platform: ${process.platform}`);
console.log(`Node.js: ${process.version}`);
const tools = detectAvailableTools();
console.log("\nAvailable tools:");
for (const [tool, available] of Object.entries(tools)) {
const status = available ? "✓" : "✗";
console.log(` ${status} ${tool}`);
}
console.log("============================\n");
}