fix: 修复预览与 PDF 渲染边界问题

This commit is contained in:
SkyJourney
2026-07-27 00:33:56 +08:00
parent d93728bb8b
commit b459def232
10 changed files with 413 additions and 85 deletions
+73
View File
@@ -110,6 +110,20 @@ describe("预览 API", () => {
expect(response.json().error).toBe("INVALID_MARKDOWN");
});
it("将无效 Front Matter 映射为文档请求错误", async () => {
app = buildApp({ logger: false });
const response = await app.inject({
method: "POST",
url: "/api/render",
payload: {
markdown: "---\ntitle: [未闭合\n---\n\n# 文档"
}
});
expect(response.statusCode).toBe(400);
expect(response.json().error).toBe("INVALID_FRONT_MATTER");
});
it("返回内置主题 CSS", async () => {
app = buildApp();
const response = await app.inject({
@@ -237,6 +251,42 @@ describe("预览 API", () => {
);
});
it("忽略无效本地主题而不影响其他主题", async () => {
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-"));
const bundledRoot = join(temporaryDirectory, "bundled");
const localRoot = join(temporaryDirectory, "local");
const validThemeRoot = join(localRoot, "valid-theme");
const invalidThemeRoot = join(localRoot, "invalid-theme");
await mkdir(bundledRoot, { recursive: true });
await mkdir(validThemeRoot, { recursive: true });
await mkdir(invalidThemeRoot, { recursive: true });
await writeFile(
join(validThemeRoot, "theme.json"),
JSON.stringify(localThemeManifest("valid-theme")),
"utf8"
);
await writeFile(
join(validThemeRoot, "theme.css"),
"#write { color: #333; }",
"utf8"
);
const onWarning = vi.fn();
const registry = createThemeRegistry({
bundledRoot,
localRoot,
onWarning
});
await expect(registry.list()).resolves.toEqual([
expect.objectContaining({
manifest: expect.objectContaining({ id: "valid-theme" })
})
]);
expect(onWarning).toHaveBeenCalledWith(
expect.stringContaining("invalid-theme")
);
});
it("拒绝主题 CSS 导入外部地址", async () => {
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-"));
const bundledRoot = join(temporaryDirectory, "bundled");
@@ -357,6 +407,29 @@ describe("PDF API", () => {
expect(response.json().error).toBe("INVALID_EXPORT_CONFIG");
});
it("在 PDF 生成前拒绝无效 Front Matter", async () => {
const generate = vi.fn();
app = buildApp({
logger: false,
pdfGenerator: {
generate,
close: vi.fn(async () => undefined)
}
});
const response = await app.inject({
method: "POST",
url: "/api/pdf",
payload: {
markdown: "---\ntitle: [未闭合\n---\n",
exportConfig: defaultExportConfig
}
});
expect(response.statusCode).toBe(400);
expect(response.json().error).toBe("INVALID_FRONT_MATTER");
expect(generate).not.toHaveBeenCalled();
});
it("映射 PDF 队列已满错误", async () => {
app = buildApp({
logger: false,
+50 -2
View File
@@ -29,7 +29,10 @@ function payload(): PdfRenderPayload {
};
}
function fakeBrowser(options: { hangOnGoto?: boolean } = {}) {
function fakeBrowser(
options: { hangOnGoto?: boolean; hangOnNewContext?: boolean } = {}
) {
let releaseNewContext: (() => void) | undefined;
const route = vi.fn();
const closeContext = vi.fn(async () => undefined);
const closeBrowser = vi.fn(async () => undefined);
@@ -65,7 +68,14 @@ function fakeBrowser(options: { hangOnGoto?: boolean } = {}) {
close: closeContext
};
const browser = {
newContext: vi.fn(async () => context),
newContext: options.hangOnNewContext
? vi.fn(
() =>
new Promise<typeof context>((resolve) => {
releaseNewContext = () => resolve(context);
})
)
: vi.fn(async () => context),
close: closeBrowser,
on: vi.fn()
} as unknown as Browser;
@@ -77,6 +87,9 @@ function fakeBrowser(options: { hangOnGoto?: boolean } = {}) {
context,
page,
pdfBuffer,
releaseNewContext() {
releaseNewContext?.();
},
route
};
}
@@ -240,4 +253,39 @@ describe("Playwright PDF 生成器", () => {
expect(fake.closeContext).toHaveBeenCalledTimes(1);
await generator.close();
});
it("浏览器启动阶段也受生成超时限制", async () => {
const fake = fakeBrowser();
let releaseBrowser: (() => void) | undefined;
const generator = new PlaywrightPdfGenerator({
launchBrowser: () =>
new Promise<Browser>((resolve) => {
releaseBrowser = () => resolve(fake.browser);
}),
timeoutMs: 10
});
await expect(generator.generate(payload())).rejects.toBeInstanceOf(
PdfRenderTimeoutError
);
releaseBrowser?.();
await new Promise((resolve) => setTimeout(resolve, 0));
await generator.close();
});
it("上下文创建阶段也受生成超时限制并清理迟到上下文", async () => {
const fake = fakeBrowser({ hangOnNewContext: true });
const generator = new PlaywrightPdfGenerator({
launchBrowser: async () => fake.browser,
timeoutMs: 10
});
await expect(generator.generate(payload())).rejects.toBeInstanceOf(
PdfRenderTimeoutError
);
fake.releaseNewContext();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(fake.closeContext).toHaveBeenCalledTimes(1);
await generator.close();
});
});