fix(snapshot): preserve previews when syntax highlighting fails

Closes #6777
This commit is contained in:
t8y2 committed 2026-08-20 14:33:10 +00:00
1 parent 69e5e81071
commit 5767a0fd59
2 files changed
+63 -10

No files matched your search

@@ -1,15 +1,20 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CODE_SNAPSHOT_CSS, renderCodeSnapshotHtml, savePngDataUrlToFile, snapshotElementToPng } from "@/lib/codeSnapshot/codeSnapshot";
const { toPng, isTauriRuntime, save, writeFile } = vi.hoisted(() => ({
toPng: vi.fn(),
isTauriRuntime: vi.fn(),
save: vi.fn(),
writeFile: vi.fn(),
}));
const { createCodeHighlighter, highlightCode, toPng, isTauriRuntime, save, writeFile } = vi.hoisted(() => {
const highlightCode = vi.fn((content: string, _lang: string) => `<span class="line">${content}</span>`);
return {
createCodeHighlighter: vi.fn(async () => highlightCode),
highlightCode,
toPng: vi.fn(),
isTauriRuntime: vi.fn(),
save: vi.fn(),
writeFile: vi.fn(),
};
});
vi.mock("@/lib/ai/aiCodeHighlighter", () => ({
createAiShikiCodeHighlighter: vi.fn().mockResolvedValue((content: string, _lang: string) => `<span class="line">${content}</span>`),
createAiShikiCodeHighlighter: createCodeHighlighter,
}));
vi.mock("dom-to-image-more", () => ({
@@ -23,7 +28,45 @@ vi.mock("@/lib/backend/tauriRuntime", () => ({
vi.mock("@tauri-apps/plugin-dialog", () => ({ save }));
vi.mock("@tauri-apps/plugin-fs", () => ({ writeFile }));
beforeEach(() => {
createCodeHighlighter.mockResolvedValue(highlightCode);
highlightCode.mockClear();
highlightCode.mockImplementation((content: string) => `<span class="line">${content}</span>`);
});
describe("renderCodeSnapshotHtml", () => {
it("falls back to safe text when the code highlighter cannot initialize", async () => {
createCodeHighlighter.mockRejectedValueOnce(new SyntaxError("Invalid regular expression: invalid group specifier name"));
const html = await renderCodeSnapshotHtml({ code: "SELECT <初始化失败>", lang: "sql" }, { appearance: "light" });
expect(html).toContain('<code><span class="line">SELECT &lt;初始化失败&gt;</span></code>');
});
it("falls back to safe line-preserving text when legacy WebKit cannot compile highlighting regexes", async () => {
highlightCode.mockImplementationOnce(() => {
throw new SyntaxError("Invalid regular expression: invalid group specifier name");
});
const html = await renderCodeSnapshotHtml({ code: `SELECT '<用户>&" 😀'\r\n-- 第二行\n`, lang: "sql" }, { appearance: "light" });
expect(html).toContain('<code><span class="line">SELECT &#39;&lt;用户&gt;&amp;&quot; 😀&#39;</span><span class="line">-- 第二行</span><span class="line"></span></code>');
expect(html).not.toContain("<用户>");
expect(html.match(/class="line"/g)).toHaveLength(3);
});
it("renders large fallback selections with one highlighter attempt", async () => {
const code = Array.from({ length: 5_000 }, (_, index) => `SELECT ${index}`).join("\n");
highlightCode.mockImplementationOnce(() => {
throw new SyntaxError("Invalid regular expression: invalid group specifier name");
});
const html = await renderCodeSnapshotHtml({ code, lang: "sql" }, { appearance: "dark" });
expect(highlightCode).toHaveBeenCalledOnce();
expect(html.match(/class="line"/g)).toHaveLength(5_000);
});
it("renders a self-contained snapshot with embedded styles", async () => {
const html = await renderCodeSnapshotHtml({ code: "SELECT 1", lang: "sql" }, { appearance: "dark" });
@@ -150,6 +150,10 @@ function escapeHtml(value: string): string {
return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
}
function renderEscapedSnapshotLines(value: string): string {
return `<span class="line">${escapeHtml(value).replace(/\r\n|\r|\n/g, '</span><span class="line">')}</span>`;
}
function renderSnapshotBar(title: string | undefined, appearance: AppThemeAppearance, showTrafficLights: boolean): string {
const dots = showTrafficLights ? TRAFFIC_LIGHT_COLORS.map((color) => `<span class="dbx-code-snapshot__dot" style="background:${color}"></span>`).join("") : "";
const titleHtml = title ? `<span class="dbx-code-snapshot__title" style="color:${SNAPSHOT_BAR_TEXT[appearance]}">${escapeHtml(title)}</span>` : "";
@@ -162,8 +166,14 @@ function renderSnapshotBar(title: string | undefined, appearance: AppThemeAppear
* `v-html`, then exported with `snapshotElementToPng`.
*/
export async function renderCodeSnapshotHtml(source: CodeSnapshotSource, options: CodeSnapshotStyleOptions): Promise<string> {
const highlight = await getCodeSnapshotHighlighter();
const body = highlight(source.code, source.lang, options.appearance);
let body: string;
try {
const highlight = await getCodeSnapshotHighlighter();
body = highlight(source.code, source.lang, options.appearance);
} catch {
// Legacy WebKit can reject regular expressions generated by Shiki.
body = renderEscapedSnapshotLines(source.code);
}
const appearance = options.appearance;
const showBar = options.showTrafficLights !== false || !!source.title;