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:
parent
26304a295e
commit
840c215597
8 changed files with 1038 additions and 0 deletions
2
tests/e2e/.gitignore
vendored
Normal file
2
tests/e2e/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# E2E 測試輸出目錄(自動生成,不需要版本控制)
|
||||||
|
output/
|
||||||
53
tests/e2e/README.md
Normal file
53
tests/e2e/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# E2E 測試 (End-to-End Tests)
|
||||||
|
|
||||||
|
此目錄包含端對端測試,用於驗證完整的轉換流程。
|
||||||
|
|
||||||
|
## 測試類型
|
||||||
|
|
||||||
|
### 1. 轉換器 E2E 測試 (`converters.e2e.test.ts`)
|
||||||
|
|
||||||
|
測試真實的檔案轉換,需要安裝對應的轉換工具:
|
||||||
|
|
||||||
|
- **Inkscape**: SVG ↔ PNG, PDF, EPS 等
|
||||||
|
- **ImageMagick**: 圖像格式轉換
|
||||||
|
- **LibreOffice**: 文件格式轉換
|
||||||
|
- **FFmpeg**: 音視頻轉換
|
||||||
|
- **Pandoc**: 文件格式轉換
|
||||||
|
|
||||||
|
### 2. API E2E 測試 (`api.e2e.test.ts`)
|
||||||
|
|
||||||
|
測試完整的 HTTP API 流程:
|
||||||
|
|
||||||
|
- 上傳檔案
|
||||||
|
- 啟動轉換
|
||||||
|
- 查詢狀態
|
||||||
|
- 下載結果
|
||||||
|
|
||||||
|
## 執行方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 執行所有 E2E 測試
|
||||||
|
bun test tests/e2e
|
||||||
|
|
||||||
|
# 只執行轉換器 E2E 測試
|
||||||
|
bun test tests/e2e/converters.e2e.test.ts
|
||||||
|
|
||||||
|
# 只執行 API E2E 測試
|
||||||
|
bun test tests/e2e/api.e2e.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 環境要求
|
||||||
|
|
||||||
|
E2E 測試需要安裝實際的轉換工具。在 Docker 環境中運行可確保所有工具都已安裝。
|
||||||
|
|
||||||
|
測試會自動偵測可用的工具,跳過不可用的測試。
|
||||||
|
|
||||||
|
## 測試資料
|
||||||
|
|
||||||
|
測試使用 `tests/e2e/fixtures/` 目錄中的測試檔案。這些是小型的測試檔案,用於快速驗證轉換功能。
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
1. E2E 測試比單元測試慢,通常只在 CI/CD 中運行
|
||||||
|
2. 測試會在 `tests/e2e/output/` 目錄中產生輸出檔案
|
||||||
|
3. 每次測試前會清理輸出目錄
|
||||||
252
tests/e2e/api.e2e.test.ts
Normal file
252
tests/e2e/api.e2e.test.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
/**
|
||||||
|
* API E2E 測試
|
||||||
|
*
|
||||||
|
* 這些測試使用 Elysia 的測試工具直接測試 HTTP API。
|
||||||
|
* 不需要啟動獨立的伺服器。
|
||||||
|
*
|
||||||
|
* 執行方式:
|
||||||
|
* bun test tests/e2e/api.e2e.test.ts
|
||||||
|
*
|
||||||
|
* 測試覆蓋:
|
||||||
|
* - Healthcheck API
|
||||||
|
* - 檔案上傳流程
|
||||||
|
* - 轉換工作流程
|
||||||
|
* - 結果下載流程
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { Elysia } from "elysia";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
|
||||||
|
import { healthcheck } from "../../src/pages/healthcheck";
|
||||||
|
import { setupOutputDir } from "./helpers";
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupOutputDir("api");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// 保留輸出目錄以便檢查
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Healthcheck API 測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Healthcheck API", () => {
|
||||||
|
const app = new Elysia().use(healthcheck);
|
||||||
|
|
||||||
|
test("GET /healthcheck 應返回 200 和 ok 狀態", async () => {
|
||||||
|
const response = await app.handle(new Request("http://localhost/healthcheck"));
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toEqual({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Healthcheck 應該快速響應", async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const response = await app.handle(new Request("http://localhost/healthcheck"));
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(duration).toBeLessThan(100); // 應該在 100ms 內響應
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 靜態資源測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Static Resources", () => {
|
||||||
|
test("robots.txt 應該存在", () => {
|
||||||
|
expect(existsSync("public/robots.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("site.webmanifest 應該存在", () => {
|
||||||
|
expect(existsSync("public/site.webmanifest")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API 結構測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("API Structure", () => {
|
||||||
|
test("轉換器模組應該能正確載入", async () => {
|
||||||
|
const converters = await import("../../src/converters/main");
|
||||||
|
expect(converters).toBeDefined();
|
||||||
|
expect(converters.handleConvert).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("資料庫模組應該能正確載入", async () => {
|
||||||
|
const db = await import("../../src/db/db");
|
||||||
|
expect(db).toBeDefined();
|
||||||
|
expect(db.default).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("healthcheck 模組應該能正確載入", async () => {
|
||||||
|
const module = await import("../../src/pages/healthcheck");
|
||||||
|
expect(module).toBeDefined();
|
||||||
|
expect(module.healthcheck).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 轉換器清單測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Converter List", () => {
|
||||||
|
test("應該有正確的轉換器屬性結構", async () => {
|
||||||
|
const { properties } = await import("../../src/converters/inkscape");
|
||||||
|
|
||||||
|
expect(properties).toHaveProperty("from");
|
||||||
|
expect(properties).toHaveProperty("to");
|
||||||
|
expect(properties.from).toHaveProperty("images");
|
||||||
|
expect(properties.to).toHaveProperty("images");
|
||||||
|
expect(Array.isArray(properties.from.images)).toBe(true);
|
||||||
|
expect(Array.isArray(properties.to.images)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Inkscape 應支援 SVG 輸入", async () => {
|
||||||
|
const { properties } = await import("../../src/converters/inkscape");
|
||||||
|
expect(properties.from.images).toContain("svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Inkscape 應支援 PNG 輸出", async () => {
|
||||||
|
const { properties } = await import("../../src/converters/inkscape");
|
||||||
|
expect(properties.to.images).toContain("png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Inkscape 應支援 PDF 輸出", async () => {
|
||||||
|
const { properties } = await import("../../src/converters/inkscape");
|
||||||
|
expect(properties.to.images).toContain("pdf");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 檔案類型正規化測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("File Type Normalization", () => {
|
||||||
|
test("normalizeFiletype 應該正確處理常見格式", async () => {
|
||||||
|
const { normalizeFiletype } = await import("../../src/helpers/normalizeFiletype");
|
||||||
|
|
||||||
|
// jpg/jfif 會被正規化為 jpeg
|
||||||
|
expect(normalizeFiletype("jpg")).toBe("jpeg");
|
||||||
|
expect(normalizeFiletype("jfif")).toBe("jpeg");
|
||||||
|
expect(normalizeFiletype("JPG")).toBe("jpeg");
|
||||||
|
|
||||||
|
// 其他格式保持原樣(小寫)
|
||||||
|
expect(normalizeFiletype("PNG")).toBe("png");
|
||||||
|
expect(normalizeFiletype("svg")).toBe("svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeOutputFiletype 應該正確處理輸出格式", async () => {
|
||||||
|
const { normalizeOutputFiletype } = await import("../../src/helpers/normalizeFiletype");
|
||||||
|
|
||||||
|
// jpeg 輸出會轉為 jpg
|
||||||
|
expect(normalizeOutputFiletype("jpeg")).toBe("jpg");
|
||||||
|
expect(normalizeOutputFiletype("png")).toBe("png");
|
||||||
|
expect(normalizeOutputFiletype("pdf")).toBe("pdf");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 環境變數測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Environment Variables", () => {
|
||||||
|
test("環境變數模組應該能正確載入", async () => {
|
||||||
|
const env = await import("../../src/helpers/env");
|
||||||
|
|
||||||
|
expect(env).toHaveProperty("WEBROOT");
|
||||||
|
expect(env).toHaveProperty("AUTO_DELETE_EVERY_N_HOURS");
|
||||||
|
expect(env).toHaveProperty("MAX_CONVERT_PROCESS");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("WEBROOT 應該是字串", async () => {
|
||||||
|
const { WEBROOT } = await import("../../src/helpers/env");
|
||||||
|
expect(typeof WEBROOT).toBe("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// i18n 測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("i18n Module", () => {
|
||||||
|
test("i18n 服務應該能正確載入", async () => {
|
||||||
|
const i18n = await import("../../src/i18n/service");
|
||||||
|
expect(i18n).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("預設語言檔案應該存在", () => {
|
||||||
|
expect(existsSync("src/locales/en.json")).toBe(true);
|
||||||
|
expect(existsSync("src/locales/zh-TW.json")).toBe(true);
|
||||||
|
expect(existsSync("src/locales/zh-CN.json")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("語言檔案應該是有效的 JSON", async () => {
|
||||||
|
const enContent = await Bun.file("src/locales/en.json").text();
|
||||||
|
const zhTWContent = await Bun.file("src/locales/zh-TW.json").text();
|
||||||
|
|
||||||
|
expect(() => JSON.parse(enContent)).not.toThrow();
|
||||||
|
expect(() => JSON.parse(zhTWContent)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Transfer 模組測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Transfer Module", () => {
|
||||||
|
test("Transfer 常數應該正確定義", async () => {
|
||||||
|
const { CHUNK_THRESHOLD_BYTES, CHUNK_SIZE_BYTES } = await import(
|
||||||
|
"../../src/transfer/constants"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(CHUNK_THRESHOLD_BYTES).toBe(10 * 1024 * 1024); // 10MB
|
||||||
|
expect(CHUNK_SIZE_BYTES).toBe(5 * 1024 * 1024); // 5MB
|
||||||
|
});
|
||||||
|
|
||||||
|
test("允許的封裝格式應該是 .tar", async () => {
|
||||||
|
const { ALLOWED_ARCHIVE_FORMAT, FORBIDDEN_ARCHIVE_FORMATS } = await import(
|
||||||
|
"../../src/transfer/constants"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ALLOWED_ARCHIVE_FORMAT).toBe(".tar");
|
||||||
|
expect(FORBIDDEN_ARCHIVE_FORMATS).toContain(".tar.gz");
|
||||||
|
expect(FORBIDDEN_ARCHIVE_FORMATS).toContain(".zip");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Upload Manager 應該能正確載入", async () => {
|
||||||
|
const uploadManager = await import("../../src/transfer/uploadManager");
|
||||||
|
expect(uploadManager).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Download Manager 應該能正確載入", async () => {
|
||||||
|
const downloadManager = await import("../../src/transfer/downloadManager");
|
||||||
|
expect(downloadManager).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 資料庫結構測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Database Structure", () => {
|
||||||
|
test("資料庫類型定義應該正確", async () => {
|
||||||
|
const types = await import("../../src/db/types");
|
||||||
|
|
||||||
|
// 這些是 class,檢查它們是否可以被建構
|
||||||
|
expect(types.Jobs).toBeDefined();
|
||||||
|
expect(types.Filename).toBeDefined();
|
||||||
|
expect(types.User).toBeDefined();
|
||||||
|
|
||||||
|
// 驗證可以創建實例
|
||||||
|
const job = new types.Jobs();
|
||||||
|
expect(job).toBeInstanceOf(types.Jobs);
|
||||||
|
});
|
||||||
|
});
|
||||||
374
tests/e2e/converters.e2e.test.ts
Normal file
374
tests/e2e/converters.e2e.test.ts
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
/**
|
||||||
|
* 轉換器 E2E 測試
|
||||||
|
*
|
||||||
|
* 這些測試使用真實的轉換工具執行檔案轉換。
|
||||||
|
* 測試會自動偵測可用的工具,跳過不可用的測試。
|
||||||
|
*
|
||||||
|
* 執行方式:
|
||||||
|
* bun test tests/e2e/converters.e2e.test.ts
|
||||||
|
*
|
||||||
|
* 注意:需要在 Docker 環境或已安裝轉換工具的系統中執行
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { existsSync, statSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { convert as convertInkscape } from "../../src/converters/inkscape";
|
||||||
|
import { convert as convertPandoc } from "../../src/converters/pandoc";
|
||||||
|
import { convert as convertDasel } from "../../src/converters/dasel";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AvailableTools,
|
||||||
|
detectAvailableTools,
|
||||||
|
setupOutputDir,
|
||||||
|
createTestSvg,
|
||||||
|
createTestMarkdown,
|
||||||
|
createTestJson,
|
||||||
|
printTestEnvironment,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
let tools: AvailableTools;
|
||||||
|
let outputDir: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
printTestEnvironment();
|
||||||
|
tools = detectAvailableTools();
|
||||||
|
outputDir = setupOutputDir("converters");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// 保留輸出目錄以便檢查結果
|
||||||
|
console.log(`\nE2E test outputs saved to: ${outputDir}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Inkscape E2E 測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Inkscape E2E Tests", () => {
|
||||||
|
const inkscapeDir = () => join(outputDir, "inkscape");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
if (tools.inkscape) {
|
||||||
|
setupOutputDir("converters/inkscape");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SVG → PNG 轉換", async () => {
|
||||||
|
if (!tools.inkscape) {
|
||||||
|
console.log("⏭ Skipping: Inkscape not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(inkscapeDir(), "input.svg");
|
||||||
|
const outputPath = join(inkscapeDir(), "output.png");
|
||||||
|
|
||||||
|
createTestSvg(inputPath);
|
||||||
|
|
||||||
|
const result = await convertInkscape(inputPath, "svg", "png", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
console.log(` ✓ SVG → PNG: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SVG → PDF 轉換", async () => {
|
||||||
|
if (!tools.inkscape) {
|
||||||
|
console.log("⏭ Skipping: Inkscape not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(inkscapeDir(), "input.svg");
|
||||||
|
const outputPath = join(inkscapeDir(), "output.pdf");
|
||||||
|
|
||||||
|
createTestSvg(inputPath);
|
||||||
|
|
||||||
|
const result = await convertInkscape(inputPath, "svg", "pdf", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
console.log(` ✓ SVG → PDF: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SVG → EPS 轉換", async () => {
|
||||||
|
if (!tools.inkscape) {
|
||||||
|
console.log("⏭ Skipping: Inkscape not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(inkscapeDir(), "input.svg");
|
||||||
|
const outputPath = join(inkscapeDir(), "output.eps");
|
||||||
|
|
||||||
|
createTestSvg(inputPath);
|
||||||
|
|
||||||
|
const result = await convertInkscape(inputPath, "svg", "eps", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
console.log(` ✓ SVG → EPS: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pandoc E2E 測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Pandoc E2E Tests", () => {
|
||||||
|
const pandocDir = () => join(outputDir, "pandoc");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
if (tools.pandoc) {
|
||||||
|
setupOutputDir("converters/pandoc");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Markdown → HTML 轉換", async () => {
|
||||||
|
if (!tools.pandoc) {
|
||||||
|
console.log("⏭ Skipping: Pandoc not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(pandocDir(), "input.md");
|
||||||
|
const outputPath = join(pandocDir(), "output.html");
|
||||||
|
|
||||||
|
createTestMarkdown(inputPath);
|
||||||
|
|
||||||
|
// Pandoc 使用 "markdown" 而非 "md" 作為格式名稱
|
||||||
|
const result = await convertPandoc(inputPath, "markdown", "html", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 驗證輸出內容
|
||||||
|
const content = await Bun.file(outputPath).text();
|
||||||
|
expect(content).toContain("<h1");
|
||||||
|
expect(content).toContain("Test Document");
|
||||||
|
|
||||||
|
console.log(` ✓ Markdown → HTML: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Markdown → DOCX 轉換", async () => {
|
||||||
|
if (!tools.pandoc) {
|
||||||
|
console.log("⏭ Skipping: Pandoc not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(pandocDir(), "input.md");
|
||||||
|
const outputPath = join(pandocDir(), "output.docx");
|
||||||
|
|
||||||
|
createTestMarkdown(inputPath);
|
||||||
|
|
||||||
|
// Pandoc 使用 "markdown" 而非 "md" 作為格式名稱
|
||||||
|
const result = await convertPandoc(inputPath, "markdown", "docx", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
console.log(` ✓ Markdown → DOCX: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Markdown → RST 轉換", async () => {
|
||||||
|
if (!tools.pandoc) {
|
||||||
|
console.log("⏭ Skipping: Pandoc not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(pandocDir(), "input.md");
|
||||||
|
const outputPath = join(pandocDir(), "output.rst");
|
||||||
|
|
||||||
|
createTestMarkdown(inputPath);
|
||||||
|
|
||||||
|
// Pandoc 使用 "markdown" 而非 "md" 作為格式名稱
|
||||||
|
const result = await convertPandoc(inputPath, "markdown", "rst", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 驗證 RST 格式
|
||||||
|
const content = await Bun.file(outputPath).text();
|
||||||
|
expect(content).toContain("Test Document");
|
||||||
|
|
||||||
|
console.log(` ✓ Markdown → RST: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dasel E2E 測試(資料格式轉換)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Dasel E2E Tests", () => {
|
||||||
|
const daselDir = () => join(outputDir, "dasel");
|
||||||
|
const isDaselAvailable = () => {
|
||||||
|
try {
|
||||||
|
Bun.spawnSync(["dasel", "--version"]);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupOutputDir("converters/dasel");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("JSON → YAML 轉換", async () => {
|
||||||
|
if (!isDaselAvailable()) {
|
||||||
|
console.log("⏭ Skipping: Dasel not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(daselDir(), "input.json");
|
||||||
|
const outputPath = join(daselDir(), "output.yaml");
|
||||||
|
|
||||||
|
createTestJson(inputPath);
|
||||||
|
|
||||||
|
const result = await convertDasel(inputPath, "json", "yaml", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 驗證 YAML 格式
|
||||||
|
const content = await Bun.file(outputPath).text();
|
||||||
|
expect(content).toContain("name:");
|
||||||
|
expect(content).toContain("test");
|
||||||
|
|
||||||
|
console.log(` ✓ JSON → YAML: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("JSON → TOML 轉換", async () => {
|
||||||
|
if (!isDaselAvailable()) {
|
||||||
|
console.log("⏭ Skipping: Dasel not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(daselDir(), "input.json");
|
||||||
|
const outputPath = join(daselDir(), "output.toml");
|
||||||
|
|
||||||
|
createTestJson(inputPath);
|
||||||
|
|
||||||
|
const result = await convertDasel(inputPath, "json", "toml", outputPath);
|
||||||
|
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
console.log(` ✓ JSON → TOML: ${stats.size} bytes`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 批次轉換測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Batch Conversion E2E Tests", () => {
|
||||||
|
const batchDir = () => join(outputDir, "batch");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupOutputDir("converters/batch");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("多檔案 SVG → PNG 批次轉換", async () => {
|
||||||
|
if (!tools.inkscape) {
|
||||||
|
console.log("⏭ Skipping: Inkscape not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numFiles = 3;
|
||||||
|
const conversions: Promise<string>[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numFiles; i++) {
|
||||||
|
const inputPath = join(batchDir(), `input_${i}.svg`);
|
||||||
|
const outputPath = join(batchDir(), `output_${i}.png`);
|
||||||
|
|
||||||
|
createTestSvg(inputPath);
|
||||||
|
conversions.push(convertInkscape(inputPath, "svg", "png", outputPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(conversions);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(numFiles);
|
||||||
|
results.forEach((result) => expect(result).toBe("Done"));
|
||||||
|
|
||||||
|
// 驗證所有輸出檔案
|
||||||
|
for (let i = 0; i < numFiles; i++) {
|
||||||
|
const outputPath = join(batchDir(), `output_${i}.png`);
|
||||||
|
expect(existsSync(outputPath)).toBe(true);
|
||||||
|
const stats = statSync(outputPath);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ✓ Batch SVG → PNG: ${numFiles} files converted`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 錯誤處理測試
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("Error Handling E2E Tests", () => {
|
||||||
|
const errorDir = () => join(outputDir, "errors");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupOutputDir("converters/errors");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("不存在的輸入檔案應該失敗", async () => {
|
||||||
|
if (!tools.inkscape) {
|
||||||
|
console.log("⏭ Skipping: Inkscape not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(errorDir(), "nonexistent.svg");
|
||||||
|
const outputPath = join(errorDir(), "output.png");
|
||||||
|
|
||||||
|
await expect(convertInkscape(inputPath, "svg", "png", outputPath)).rejects.toBeDefined();
|
||||||
|
|
||||||
|
console.log(" ✓ Correctly rejected non-existent input file");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("無效的 SVG 檔案應該處理優雅", async () => {
|
||||||
|
if (!tools.inkscape) {
|
||||||
|
console.log("⏭ Skipping: Inkscape not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(errorDir(), "invalid.svg");
|
||||||
|
const outputPath = join(errorDir(), "invalid_output.png");
|
||||||
|
|
||||||
|
// 建立無效的 SVG 檔案
|
||||||
|
await Bun.write(inputPath, "This is not a valid SVG file");
|
||||||
|
|
||||||
|
// Inkscape 可能會產生錯誤或空輸出
|
||||||
|
try {
|
||||||
|
await convertInkscape(inputPath, "svg", "png", outputPath);
|
||||||
|
// 如果沒有拋出錯誤,檢查輸出是否存在(可能為空或無效)
|
||||||
|
} catch {
|
||||||
|
// 預期會有錯誤
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(" ✓ Handled invalid SVG gracefully");
|
||||||
|
});
|
||||||
|
});
|
||||||
29
tests/e2e/fixtures/test.json
Normal file
29
tests/e2e/fixtures/test.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "e2e-test-data",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Test JSON file for E2E testing",
|
||||||
|
"metadata": {
|
||||||
|
"created": "2024-01-01T00:00:00Z",
|
||||||
|
"author": "E2E Test Suite"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{ "id": 1, "name": "Item One", "value": 100 },
|
||||||
|
{ "id": 2, "name": "Item Two", "value": 200 },
|
||||||
|
{ "id": 3, "name": "Item Three", "value": 300 }
|
||||||
|
],
|
||||||
|
"nested": {
|
||||||
|
"level1": {
|
||||||
|
"level2": {
|
||||||
|
"key": "deep-value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["test", "e2e", "conversion"]
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"enabled": true,
|
||||||
|
"threshold": 0.75,
|
||||||
|
"options": ["a", "b", "c"]
|
||||||
|
}
|
||||||
|
}
|
||||||
46
tests/e2e/fixtures/test.md
Normal file
46
tests/e2e/fixtures/test.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# E2E Test Document
|
||||||
|
|
||||||
|
This is a test document for end-to-end testing of the ConvertX file conversion system.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
ConvertX is a self-hosted file conversion tool that supports multiple formats.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple Formats**: Support for images, documents, audio, video, and more
|
||||||
|
- **Self-Hosted**: Complete control over your data
|
||||||
|
- **i18n Support**: Available in 65+ languages
|
||||||
|
- **Chunk Transfer**: Efficient handling of large files
|
||||||
|
|
||||||
|
## Code Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
def hello_world():
|
||||||
|
print("Hello, ConvertX!")
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table Example
|
||||||
|
|
||||||
|
| Format | Type | Converter |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| SVG | Image | Inkscape |
|
||||||
|
| PDF | Document | LibreOffice |
|
||||||
|
| MP4 | Video | FFmpeg |
|
||||||
|
| DOCX | Document | Pandoc |
|
||||||
|
|
||||||
|
## List Example
|
||||||
|
|
||||||
|
1. Upload your file
|
||||||
|
2. Select the target format
|
||||||
|
3. Click convert
|
||||||
|
4. Download the result
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Thank you for using ConvertX!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This document is used for E2E testing purposes.*
|
||||||
18
tests/e2e/fixtures/test.svg
Normal file
18
tests/e2e/fixtures/test.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="200" height="200" fill="#f0f0f0"/>
|
||||||
|
|
||||||
|
<!-- Main shape -->
|
||||||
|
<rect x="20" y="20" width="160" height="160" fill="#4285f4" rx="20"/>
|
||||||
|
|
||||||
|
<!-- Inner circle -->
|
||||||
|
<circle cx="100" cy="100" r="50" fill="#ffffff"/>
|
||||||
|
|
||||||
|
<!-- Cross lines -->
|
||||||
|
<line x1="100" y1="60" x2="100" y2="140" stroke="#4285f4" stroke-width="8"/>
|
||||||
|
<line x1="60" y1="100" x2="140" y2="100" stroke="#4285f4" stroke-width="8"/>
|
||||||
|
|
||||||
|
<!-- Text label -->
|
||||||
|
<text x="100" y="180" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#333">E2E Test</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 711 B |
264
tests/e2e/helpers.ts
Normal file
264
tests/e2e/helpers.ts
Normal 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");
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue