Fix bundled font or custom font not applied after theme switch (#31591)
Some checks failed
Build / Build on macos-14 (push) Waiting to run
Build / Build on ubuntu-24.04 (push) Waiting to run
Build / Build on windows-2022 (push) Waiting to run
Build and Deploy develop / Build & Deploy develop.element.io (push) Waiting to run
Deploy documentation / GitHub Pages (push) Waiting to run
Deploy documentation / deploy (push) Blocked by required conditions
Shared Component Visual Tests / Run Visual Tests (push) Waiting to run
Static Analysis / Typescript Syntax Check (push) Waiting to run
Static Analysis / i18n Check (push) Waiting to run
Static Analysis / Rethemendex Check (push) Waiting to run
Static Analysis / ESLint (push) Waiting to run
Static Analysis / Style Lint (push) Waiting to run
Static Analysis / Workflow Lint (push) Waiting to run
Static Analysis / Analyse Dead Code (push) Waiting to run
Localazy Upload / upload (push) Has been cancelled

* refactor: transform `FontWater.onAction` to switch

* fix: reload font after switching theme

Fix #26248 #31588

When a theme is swiched, `clearCustomTheme` remove all css variables.
After the styles are re-applied but the custom fonts or emoji are not
re-applied.

* test: add test for `Action.ReloadFont`

* test: add missing tests for existing actions

* test(e2e): add tests to ensure that font and emoji stay unchanged

* Revert "fix: reload font after switching theme"

This reverts commit 2b0071af21.

* Revert "refactor: transform `FontWater.onAction` to switch"

This reverts commit 4119158609.

* Revert "test: add test for `Action.ReloadFont`"

This reverts commit 31b3b224cd.

* fix: don't remove custom emoji and cpd font when clearing custom theme

Fix #26248 #31588

When a theme is swiched, `clearCustomTheme` remove all css variables.
After the styles are re-applied but the custom fonts or emoji are not
re-applied.
This fix avoid the custom font and emoji to be removed.

* test: add tests
This commit is contained in:
Florian Duros 2026-01-05 17:14:08 +01:00 committed by GitHub
parent 4bd1d4144f
commit be7be39d0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 113 additions and 4 deletions

View file

@ -56,4 +56,35 @@ test.describe("Appearance user settings tab", () => {
// Assert that the font-family value was removed
await expect(page.locator("body")).toHaveCSS("font-family", '""');
});
test(
"should keep same font and emoji when switching theme",
{ tag: "@screenshot" },
async ({ page, app, user, util }) => {
const roomId = await util.createAndDisplayRoom();
await app.client.sendMessage(roomId, { body: "Message with 🦡", msgtype: "m.text" });
await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
await tab.getByRole("button", { name: "Show advanced" }).click();
await tab.getByRole("switch", { name: "Use bundled emoji font" }).click();
await tab.getByRole("switch", { name: "Use a system font" }).click();
await app.closeDialog();
await expect(page).toMatchScreenshot("window-before-switch.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
// Switch to dark theme
await app.settings.openUserSettings("Appearance");
await util.getMatchSystemThemeSwitch().click();
await util.getDarkTheme().click();
await app.closeDialog();
// Font and emoji should remain the same after theme switch
await expect(page).toMatchScreenshot("window-after-switch.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
},
);
});

View file

@ -150,9 +150,10 @@ class Helpers {
/**
* Create and display a room named Test Room
*/
async createAndDisplayRoom() {
await this.app.client.createRoom({ name: "Test Room" });
async createAndDisplayRoom(): Promise<string> {
const roomId = await this.app.client.createRoom({ name: "Test Room" });
await this.app.viewRoomByName("Test Room");
return roomId;
}
/**

View file

@ -26,6 +26,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "./languageHandler";
import SettingsStore from "./settings/SettingsStore";
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
import { FontWatcher } from "./settings/watchers/FontWatcher";
export const DEFAULT_THEME = "light";
const HIGH_CONTRAST_THEMES: Record<string, string> = {
@ -126,10 +127,15 @@ export function getOrderedThemes(): ITheme[] {
}
function clearCustomTheme(): void {
// remove all css variables, we assume these are there because of the custom theme
// remove all css variables (except font and emoji variables), we assume these are there because of the custom theme
const inlineStyleProps = Object.values(document.body.style);
for (const prop of inlineStyleProps) {
if (typeof prop === "string" && prop.startsWith("--")) {
if (
typeof prop === "string" &&
prop.startsWith("--") &&
prop !== FontWatcher.FONT_FAMILY_CUSTOM_PROPERTY &&
prop !== FontWatcher.EMOJI_FONT_FAMILY_CUSTOM_PROPERTY
) {
document.body.style.removeProperty(prop);
}
}

View file

@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { sleep } from "matrix-js-sdk/src/utils";
import { waitFor } from "jest-matrix-react";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
@ -155,5 +156,33 @@ describe("FontWatcher", function () {
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(0);
});
it("should trigger migration when dispatched", async () => {
await watcher!.start();
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 18);
defaultDispatcher.fire(Action.MigrateBaseFontSize);
await waitFor(() => {
// 18px - 16px (default browser font size) = 2px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(2);
// baseFontSizeV2 should be cleared
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(0);
});
});
});
it("should update root font size with positive delta", async () => {
await new FontWatcher().start();
defaultDispatcher.dispatch({
action: Action.UpdateFontSizeDelta,
delta: 2,
});
await waitFor(() => {
const rootFontSize = document.querySelector<HTMLElement>(":root")!.style.fontSize;
expect(rootFontSize).toContain("2px");
});
});
});

View file

@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import SettingsStore from "../../src/settings/SettingsStore";
import { FontWatcher } from "../../src/settings/watchers/FontWatcher";
import { enumerateThemes, getOrderedThemes, setTheme } from "../../src/theme";
describe("theme", () => {
@ -223,4 +224,45 @@ describe("theme", () => {
]);
});
});
describe("clearCustomTheme", () => {
beforeEach(() => {
// Reset document state
document.body.style.cssText = "";
document.head.querySelectorAll("style[title^='custom-theme-']").forEach((el) => el.remove());
});
it("should not remove font family custom properties", async () => {
// Mock theme elements
const lightTheme = {
dataset: { mxTheme: "light" },
disabled: true,
href: "fake URL",
onload: (): void => void 0,
} as unknown as HTMLStyleElement;
const removePropertySpy = jest.fn();
const styleObject = {
0: FontWatcher.FONT_FAMILY_CUSTOM_PROPERTY,
1: FontWatcher.EMOJI_FONT_FAMILY_CUSTOM_PROPERTY,
2: "--custom-color",
length: 3,
removeProperty: removePropertySpy,
};
jest.spyOn(document.body, "style", "get").mockReturnValue(styleObject as any);
jest.spyOn(document, "querySelectorAll").mockReturnValue([lightTheme] as any);
// Trigger clearCustomTheme via setTheme
await new Promise((resolve) => {
setTheme("light").then(resolve);
lightTheme.onload!({} as Event);
});
// Check that font properties were NOT removed
expect(removePropertySpy).not.toHaveBeenCalledWith(FontWatcher.FONT_FAMILY_CUSTOM_PROPERTY);
expect(removePropertySpy).not.toHaveBeenCalledWith(FontWatcher.EMOJI_FONT_FAMILY_CUSTOM_PROPERTY);
// But custom color should be removed
expect(removePropertySpy).toHaveBeenCalledWith("--custom-color");
});
});
});