Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions packages/engine/src/services/frameCapture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, expect } from "vitest";
import { isFontResourceError } from "./frameCapture.js";

describe("isFontResourceError", () => {
it("matches Google Fonts CSS load failures via location.url", () => {
expect(
isFontResourceError(
"error",
"Failed to load resource: net::ERR_FAILED",
"https://fonts.googleapis.com/css2?family=Inter",
),
).toBe(true);
});

it("matches gstatic font binaries via location.url", () => {
expect(
isFontResourceError(
"error",
"Failed to load resource: the server responded with a status of 404 (Not Found)",
"https://fonts.gstatic.com/s/inter/v12/foo.woff2",
),
).toBe(true);
});

it("matches self-hosted woff2 failures", () => {
expect(
isFontResourceError(
"error",
"Failed to load resource: net::ERR_CONNECTION_REFUSED",
"http://localhost:9999/font.woff2",
),
).toBe(true);
});

it("matches .ttf and .otf URLs", () => {
expect(
isFontResourceError("error", "Failed to load resource: 404", "http://example.com/a.ttf"),
).toBe(true);
expect(
isFontResourceError("error", "Failed to load resource: 404", "http://example.com/b.otf"),
).toBe(true);
});

it("does NOT match non-font resources (images, scripts, videos)", () => {
expect(
isFontResourceError("error", "Failed to load resource: 404", "https://example.com/img.png"),
).toBe(false);
expect(
isFontResourceError(
"error",
"Failed to load resource: 404",
"https://cdn.example.com/bundle.js",
),
).toBe(false);
expect(
isFontResourceError("error", "Failed to load resource: 404", "https://example.com/video.mp4"),
).toBe(false);
});

it("does NOT match when location.url is missing and text has no URL (safe default)", () => {
expect(isFontResourceError("error", "Failed to load resource: 404", "")).toBe(false);
});

it("still matches when URL appears in text (older Chrome formats)", () => {
expect(
isFontResourceError(
"error",
"Failed to load resource: https://fonts.googleapis.com/... 404",
"",
),
).toBe(true);
});

it("does NOT match non-error console messages", () => {
expect(
isFontResourceError(
"warn",
"Failed to load resource: 404",
"https://fonts.googleapis.com/css2",
),
).toBe(false);
expect(
isFontResourceError(
"info",
"Failed to load resource: 404",
"https://fonts.googleapis.com/css2",
),
).toBe(false);
});

it("does NOT match unrelated error messages", () => {
expect(isFontResourceError("error", "Uncaught ReferenceError: x is not defined", "")).toBe(
false,
);
expect(
isFontResourceError("error", "Some other error", "https://fonts.googleapis.com/css2"),
).toBe(false);
});

it("is case-insensitive for URL matching", () => {
expect(
isFontResourceError(
"error",
"Failed to load resource: 404",
"https://FONTS.GOOGLEAPIS.COM/css2",
),
).toBe(true);
expect(
isFontResourceError("error", "Failed to load resource: 404", "http://example.com/FONT.WOFF2"),
).toBe(true);
});
});
30 changes: 23 additions & 7 deletions packages/engine/src/services/frameCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,36 @@ export async function createCaptureSession(
};
}

/**
* Classify a console "Failed to load resource" error as a font-load failure.
*
* These are expected when deterministic font injection replaces Google Fonts
* @import URLs with embedded base64 — or when the render environment has no
* network access to Google Fonts. Suppressing them reduces noise in render
* output without hiding real asset failures (images, videos, scripts, etc.).
*
* Chrome's `msg.text()` for a failed resource is typically just
* `"Failed to load resource: net::ERR_FAILED"` — the URL is only on
* `msg.location().url`. We match against both so the filter works regardless
* of which form Chrome emits.
*/
export function isFontResourceError(type: string, text: string, locationUrl: string): boolean {
if (type !== "error") return false;
if (!text.startsWith("Failed to load resource")) return false;
return /fonts\.googleapis|fonts\.gstatic|\.(woff2?|ttf|otf)(\b|$)/i.test(
`${locationUrl} ${text}`,
);
}

export async function initializeSession(session: CaptureSession): Promise<void> {
const { page, serverUrl } = session;

// Forward browser console to host with [Browser] prefix
page.on("console", (msg: ConsoleMessage) => {
const type = msg.type();
const text = msg.text();

// Suppress font-loading 404s entirely. These are expected when deterministic
// font injection replaces Google Fonts @import URLs with embedded base64.
const isFontLoadError =
type === "error" &&
text.startsWith("Failed to load resource") &&
/fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text);
const locationUrl = msg.location()?.url ?? "";
const isFontLoadError = isFontResourceError(type, text, locationUrl);

// Other "Failed to load resource" 404s are typically non-blocking (e.g.
// favicon, sourcemaps, optional assets). Prefix them so users know they
Expand Down
Loading