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
264 lines
6.3 KiB
TypeScript
264 lines
6.3 KiB
TypeScript
/**
|
||
* 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");
|
||
}
|