import { afterEach, describe, expect, it } from "vitest"; import type { FastifyInstance } from "fastify"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildApp } from "../src/app.js"; let app: FastifyInstance | undefined; let temporaryDirectory: string | undefined; afterEach(async () => { await app?.close(); if (temporaryDirectory) { await rm(temporaryDirectory, { recursive: true, force: true }); } app = undefined; temporaryDirectory = undefined; }); describe("预览 API", () => { it("渲染 Markdown 并返回安全文章结构", async () => { app = buildApp(); const response = await app.inject({ method: "POST", url: "/api/render", payload: { markdown: "# 文档\n\n" } }); expect(response.statusCode).toBe(200); const result = response.json(); expect(result.articleHtml).toContain('id="write"'); expect(result.articleHtml).not.toContain(" { app = buildApp(); const response = await app.inject({ method: "POST", url: "/api/render", payload: { markdown: 42 } }); expect(response.statusCode).toBe(400); expect(response.json().error).toBe("INVALID_MARKDOWN"); }); it("返回内置主题 CSS", async () => { app = buildApp(); const response = await app.inject({ method: "GET", url: "/api/themes/typora-like/css" }); expect(response.statusCode).toBe(200); expect(response.headers["content-type"]).toContain("text/css"); expect(response.body).toContain("#write"); }); it("发现本地主题并安全提供相对字体资源", async () => { temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-")); const themeDirectory = join(temporaryDirectory, "local-test"); await mkdir(join(themeDirectory, "fonts"), { recursive: true }); await writeFile( join(themeDirectory, "theme.json"), JSON.stringify({ manifestVersion: 1, id: "local-test", name: "本地测试主题", version: "local", description: "本地主题测试", author: "test", license: "local-only", entry: "theme.css", domPreset: "typora", defaultFontSize: "16px", supportedFeatures: ["code", "table"], bundled: false }), "utf8" ); await writeFile( join(themeDirectory, "theme.css"), [ "@include-when-export url(https://example.com/font.css);", "@font-face { src: url('./fonts/test.woff2') format('woff2'); }", "#write { font-family: LocalTest; }" ].join("\n"), "utf8" ); await writeFile( join(themeDirectory, "fonts", "test.woff2"), Buffer.from([0, 1, 2, 3]) ); app = buildApp({ localThemeRoot: temporaryDirectory, logger: false }); const themesResponse = await app.inject({ method: "GET", url: "/api/themes" }); expect(themesResponse.statusCode).toBe(200); expect(themesResponse.json().themes).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "local-test", bundled: false, source: "local" }) ]) ); const cssResponse = await app.inject({ method: "GET", url: "/api/themes/local-test/css" }); expect(cssResponse.statusCode).toBe(200); expect(cssResponse.body).not.toContain("@include-when-export"); expect(cssResponse.body).toContain( "/api/themes/local-test/assets/fonts/test.woff2" ); const assetResponse = await app.inject({ method: "GET", url: "/api/themes/local-test/assets/fonts/test.woff2" }); expect(assetResponse.statusCode).toBe(200); expect(assetResponse.headers["content-type"]).toContain("font/woff2"); expect(assetResponse.rawPayload).toEqual(Buffer.from([0, 1, 2, 3])); }); });