Файли
openidea/src/platform/candidateFonts.test.ts
T
l0ng-ai 1e898367a5 fix(fonts): render unavailable macOS user fonts through binary IPC (#643)
On macOS versions where WKWebView refuses user-installed fonts by family name (fonts in ~/Library/Fonts or /Library/Fonts), the editor, terminal, Markdown and UI fell back to the default font and Nerd Font icons showed as boxes; only a webfont loaded from the file's bytes renders.

Root cause: the shared font resolver only ever referred to fonts by local family name, so every consumer depended on the WebView resolving user fonts by name. A URL-based webfont is blocked by the existing font-src policy, which stays unchanged.

Fix: the shared resolver probes whether a family renders by name (on a document-attached canvas; a detached canvas falls back even when the font works) and only when it does not, fetches the enumerated user font files over a raw-bytes IPC (user fonts only, looked up by family, no paths accepted) and registers them with the FontFace API under a stable alias. Only the faces nearest regular/bold and their italics are loaded, once per family. Covers editor and editor fallback, diff/merge views, terminal font, fallback and built-in Nerd fallback (re-measured after loading), embedded terminal, settings preview, custom UI font, Markdown and Mermaid. A debug-only environment switch reproduces the WebView restriction for testing.

Verified on macOS 26 with the restriction simulated (icons boxes before, correct after) and without it (no font bytes transferred, no regression). Not run on the reporter's macOS version, Windows or Linux; other platforms keep the previous behaviour.

Fixes #592
2026-11-01 12:23:08 +08:00

55 рядки
2.3 KiB
TypeScript

import { afterEach, expect, it, vi } from "vitest";
import { detectCandidateFonts } from "./candidateFonts";
afterEach(() => vi.restoreAllMocks());
it("probes the legacy list and classifies measured mono and proportional families", () => {
const ctx = {
font: "",
measureText(text: string) {
if (this.font.includes('"Menlo"')) return { width: 120 };
if (this.font.includes('"Georgia"')) return { width: text.startsWith("i") ? 60 : 180 };
return { width: 100 };
},
};
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(ctx as unknown as CanvasRenderingContext2D);
expect(detectCandidateFonts()).toEqual([
{ name: "Menlo", monospaced: true },
{ name: "Georgia", monospaced: false },
]);
});
it("returns no inferred families when Canvas is absent", () => {
const remove = vi.spyOn(HTMLCanvasElement.prototype, "remove");
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
expect(detectCandidateFonts()).toEqual([]);
expect(remove).toHaveBeenCalledOnce();
});
it("detects user-installed candidates with a connected probe and removes it afterwards", () => {
const probes: HTMLCanvasElement[] = [];
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(function (this: HTMLCanvasElement) {
probes.push(this);
const isConnected = () => this.isConnected;
return {
font: "",
measureText(this: { font: string }) {
return { width: isConnected() && this.font.includes('"Fira Code"') ? 120 : 100 };
},
} as unknown as CanvasRenderingContext2D;
});
expect(detectCandidateFonts()).toEqual([{ name: "Fira Code", monospaced: true }]);
expect(probes[0]?.hidden).toBe(true);
expect(probes[0]?.isConnected).toBe(false);
});
it("contains failures from Canvas and never claims candidates are installed", () => {
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => {
throw new Error("disabled");
});
expect(detectCandidateFonts()).toEqual([]);
});
it("does not treat invalid measurements as available fonts", () => {
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({
font: "",
measureText: () => ({ width: Number.NaN }),
} as unknown as CanvasRenderingContext2D);
expect(detectCandidateFonts()).toEqual([]);
});