From 7f0f5482e549a8a6d462066928ec0b1415ede721 Mon Sep 17 00:00:00 2001 From: DavidBabinec Date: Tue, 27 Oct 2026 11:09:42 +0100 Subject: [PATCH] fix(wp): upload custom fonts without Google Fonts lookup The shared import handler replaced local file data with a Google Fonts request before the custom-aware uploader could run. Preserve local data and report local upload failures with the right error. --- CHANGELOG.md | 1 + .../src/components/modules/fonts/FontsTab.tsx | 13 ++- .../src/__tests__/wpCustomFontImport.test.ts | 98 +++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 packages/www/src/__tests__/wpCustomFontImport.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e06dff9..f3bb84e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed +- Fixed importing a local font into WordPress failing with a Google Font download error. Custom fonts now upload the selected file directly instead of trying to find it on Google Fonts. - Fixed Bricks sites hanging when removed Core Framework classes were cleaned up. The initial lookup joined post metadata five times, multiplying rows on sites with many metadata entries. It now checks all five Bricks metadata keys through one join while retaining the existing batched cleanup. - Fixed transparent variants and shade/tint counts for RAW colors that reference another variable, such as `var(--bg-body)`. Generated variants now follow the current source color, while manually edited swatches are preserved. Older saved swatches without edit history are left unchanged on load; use Reset on a stale swatch to return it to automatic generation, then save to update the site's CSS. - Fixed the block editor going blank with the Core Framework Gutenberg integration enabled. The editor script was loaded with an outdated, hard-coded dependency list that left out WordPress's `react-jsx-runtime`, so rendering the Core Framework class panel threw on an undefined `ReactJSXRuntime` and took the whole editor down. The script now loads the dependencies its build declares. diff --git a/packages/wp/src/components/modules/fonts/FontsTab.tsx b/packages/wp/src/components/modules/fonts/FontsTab.tsx index 2069896..fd33d05 100644 --- a/packages/wp/src/components/modules/fonts/FontsTab.tsx +++ b/packages/wp/src/components/modules/fonts/FontsTab.tsx @@ -88,7 +88,10 @@ export function FontsTab() { if (!font.family) return; try { - const files = await fetchGoogleFontFiles(font.family, selectedVariants); + const files = + font.category === "custom-font" + ? font.files + : await fetchGoogleFontFiles(font.family, selectedVariants); const resolvedFont = { ...font, files }; const id = `${font.title}-${font.family}-${selectedVariants.join(".")}`; const variants = font.variants.reduce((acc: FontVariantData[], v: FontVariantData) => { @@ -107,8 +110,12 @@ export function FontsTab() { setActiveTab(USER_FONTS); toast.success("Font added successfully"); } catch (error) { - console.error("Failed to download Google Font:", error); - toast.error("Could not download the selected Google Font"); + console.error("Failed to import font:", error); + toast.error( + font.category === "custom-font" + ? "Could not upload the selected custom font" + : "Could not download the selected Google Font", + ); } }; diff --git a/packages/www/src/__tests__/wpCustomFontImport.test.ts b/packages/www/src/__tests__/wpCustomFontImport.test.ts new file mode 100644 index 0000000..e8e0363 --- /dev/null +++ b/packages/www/src/__tests__/wpCustomFontImport.test.ts @@ -0,0 +1,98 @@ +import { act, createElement, type ComponentType } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { Provider, createStore, type PrimitiveAtom } from "jotai"; +import { toast } from "sonner"; + +// The WP shell has its own globals and aliases, checked by build:wp. +// Runtime imports keep that separate type environment out of the web build. +const { FontsTab } = require("../../../wp/src/components/modules/fonts/FontsTab") as { FontsTab: ComponentType }; +const { fontsDataAtom } = require("../../../wp/src/state") as { fontsDataAtom: PrimitiveAtom<{ fonts: Record[] }> }; + +jest.mock("../../../wp/src/state", () => { + const { atom } = jest.requireActual("jotai"); + return { fontsDataAtom: atom({ fonts: [] }) }; +}); +jest.mock("../../../wp/src/components/ClassSectionHeading", () => ({ ClassSectionHeading: () => null })); +jest.mock("../../../wp/src/components/modules/fonts/components/FontsList", () => ({ FontsList: () => null })); +jest.mock("../../../wp/src/components/modules/fonts/ImportGoogleFonts", () => ({ ImportGoogleFonts: () => null })); +jest.mock("../../../wp/src/components/modules/fonts/ImportCustomFonts", () => ({ + ImportCustomFonts: ({ setSelectedVariants, downloadFontsLocally }: { setSelectedVariants: (variants: string[]) => void; downloadFontsLocally: (font: unknown) => Promise }) => + createElement("div", null, createElement("button", { + onClick: () => { + setSelectedVariants(["400normal"]); + }, + }, "Select file"), + createElement("button", { onClick: (event: Event) => { + event.stopPropagation(); + return downloadFontsLocally({ family: "Local Test Font", title: "Local Test Font", category: "custom-font", + files: { "400normal": "data:font/woff2;base64,d09GMg==" }, + variants: [{ id: "400normal", cssSelector: "", comment: "", fontDisplay: "auto" }], + customSelectors: "", customVariable: "" }); + } }, "Add Font")), +})); +jest.mock("../../../wp/src/components/modules/fonts/utils/utils", () => { + Object.assign(globalThis, { TextEncoder: require("node:util").TextEncoder, TextDecoder: require("node:util").TextDecoder }); + return jest.requireActual("../../../wp/src/components/modules/fonts/utils/utils"); +}); +jest.mock("sonner", () => ({ toast: { error: jest.fn(), success: jest.fn() } })); + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const originalFetch = globalThis.fetch; +let root: Root; +let container: HTMLDivElement; +let store: ReturnType; +const fetchMock = jest.fn(); + +beforeEach(async () => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => {}); + globalThis.fetch = fetchMock; + Object.assign(window, { + coreframework: { core_api_url: "https://example.test/wp-json/core-framework/v2/" }, + wpApiSettings: { nonce: "test-nonce" }, + }); + fetchMock.mockImplementation(async (url: string) => { + if (url.includes("googleapis.com")) throw new Error("This local font is not on Google Fonts"); + return { ok: true, json: async () => ({ success: true }) }; + }); + store = createStore(); + container = document.createElement("div"); + root = createRoot(container); + await act(async () => root.render(createElement(Provider, { store }, createElement(FontsTab)))); +}); + +afterEach(() => { + act(() => root.unmount()); + globalThis.fetch = originalFetch; + jest.restoreAllMocks(); +}); + +async function click(text: string) { + const button = [...container.querySelectorAll("button")].find((button) => button.textContent === text || button.firstChild?.textContent === text); + if (!button) throw new Error(`Missing button: ${text}`); + await act(async () => button.click()); +} + +test("uploads the selected local file without replacing it with a Google Fonts lookup", async () => { + await click("Import Local Font"); + await click("Select file"); + await click("Add Font"); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith("https://example.test/wp-json/core-framework/v2/upload-fonts", expect.objectContaining({ + method: "POST", + body: JSON.stringify({ fonts: [{ font_base64: "d09GMg==", filename: "Local-Test-Font-400normal.woff2" }] }), + })); + expect(store.get(fontsDataAtom).fonts).toEqual([expect.objectContaining({ family: "Local Test Font", category: "custom-font" })]); + expect(toast.success).toHaveBeenCalledWith("Font added successfully"); + expect(toast.error).not.toHaveBeenCalled(); +}); + +test("keeps a failed local upload out of the project and reports a custom-font error", async () => { + fetchMock.mockResolvedValue({ ok: false, status: 500 }); + await click("Import Local Font"); + await click("Select file"); + await click("Add Font"); + expect(store.get(fontsDataAtom).fonts).toEqual([]); + expect(toast.success).not.toHaveBeenCalled(); + expect(toast.error).toHaveBeenCalledWith("Could not upload the selected custom font"); +});