fix(snapshot): preserve code layout and export full images
This commit is contained in:
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`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user