From 23a42e0d54f73a842c5d1a8cf47a5e7658cce91b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 15 Apr 2025 09:01:35 +0100 Subject: [PATCH] Refactor several unit tests to use SettingsStore directly. (#29744) * Refactor notifications-test.ts * Refactor other tests to stop mocking SettingsStore --- test/unit-tests/HtmlUtils-test.tsx | 32 +++--- .../components/structures/MatrixChat-test.tsx | 4 + .../structures/TimelinePanel-test.tsx | 16 ++- .../rooms/RoomHeader/RoomHeader-test.tsx | 98 +++++++++++++++---- .../__snapshots__/RoomHeader-test.tsx.snap | 8 +- .../room/NotificationSettingsTab-test.tsx | 4 + test/unit-tests/utils/notifications-test.ts | 50 ++++++---- 7 files changed, 146 insertions(+), 66 deletions(-) diff --git a/test/unit-tests/HtmlUtils-test.tsx b/test/unit-tests/HtmlUtils-test.tsx index 0650db1890..16546e69dc 100644 --- a/test/unit-tests/HtmlUtils-test.tsx +++ b/test/unit-tests/HtmlUtils-test.tsx @@ -7,22 +7,19 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { mocked } from "jest-mock"; import { render, screen } from "jest-matrix-react"; import parse from "html-react-parser"; import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils"; import SettingsStore from "../../src/settings/SettingsStore"; - -jest.mock("../../src/settings/SettingsStore"); - -const enableHtmlTopicFeature = () => { - mocked(SettingsStore).getValue.mockImplementation((arg): any => { - return arg === "feature_html_topic"; - }); -}; +import { SettingLevel } from "../../src/settings/SettingLevel"; +import SdkConfig from "../../src/SdkConfig"; describe("topicToHtml", () => { + afterEach(() => { + SettingsStore.reset(); + }); + function getContent() { return screen.getByRole("contentinfo").children[0].innerHTML; } @@ -38,19 +35,19 @@ describe("topicToHtml", () => { }); it("converts literal HTML topic to HTML", async () => { - enableHtmlTopicFeature(); + SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true); render(
{topicToHtml("pizza", undefined, null, false)}
); expect(getContent()).toEqual("<b>pizza</b>"); }); it("converts true HTML topic to HTML", async () => { - enableHtmlTopicFeature(); + SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true); render(
{topicToHtml("**pizza**", "pizza", null, false)}
); expect(getContent()).toEqual("pizza"); }); it("converts true HTML topic with emoji to HTML", async () => { - enableHtmlTopicFeature(); + SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true); render(
{topicToHtml("**pizza** 🍕", "pizza 🍕", null, false)}
); expect(getContent()).toEqual('pizza 🍕'); }); @@ -107,7 +104,12 @@ describe("bodyToHtml", () => { describe("feature_latex_maths", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_latex_maths"); + SettingsStore.setValue("feature_latex_maths", null, SettingLevel.DEVICE, true); + }); + + afterEach(() => { + SettingsStore.reset(); + SdkConfig.reset(); }); it("should render inline katex", () => { @@ -228,4 +230,8 @@ describe("bodyToNode", () => { expect(asFragment()).toMatchSnapshot(); }); + + afterEach(() => { + jest.resetAllMocks(); + }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index d6db2ce941..8633b3cfcf 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -484,6 +484,10 @@ describe("", () => { ); }); + afterEach(() => { + SettingsStore.reset(); + }); + it("should persist login credentials", async () => { getComponent({ realQueryParams }); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 886fe777e4..87c788d9f9 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -50,6 +50,8 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; import ScrollPanel from "../../../../src/components/structures/ScrollPanel"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController"; // ScrollPanel calls this, but jsdom doesn't mock it for us HTMLDivElement.prototype.scrollBy = () => {}; @@ -310,18 +312,14 @@ describe("TimelinePanel", () => { describe("and sending receipts is disabled", () => { beforeEach(async () => { - client.isVersionSupported.mockResolvedValue(true); - client.doesServerSupportUnstableFeature.mockResolvedValue(true); - - jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string): any => { - if (setting === "sendReadReceipts") return false; - - return undefined; - }); + // Ensure this setting is supported, otherwise it will use the default value. + client.isVersionSupported.mockImplementation(async (v) => v === "v1.4"); + MatrixClientBackedController.matrixClient = client; + SettingsStore.setValue("sendReadReceipts", null, SettingLevel.DEVICE, false); }); afterEach(() => { - mocked(SettingsStore.getValue).mockReset(); + SettingsStore.reset(); }); it("should send a fully read marker and a private receipt", async () => { diff --git a/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx index 846adc8ab4..17d1a048c2 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -57,6 +57,7 @@ import * as UseCall from "../../../../../../src/hooks/useCall"; import { SdkContextClass } from "../../../../../../src/contexts/SDKContext"; import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore"; import { UIFeature } from "../../../../../../src/settings/UIFeature"; +import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; jest.mock("../../../../../../src/utils/ShieldUtils"); jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({ @@ -99,6 +100,7 @@ describe("RoomHeader", () => { afterEach(() => { jest.restoreAllMocks(); + SettingsStore.reset(); }); it("renders the room header", () => { @@ -187,9 +189,7 @@ describe("RoomHeader", () => { it("opens the notifications panel", async () => { const user = userEvent.setup(); - jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => { - if (name === "feature_notifications") return true; - }); + SettingsStore.setValue("feature_notifications", null, SettingLevel.DEVICE, true); render(, getWrapper()); @@ -228,7 +228,15 @@ describe("RoomHeader", () => { describe("UIFeature.Widgets enabled (default)", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature == UIFeature.Widgets); + SdkConfig.put({ + setting_defaults: { + [UIFeature.Widgets]: true, + }, + }); + }); + + afterEach(() => { + SdkConfig.reset(); }); it("should show call buttons in a room with 2 members", () => { @@ -248,7 +256,15 @@ describe("RoomHeader", () => { describe("UIFeature.Widgets disabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => false); + SdkConfig.put({ + setting_defaults: { + [UIFeature.Widgets]: false, + }, + }); + }); + + afterEach(() => { + SdkConfig.reset(); }); it("should show call buttons in a room with 2 members", () => { @@ -268,7 +284,15 @@ describe("RoomHeader", () => { describe("groups call disabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature == UIFeature.Widgets); + SdkConfig.put({ + setting_defaults: { + [UIFeature.Widgets]: true, + }, + }); + }); + + afterEach(() => { + SdkConfig.reset(); }); it("you can't call if you're alone", () => { @@ -333,15 +357,26 @@ describe("RoomHeader", () => { describe("group call enabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation( - (feature) => feature === "feature_group_calls" || feature == UIFeature.Widgets, - ); + SdkConfig.put({ + features: { + feature_group_calls: true, + }, + }); + }); + + afterEach(() => { + SdkConfig.reset(); + jest.restoreAllMocks(); }); it("renders only the video call element", async () => { const user = userEvent.setup(); mockRoomMembers(room, 3); - jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true }); + SdkConfig.add({ + element_call: { + use_exclusively: true, + }, + }); // allow element calls jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); @@ -359,7 +394,11 @@ describe("RoomHeader", () => { }); it("can't call if there's an ongoing (pinned) call", () => { - jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true }); + SdkConfig.add({ + element_call: { + use_exclusively: true, + }, + }); // allow element calls jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true); @@ -377,7 +416,14 @@ describe("RoomHeader", () => { it("clicking on ongoing (unpinned) call re-pins it", async () => { const user = userEvent.setup(); mockRoomMembers(room, 3); - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature == UIFeature.Widgets); + SdkConfig.add({ + setting_defaults: { + [UIFeature.Widgets]: true, + }, + features: { + feature_group_calls: false, + }, + }); // allow calls jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false); @@ -427,8 +473,10 @@ describe("RoomHeader", () => { jest.spyOn(room.currentState, "maySendStateEvent").mockReturnValue(true); jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite); jest.spyOn(room, "canInvite").mockReturnValue(false); - const guestSpaUrlMock = jest.spyOn(SdkConfig, "get").mockImplementation((key) => { - return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" }; + SdkConfig.add({ + element_call: { + guest_spa_url: "https://guest_spa_url.com", + }, }); const { container: containerNoInviteNotPublicCanUpgradeAccess } = render( , @@ -442,8 +490,10 @@ describe("RoomHeader", () => { jest.spyOn(room.currentState, "maySendStateEvent").mockReturnValue(false); jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite); jest.spyOn(room, "canInvite").mockReturnValue(false); - jest.spyOn(SdkConfig, "get").mockImplementation((key) => { - return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" }; + SdkConfig.add({ + element_call: { + guest_spa_url: "https://guest_spa_url.com", + }, }); const { container: containerNoInviteNotPublic } = render(, getWrapper()); expect(queryAllByLabelText(containerNoInviteNotPublic, "There's no one here to call")).toHaveLength(2); @@ -463,8 +513,9 @@ describe("RoomHeader", () => { const { container: containerInvitePublic } = render(, getWrapper()); expect(queryAllByLabelText(containerInvitePublic, "There's no one here to call")).toHaveLength(0); + // Clear guest_spa_url + SdkConfig.reset(); // last we can allow everything but without guest_spa_url nothing will work - guestSpaUrlMock.mockRestore(); const { container: containerAllAllowedButNoGuestSpaUrl } = render(, getWrapper()); expect( queryAllByLabelText(containerAllAllowedButNoGuestSpaUrl, "There's no one here to call"), @@ -643,6 +694,10 @@ describe("RoomHeader", () => { ]); }); + afterEach(() => { + SdkConfig.reset(); + }); + it.each([ [ShieldUtils.E2EStatus.Verified, "Verified"], [ShieldUtils.E2EStatus.Warning, "Untrusted"], @@ -655,6 +710,11 @@ describe("RoomHeader", () => { }); it("does not show the face pile for DMs", () => { + SdkConfig.put({ + features: { + feature_notifications: false, + }, + }); const { asFragment } = render(, getWrapper()); expect(asFragment()).toMatchSnapshot(); @@ -751,7 +811,7 @@ describe("RoomHeader", () => { describe("ask to join enabled", () => { it("does render the RoomKnocksBar", () => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_ask_to_join"); + SettingsStore.setValue("feature_ask_to_join", null, SettingLevel.DEVICE, true); jest.spyOn(room, "canInvite").mockReturnValue(true); jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock); jest.spyOn(room, "getMembersWithMembership").mockReturnValue([new RoomMember(room.roomId, "@foo")]); diff --git a/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap index b0505fac0b..bbcf4cfb4b 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap @@ -55,7 +55,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = ` style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);" >