fix(snapshot): preserve code layout and export full images

This commit is contained in:
dadong authored and GitHub committed 2026-08-20 22:11:49 +08:00
1 parent 68d2919219
commit 81872165da
4 files changed
+102 -16

No files matched your search

@@ -32,6 +32,9 @@ describe("renderCodeSnapshotHtml", () => {
expect(html).toContain('class="dbx-code-snapshot"');
expect(html).toContain('data-snapshot-appearance="dark"');
expect(html).toContain('class="dbx-code-snapshot__pre dbx-code-snapshot__pre--numbered"');
expect(html).toContain(".dbx-code-snapshot *");
expect(html).toContain("border: 0");
expect(html).toContain("outline: 0");
});
it("renders macOS traffic lights and an optional title by default", async () => {
@@ -93,25 +96,72 @@ describe("renderCodeSnapshotHtml", () => {
toPng.mockResolvedValue("data:image/png;base64,test");
vi.stubGlobal("window", { devicePixelRatio: 3 });
try {
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160 } as HTMLElement);
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160, scrollWidth: 640, scrollHeight: 240 } as HTMLElement);
expect(toPng).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
width: 320,
height: 160,
pixelRatio: 2,
width: 640,
height: 240,
scale: 2,
style: {
width: "640px",
height: "240px",
},
}),
);
vi.stubGlobal("window", { devicePixelRatio: 0 });
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160 } as HTMLElement);
expect(toPng).toHaveBeenLastCalledWith(expect.anything(), expect.objectContaining({ pixelRatio: 1 }));
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160, scrollWidth: 320, scrollHeight: 160 } as HTMLElement);
expect(toPng).toHaveBeenLastCalledWith(expect.anything(), expect.objectContaining({ scale: 1 }));
} finally {
vi.unstubAllGlobals();
}
});
it("reduces the export scale when a wide snapshot reaches the canvas edge limit", async () => {
toPng.mockResolvedValue("data:image/png;base64,test");
vi.stubGlobal("window", { devicePixelRatio: 2 });
try {
await snapshotElementToPng({ offsetWidth: 10_000, offsetHeight: 100, scrollWidth: 10_000, scrollHeight: 100 } as HTMLElement);
expect(toPng).toHaveBeenLastCalledWith(expect.anything(), expect.objectContaining({ scale: 16_384 / 10_000 }));
} finally {
vi.unstubAllGlobals();
}
});
it("reduces the export scale when a tall snapshot reaches the canvas edge limit", async () => {
toPng.mockResolvedValue("data:image/png;base64,test");
vi.stubGlobal("window", { devicePixelRatio: 2 });
try {
await snapshotElementToPng({ offsetWidth: 100, offsetHeight: 10_000, scrollWidth: 100, scrollHeight: 10_000 } as HTMLElement);
expect(toPng).toHaveBeenLastCalledWith(expect.anything(), expect.objectContaining({ scale: 16_384 / 10_000 }));
} finally {
vi.unstubAllGlobals();
}
});
it("reduces the export scale when a dense snapshot reaches the total pixel limit", async () => {
toPng.mockResolvedValue("data:image/png;base64,test");
vi.stubGlobal("window", { devicePixelRatio: 2 });
try {
await snapshotElementToPng({ offsetWidth: 6_000, offsetHeight: 6_000, scrollWidth: 6_000, scrollHeight: 6_000 } as HTMLElement);
expect(toPng).toHaveBeenLastCalledWith(expect.anything(), expect.objectContaining({ scale: Math.sqrt((64 * 1024 * 1024) / (6_000 * 6_000)) }));
} finally {
vi.unstubAllGlobals();
}
});
it("rejects snapshots that exceed the safe rendering budget at 1x", async () => {
toPng.mockClear();
await expect(snapshotElementToPng({ offsetWidth: 20_000, offsetHeight: 100, scrollWidth: 20_000, scrollHeight: 100 } as HTMLElement)).rejects.toThrow("Snapshot is too large to export safely (20000 × 100px)");
expect(toPng).not.toHaveBeenCalled();
});
it("propagates a failed native save instead of reporting a browser download", async () => {
isTauriRuntime.mockReturnValue(true);
save.mockResolvedValue("C:\\Users\\example\\snapshot.png");
@@ -57,12 +57,19 @@ const SNAPSHOT_LINE_NUMBER: Record<AppThemeAppearance, string> = {
const TRAFFIC_LIGHT_COLORS = ["#ff5f57", "#febc2e", "#28c840"] as const;
const MAX_EXPORT_PIXEL_RATIO = 2;
const MAX_EXPORT_CANVAS_EDGE = 16_384;
const MAX_EXPORT_CANVAS_PIXELS = 64 * 1024 * 1024;
/**
* Self-contained stylesheet embedded in every snapshot DOM. Keep it prefix
* namespaced (`dbx-code-snapshot`) so it never leaks into the app chrome.
*/
export const CODE_SNAPSHOT_CSS = `
.dbx-code-snapshot,
.dbx-code-snapshot * {
border: 0;
outline: 0;
}
.dbx-code-snapshot {
border-radius: 8px;
overflow: hidden;
@@ -180,13 +187,26 @@ export async function renderCodeSnapshotHtml(source: CodeSnapshotSource, options
/** Convert a snapshot DOM element into a PNG data URL. */
export async function snapshotElementToPng(element: HTMLElement): Promise<string> {
const domtoimage = (await import("dom-to-image-more")).default;
// 预览区允许滚动,因此导出尺寸必须覆盖完整内容,而不是只取当前可见宽度。
const width = Math.max(element.offsetWidth, element.scrollWidth);
const height = Math.max(element.offsetHeight, element.scrollHeight);
const basePixels = width * height;
if (width > MAX_EXPORT_CANVAS_EDGE || height > MAX_EXPORT_CANVAS_EDGE || basePixels > MAX_EXPORT_CANVAS_PIXELS) {
throw new Error(`Snapshot is too large to export safely (${width} × ${height}px). Reduce the code size and try again.`);
}
const targetScale = Math.min(MAX_EXPORT_PIXEL_RATIO, Math.max(1, typeof window === "undefined" ? 1 : window.devicePixelRatio || 1));
const scale = Math.min(targetScale, MAX_EXPORT_CANVAS_EDGE / width, MAX_EXPORT_CANVAS_EDGE / height, Math.sqrt(MAX_EXPORT_CANVAS_PIXELS / basePixels));
return domtoimage.toPng(element, {
quality: 1,
width: element.offsetWidth,
height: element.offsetHeight,
// Preserve text sharpness on high-DPI displays without letting a large
// selected block allocate an unbounded export canvas.
pixelRatio: Math.min(MAX_EXPORT_PIXEL_RATIO, Math.max(1, typeof window === "undefined" ? 1 : window.devicePixelRatio || 1)),
width,
height,
// dom-to-image-more 使用 scale 放大实际画布;pixelRatio 不是该库支持的参数。
scale,
style: {
width: `${width}px`,
height: `${height}px`,
},
});
}