/* Copyright 2024 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React from "react"; import { act, render, screen, waitFor } from "@testing-library/react"; import { mocked, MockedObject } from "jest-mock"; import userEvent from "@testing-library/user-event"; import fetchMock from "fetch-mock-jest"; import { ThemeChoicePanel } from "../../../../src/components/views/settings/ThemeChoicePanel"; import SettingsStore from "../../../../src/settings/SettingsStore"; import ThemeWatcher from "../../../../src/settings/watchers/ThemeWatcher"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; jest.mock("../../../../src/settings/watchers/ThemeWatcher"); describe("", () => { /** * Enable or disable the system theme * @param enable */ async function enableSystemTheme(enable: boolean) { await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, enable); } /** * Set the theme * @param theme */ async function setTheme(theme: string) { await SettingsStore.setValue("theme", null, SettingLevel.DEVICE, theme); } beforeEach(async () => { mocked(ThemeWatcher).mockImplementation(() => { return { isSystemThemeSupported: jest.fn().mockReturnValue(true), } as unknown as MockedObject; }); await enableSystemTheme(false); await setTheme("light"); }); it("renders the theme choice UI", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); describe("theme selection", () => { describe("system theme", () => { it("should disable Match system theme", async () => { render(); expect(screen.getByRole("checkbox", { name: "Match system theme" })).not.toBeChecked(); }); it("should enable Match system theme", async () => { await enableSystemTheme(true); render(); expect(screen.getByRole("checkbox", { name: "Match system theme" })).toBeChecked(); }); it("should change the system theme when clicked", async () => { jest.spyOn(SettingsStore, "setValue"); render(); act(() => screen.getByRole("checkbox", { name: "Match system theme" }).click()); // The system theme should be enabled expect(screen.getByRole("checkbox", { name: "Match system theme" })).toBeChecked(); expect(SettingsStore.setValue).toHaveBeenCalledWith("use_system_theme", null, "device", true); }); }); describe("theme selection", () => { it("should disable theme selection when system theme is enabled", async () => { await enableSystemTheme(true); render(); // We expect all the themes to be disabled const themes = screen.getAllByRole("radio"); themes.forEach((theme) => { expect(theme).toBeDisabled(); }); }); it("should enable theme selection when system theme is disabled", async () => { render(); // We expect all the themes to be disabled const themes = screen.getAllByRole("radio"); themes.forEach((theme) => { expect(theme).not.toBeDisabled(); }); }); it("should have light theme selected", async () => { render(); // We expect the light theme to be selected const lightTheme = screen.getByRole("radio", { name: "Light" }); expect(lightTheme).toBeChecked(); // And the dark theme shouldn't be selected const darkTheme = screen.getByRole("radio", { name: "Dark" }); expect(darkTheme).not.toBeChecked(); }); it("should switch to dark theme", async () => { jest.spyOn(SettingsStore, "setValue"); render(); const darkTheme = screen.getByRole("radio", { name: "Dark" }); const lightTheme = screen.getByRole("radio", { name: "Light" }); expect(darkTheme).not.toBeChecked(); // Switch to the dark theme act(() => darkTheme.click()); expect(SettingsStore.setValue).toHaveBeenCalledWith("theme", null, "device", "dark"); // Dark theme is now selected await waitFor(() => expect(darkTheme).toBeChecked()); // Light theme is not selected anymore expect(lightTheme).not.toBeChecked(); // The setting should be updated expect(SettingsStore.setValue).toHaveBeenCalledWith("theme", null, "device", "dark"); }); }); }); describe("custom theme", () => { const aliceTheme = { name: "Alice theme", is_dark: true, colors: {} }; const bobTheme = { name: "Bob theme", is_dark: false, colors: {} }; beforeEach(async () => { await SettingsStore.setValue("feature_custom_themes", null, SettingLevel.DEVICE, true); await SettingsStore.setValue("custom_themes", null, SettingLevel.DEVICE, [aliceTheme]); }); it("should render the custom theme section", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); it("should add a custom theme", async () => { jest.spyOn(SettingsStore, "setValue"); // Respond to the theme request fetchMock.get("http://bob.theme", { body: bobTheme, }); render(); // Add the new custom theme const customThemeInput = screen.getByRole("textbox", { name: "Add custom theme" }); await userEvent.type(customThemeInput, "http://bob.theme"); screen.getByRole("button", { name: "Add custom theme" }).click(); // The new custom theme is added to the user's themes await waitFor(() => expect(SettingsStore.setValue).toHaveBeenCalledWith("custom_themes", null, "account", [ aliceTheme, bobTheme, ]), ); }); it("should display custom theme", () => { const { asFragment } = render(); expect(screen.getByRole("radio", { name: aliceTheme.name })).toBeInTheDocument(); expect(screen.getByRole("listitem", { name: aliceTheme.name })).toBeInTheDocument(); expect(asFragment()).toMatchSnapshot(); }); }); });