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
This commit is contained in:
Your Name 2026-01-23 00:14:31 +08:00
parent 26304a295e
commit 840c215597
8 changed files with 1038 additions and 0 deletions

264
tests/e2e/helpers.ts Normal file
View file

@ -0,0 +1,264 @@
/**
* 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");
}