fix(desktop): add legacy webview fallbacks for snapshots and dialogs
This commit is contained in:
8 files changed
+232
-13
No files matched your search
@@ -1,5 +1,12 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CODE_SNAPSHOT_CSS, renderCodeSnapshotHtml, savePngDataUrlToFile, snapshotElementToPng } from "@/lib/codeSnapshot/codeSnapshot";
|
||||
import { CODE_SNAPSHOT_CSS, materializeSnapshotCloneLineNumbers, renderCodeSnapshotHtml, savePngDataUrlToFile, snapshotElementToPng } from "@/lib/codeSnapshot/codeSnapshot";
|
||||
import { LEGACY_WEBVIEW_CLASS } from "@/lib/ui/legacyWebView";
|
||||
|
||||
const codeSnapshotDialogSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/codeSnapshot/CodeSnapshotDialog.vue"), "utf8");
|
||||
|
||||
const { createCodeHighlighter, highlightCode, toPng, isTauriRuntime, save, writeFile } = vi.hoisted(() => {
|
||||
const highlightCode = vi.fn((content: string, _lang: string) => `<span class="line">${content}</span>`);
|
||||
@@ -78,6 +85,7 @@ describe("renderCodeSnapshotHtml", () => {
|
||||
expect(html).toContain(".dbx-code-snapshot *");
|
||||
expect(html).toContain("border: 0");
|
||||
expect(html).toContain("outline: 0");
|
||||
expect(html).toContain(".dbx-code-snapshot__line-number");
|
||||
});
|
||||
|
||||
it("renders macOS traffic lights and an optional title by default", async () => {
|
||||
@@ -124,8 +132,10 @@ describe("renderCodeSnapshotHtml", () => {
|
||||
|
||||
expect(dark).toContain("background:#0d1117");
|
||||
expect(dark).toContain("--dbx-snapshot-line-number:#484f58");
|
||||
expect(dark).toContain("color:#e1e4e8");
|
||||
expect(light).toContain("background:#ffffff");
|
||||
expect(light).toContain("--dbx-snapshot-line-number:#d0d7de");
|
||||
expect(light).toContain("color:#24292f");
|
||||
});
|
||||
|
||||
it("applies custom padding and font size", async () => {
|
||||
@@ -135,12 +145,35 @@ describe("renderCodeSnapshotHtml", () => {
|
||||
expect(html).toContain("padding:0 24px 24px");
|
||||
});
|
||||
|
||||
it("keeps the snapshot dialog responsive layout classes in source", () => {
|
||||
expect(codeSnapshotDialogSource).toContain("sm:max-w-[860px]");
|
||||
expect(codeSnapshotDialogSource).toContain("md:flex-row");
|
||||
expect(codeSnapshotDialogSource).toContain("md:w-52");
|
||||
});
|
||||
|
||||
it("materializes real line-number nodes in the export clone", () => {
|
||||
document.body.innerHTML = '<div class="dbx-code-snapshot"><pre class="dbx-code-snapshot__pre dbx-code-snapshot__pre--numbered"><code><span class="line">SELECT 1</span><span class="line">FROM dual</span></code></pre></div>';
|
||||
const root = document.body.firstElementChild as HTMLElement;
|
||||
|
||||
materializeSnapshotCloneLineNumbers(root);
|
||||
|
||||
const pre = root.querySelector(".dbx-code-snapshot__pre") as HTMLElement;
|
||||
const lines = Array.from(root.querySelectorAll<HTMLElement>("code > .line"));
|
||||
expect(pre.classList.contains("dbx-code-snapshot__pre--numbered")).toBe(false);
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(lines[0]?.querySelector(".dbx-code-snapshot__line-number")?.textContent).toBe("1");
|
||||
expect(lines[1]?.querySelector(".dbx-code-snapshot__line-number")?.textContent).toBe("2");
|
||||
expect(lines[0]?.querySelector(".dbx-code-snapshot__line-content")?.textContent).toBe("SELECT 1");
|
||||
expect(lines[1]?.querySelector(".dbx-code-snapshot__line-content")?.textContent).toBe("FROM dual");
|
||||
});
|
||||
|
||||
it("exports at a capped device-pixel ratio for crisp high-DPI snapshots", async () => {
|
||||
toPng.mockResolvedValue("data:image/png;base64,test");
|
||||
vi.stubGlobal("window", { devicePixelRatio: 3 });
|
||||
try {
|
||||
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160, scrollWidth: 640, scrollHeight: 240 } as HTMLElement);
|
||||
|
||||
const options = toPng.mock.calls.at(-1)?.[1];
|
||||
expect(toPng).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
@@ -153,6 +186,8 @@ describe("renderCodeSnapshotHtml", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(options?.adjustPseudoElement).toBeUndefined();
|
||||
expect(options?.onclone).toBeUndefined();
|
||||
|
||||
vi.stubGlobal("window", { devicePixelRatio: 0 });
|
||||
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160, scrollWidth: 320, scrollHeight: 160 } as HTMLElement);
|
||||
@@ -162,6 +197,31 @@ describe("renderCodeSnapshotHtml", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("enables the legacy WebView export fallback only when the runtime is marked legacy", async () => {
|
||||
toPng.mockResolvedValue("data:image/png;base64,test");
|
||||
document.documentElement.classList.add(LEGACY_WEBVIEW_CLASS);
|
||||
try {
|
||||
await snapshotElementToPng({ offsetWidth: 320, offsetHeight: 160, scrollWidth: 320, scrollHeight: 160 } as HTMLElement);
|
||||
|
||||
const options = toPng.mock.calls.at(-1)?.[1];
|
||||
expect(typeof options?.adjustPseudoElement).toBe("function");
|
||||
expect(typeof options?.onclone).toBe("function");
|
||||
expect(options.adjustPseudoElement?.(document.createElement("span"), ":after", {} as CSSStyleDeclaration)).toBeUndefined();
|
||||
|
||||
const clone = document.createElement("div");
|
||||
clone.innerHTML = '<div class="dbx-code-snapshot"><pre class="dbx-code-snapshot__pre dbx-code-snapshot__pre--numbered" style="--dbx-snapshot-line-number:#d0d7de"><code><span class="line">SELECT 1</span><span class="line">FROM dual</span></code></pre></div>';
|
||||
options.onclone?.(clone);
|
||||
|
||||
const line = clone.querySelector<HTMLElement>(".dbx-code-snapshot__line-number");
|
||||
expect(line?.textContent).toBe("1");
|
||||
expect(line?.getAttribute("style")).toContain("display: inline-block");
|
||||
expect(line?.getAttribute("style")).toContain("margin-right: 1.4em");
|
||||
expect(line?.getAttribute("style")).toContain("color: #d0d7de");
|
||||
} finally {
|
||||
document.documentElement.classList.remove(LEGACY_WEBVIEW_CLASS);
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import type { AppThemeAppearance } from "@/lib/app/appTheme";
|
||||
import { createAiShikiBlockCodeHighlighter, type AiCodeHighlighter } from "@/lib/ai/aiCodeHighlighter";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { LEGACY_WEBVIEW_CLASS } from "@/lib/ui/legacyWebView";
|
||||
|
||||
export interface CodeSnapshotSource {
|
||||
code: string;
|
||||
@@ -27,7 +28,7 @@ export interface CodeSnapshotStyleOptions {
|
||||
appearance: AppThemeAppearance;
|
||||
/** Render the macOS-style traffic light dots. Defaults to true. */
|
||||
showTrafficLights?: boolean;
|
||||
/** Render line numbers via CSS counters. Defaults to true. */
|
||||
/** Render line numbers. Defaults to true. */
|
||||
showLineNumbers?: boolean;
|
||||
/** Padding (px) around the code area. Defaults to 16. */
|
||||
padding?: number;
|
||||
@@ -55,6 +56,11 @@ const SNAPSHOT_LINE_NUMBER: Record<AppThemeAppearance, string> = {
|
||||
dark: "#484f58",
|
||||
};
|
||||
|
||||
const SNAPSHOT_CODE_TEXT: Record<AppThemeAppearance, string> = {
|
||||
light: "#24292f",
|
||||
dark: "#e1e4e8",
|
||||
};
|
||||
|
||||
const TRAFFIC_LIGHT_COLORS = ["#ff5f57", "#febc2e", "#28c840"] as const;
|
||||
const MAX_EXPORT_PIXEL_RATIO = 2;
|
||||
const MAX_EXPORT_CANVAS_EDGE = 16_384;
|
||||
@@ -112,6 +118,16 @@ export const CODE_SNAPSHOT_CSS = `
|
||||
display: block;
|
||||
min-height: 1.6em;
|
||||
}
|
||||
.dbx-code-snapshot__line-number {
|
||||
display: inline-block;
|
||||
min-width: 2.2em;
|
||||
margin-right: 1.4em;
|
||||
text-align: right;
|
||||
color: var(--dbx-snapshot-line-number, #8b949e);
|
||||
font-variant-numeric: tabular-nums;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.dbx-code-snapshot__pre--numbered {
|
||||
counter-reset: dbx-snapshot-line;
|
||||
}
|
||||
@@ -160,6 +176,48 @@ function renderSnapshotBar(title: string | undefined, appearance: AppThemeAppear
|
||||
return `<div class="dbx-code-snapshot__bar" style="background:${SNAPSHOT_BAR_BACKGROUND[appearance]};color:${SNAPSHOT_BAR_TEXT[appearance]}">${dots}${titleHtml}</div>`;
|
||||
}
|
||||
|
||||
function isSnapshotNumberedLineNode(node: Node): node is Element {
|
||||
return node instanceof Element && node.classList.contains("line") && node.closest(".dbx-code-snapshot__pre--numbered") !== null;
|
||||
}
|
||||
|
||||
function hasMaterializedLineNumber(line: Element): boolean {
|
||||
return Array.from(line.children).some((child) => child.classList.contains("dbx-code-snapshot__line-number"));
|
||||
}
|
||||
|
||||
function legacyWebViewExportFallbackEnabled(): boolean {
|
||||
return typeof document !== "undefined" && document.documentElement.classList.contains(LEGACY_WEBVIEW_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep preview rendering on CSS counters, but rewrite the cloned export DOM to
|
||||
* use real line-number nodes. Older WebKit foreignObject exports can flatten
|
||||
* every counter() value to "1", while the live preview still looks correct.
|
||||
*/
|
||||
export function materializeSnapshotCloneLineNumbers(root: ParentNode): void {
|
||||
for (const block of Array.from(root.querySelectorAll<HTMLElement>(".dbx-code-snapshot__pre--numbered"))) {
|
||||
block.classList.remove("dbx-code-snapshot__pre--numbered");
|
||||
const lineNumberColor = block.style.getPropertyValue("--dbx-snapshot-line-number").trim() || SNAPSHOT_LINE_NUMBER.dark;
|
||||
const lines = Array.from(block.querySelectorAll<HTMLElement>("code > .line"));
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (hasMaterializedLineNumber(line)) continue;
|
||||
const lineNumber = block.ownerDocument.createElement("span");
|
||||
lineNumber.className = "dbx-code-snapshot__line-number";
|
||||
lineNumber.setAttribute("aria-hidden", "true");
|
||||
lineNumber.textContent = String(index + 1);
|
||||
lineNumber.style.cssText = ["display:inline-block", "min-width:2.2em", "margin-right:1.4em", "text-align:right", `color:${lineNumberColor}`, "font-variant-numeric:tabular-nums", "user-select:none", "-webkit-user-select:none"].join(";");
|
||||
|
||||
const lineContent = block.ownerDocument.createElement("span");
|
||||
lineContent.className = "dbx-code-snapshot__line-content";
|
||||
lineContent.style.display = "inline";
|
||||
while (line.firstChild) {
|
||||
lineContent.append(line.firstChild);
|
||||
}
|
||||
|
||||
line.append(lineNumber, lineContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a self-contained snapshot DOM (as an HTML string) for the given code.
|
||||
* The returned string includes its own `<style>` tag and can be injected via
|
||||
@@ -189,7 +247,7 @@ export async function renderCodeSnapshotHtml(source: CodeSnapshotSource, options
|
||||
`<style>${CODE_SNAPSHOT_CSS}</style>` +
|
||||
`<div class="dbx-code-snapshot" data-snapshot-appearance="${appearance}" style="background:${SNAPSHOT_BACKGROUND[appearance]};font-size:${fontSize}px">` +
|
||||
bar +
|
||||
`<pre class="${preClass}" style="--dbx-snapshot-line-number:${lineNumberColor};padding:0 ${padding}px ${padding}px"><code>${body}</code></pre>` +
|
||||
`<pre class="${preClass}" style="--dbx-snapshot-line-number:${lineNumberColor};color:${SNAPSHOT_CODE_TEXT[appearance]};padding:0 ${padding}px ${padding}px"><code>${body}</code></pre>` +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
@@ -207,12 +265,28 @@ export async function snapshotElementToPng(element: HTMLElement): Promise<string
|
||||
|
||||
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));
|
||||
const useLegacyLineNumberFallback = legacyWebViewExportFallbackEnabled();
|
||||
return domtoimage.toPng(element, {
|
||||
quality: 1,
|
||||
width,
|
||||
height,
|
||||
// dom-to-image-more 使用 scale 放大实际画布;pixelRatio 不是该库支持的参数。
|
||||
scale,
|
||||
adjustPseudoElement: useLegacyLineNumberFallback
|
||||
? (node: Node, pseudoElement: ":before" | ":after") => {
|
||||
if (pseudoElement === ":before" && isSnapshotNumberedLineNode(node)) {
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
: undefined,
|
||||
onclone: useLegacyLineNumberFallback
|
||||
? (clone: Node) => {
|
||||
if (typeof (clone as ParentNode).querySelectorAll === "function") {
|
||||
materializeSnapshotCloneLineNumbers(clone as ParentNode);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
style: {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
|
||||
Reference in new issue
Block a user