Remove more usages of singleton store getter in favour of contexts (#34099)

* Expose SDKContextClass via window for debugging

* Remove stores from window if they are exposed via sdkContext

* Avoid usages of global store instance where React context is accessible

* Remove more usages of singleton store getter in favour of contexts

* Remove more usages of singleton store getter in favour of contexts

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests

* Fix tests

* Fix tests

* Iterate

* Fix bad merge

* Iterate

* Fix tests

* Iterate

* Iterate

* Iterate

* Iterate

* Iterate

* Improve coverage

* Improve coverage
This commit is contained in:
Michael Telatynski
2026-07-14 12:58:34 +00:00
committed by GitHub
parent 9575b236c0
commit b5fd249e38
52 changed files with 666 additions and 331 deletions
@@ -10,6 +10,8 @@ import { fireEvent, render, screen } from "jest-matrix-react";
import BaseCard from "../../../../../src/components/views/right_panel/BaseCard.tsx";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore.ts";
import { clientAndSDKContextRenderOptions } from "../../../../test-utils";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
jest.mock("../../../../../src/stores/right-panel/RightPanelStore", () => ({
instance: {
@@ -24,6 +26,7 @@ describe("<BaseCard />", () => {
<BaseCard header="Heading text">
<div>Content</div>
</BaseCard>,
clientAndSDKContextRenderOptions(SDKContextClass.instance.client!, SDKContextClass.instance),
);
expect(screen.getByRole("heading")).toHaveTextContent("Heading text");
@@ -14,27 +14,34 @@ import { MatrixWidgetType } from "matrix-widget-api";
import userEvent from "@testing-library/user-event";
import ExtensionsCard from "../../../../../src/components/views/right_panel/ExtensionsCard";
import { stubClient } from "../../../../test-utils";
import { clientAndSDKContextRenderOptions, stubClient } from "../../../../test-utils";
import { type IApp } from "../../../../../src/stores/WidgetStore";
import WidgetUtils, { useWidgets } from "../../../../../src/utils/WidgetUtils";
import { WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
import { IntegrationManagers } from "../../../../../src/integrations/IntegrationManagers";
import { TestSDKContext } from "../../../TestSDKContext.ts";
jest.mock("../../../../../src/utils/WidgetUtils");
describe("<ExtensionsCard />", () => {
let client: Mocked<MatrixClient>;
let room: Room;
let sdkContext: TestSDKContext;
beforeEach(() => {
client = mocked(stubClient());
sdkContext = new TestSDKContext();
sdkContext._client = client;
room = new Room("!room:server", client, client.getSafeUserId());
mocked(WidgetUtils.getWidgetName).mockImplementation((app) => app?.name ?? "No Name");
});
it("should render empty state", () => {
mocked(useWidgets).mockReturnValue([]);
const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
const { asFragment } = render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Boost productivity with more tools, widgets and bots")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
@@ -61,7 +68,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
const { asFragment } = render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Custom Widget")).toBeInTheDocument();
expect(screen.getByText("Jitsi")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
@@ -81,7 +91,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
const { container } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
const { container } = render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await userEvent.click(container.querySelector(".mx_ExtensionsCard_app_options")!);
expect(document.querySelector(".mx_IconizedContextMenu")).toMatchSnapshot();
});
@@ -100,7 +113,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Set layout for everyone")).toBeInTheDocument();
});
@@ -118,7 +134,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Custom Widget").closest(".mx_ExtensionsCard_Button_pinned")).toBeInTheDocument();
});
@@ -137,15 +156,52 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByLabelText("You can only pin up to 3 widgets")).toBeInTheDocument();
});
it("should should open integration manager on click", async () => {
jest.spyOn(IntegrationManagers.sharedInstance(), "hasManager").mockReturnValue(false);
const spy = jest.spyOn(IntegrationManagers.sharedInstance(), "openNoManagerDialog");
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await userEvent.click(screen.getByText("Add extensions"));
expect(spy).toHaveBeenCalled();
});
it("should set room layout on click", async () => {
mocked(useWidgets).mockReturnValue([
{
id: "id",
roomId: room.roomId,
eventId: "$event1",
creatorUserId: client.getSafeUserId(),
type: MatrixWidgetType.Custom,
name: "Custom Widget",
url: "http://url1",
},
{
id: "jitsi",
roomId: room.roomId,
eventId: "$event2",
creatorUserId: client.getSafeUserId(),
type: MatrixWidgetType.JitsiMeet,
name: "Jitsi",
url: "http://jitsi",
},
] satisfies IApp[]);
jest.spyOn(sdkContext.widgetLayoutStore, "copyLayoutToRoom");
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await userEvent.click(screen.getByText("Set layout for everyone"));
expect(sdkContext.widgetLayoutStore.copyLayoutToRoom).toHaveBeenCalledWith(room);
});
});
@@ -27,13 +27,19 @@ import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEven
import { sleep } from "matrix-js-sdk/src/utils";
import userEvent from "@testing-library/user-event";
import { stubClient, mkEvent, mkMessage, flushPromises } from "../../../../test-utils";
import {
stubClient,
mkEvent,
mkMessage,
flushPromises,
clientAndSDKContextRenderOptions,
} from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { PinnedMessagesCard } from "../../../../../src/components/views/right_panel/PinnedMessagesCard";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import Modal from "../../../../../src/Modal";
import { UnpinAllDialog } from "../../../../../src/components/views/dialogs/UnpinAllDialog";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
describe("<PinnedMessagesCard />", () => {
let cli: MockedObject<MatrixClient>;
@@ -86,13 +92,12 @@ describe("<PinnedMessagesCard />", () => {
async function renderMessagePinList(room: Room): Promise<RenderResult> {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>
</MatrixClientContext.Provider>,
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
// Wait a tick for state updates
await act(() => sleep(0));
@@ -170,13 +175,12 @@ describe("<PinnedMessagesCard />", () => {
it("should show spinner whilst loading", async () => {
const room = mkRoom([], [pin1]);
render(
<MatrixClientContext.Provider value={cli}>
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>
</MatrixClientContext.Provider>,
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
@@ -322,13 +326,12 @@ describe("<PinnedMessagesCard />", () => {
).mockReturnValue(false);
const { asFragment } = render(
<MatrixClientContext.Provider value={cli}>
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>
</MatrixClientContext.Provider>,
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
// Wait a tick for state updates
@@ -23,6 +23,8 @@ import {
useRoomSummaryCardViewModel,
} from "../../../../../src/components/viewmodels/right_panel/RoomSummaryCardViewModel";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
// Mock the viewmodel hooks
jest.mock("../../../../../src/components/viewmodels/right_panel/RoomSummaryCardViewModel", () => ({
@@ -45,9 +47,11 @@ describe("<RoomSummaryCard />", () => {
return render(<RoomSummaryCardView {...defaultProps} {...props} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>
<LinkedTextContext.Provider value={{}}>{children}</LinkedTextContext.Provider>
</MatrixClientContext.Provider>
<SDKContext.Provider value={SDKContextClass.instance}>
<MatrixClientContext.Provider value={mockClient}>
<LinkedTextContext.Provider value={{}}>{children}</LinkedTextContext.Provider>
</MatrixClientContext.Provider>
</SDKContext.Provider>
),
});
};
@@ -32,12 +32,12 @@ import UserInfo, { disambiguateDevices } from "../../../../../src/components/vie
import { getPowerLevels } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../src/Modal";
import { clearAllModals, flushPromises } from "../../../../test-utils";
import { clearAllModals, clientAndSDKContextRenderOptions, flushPromises } from "../../../../test-utils";
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../../src/settings/UIFeature";
import { TestSDKContext } from "../../../TestSDKContext.ts";
jest.mock("../../../../../src/utils/direct-messages", () => ({
...jest.requireActual("../../../../../src/utils/direct-messages"),
@@ -78,6 +78,7 @@ const defaultUser = new User(defaultUserId);
let mockRoom: Mocked<Room>;
let mockClient: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
let sdkContext: TestSDKContext;
const origDate = global.Date.prototype.toLocaleString;
beforeEach(() => {
@@ -131,6 +132,8 @@ beforeEach(() => {
setPowerLevel: jest.fn(),
getCrypto: jest.fn().mockReturnValue(mockCrypto),
} as unknown as MatrixClient);
sdkContext = new TestSDKContext();
sdkContext._client = mockClient;
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
@@ -162,13 +165,10 @@ describe("<UserInfo />", () => {
};
const renderComponent = (props = {}) => {
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<UserInfo {...defaultProps} {...props} />, {
wrapper: Wrapper,
});
return render(
<UserInfo {...defaultProps} {...props} />,
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};
beforeEach(() => {
@@ -307,11 +307,7 @@ describe("<UserInfo />", () => {
});
it("renders the message button", () => {
render(
<MatrixClientContext.Provider value={mockClient}>
<UserInfo {...defaultProps} />
</MatrixClientContext.Provider>,
);
render(<UserInfo {...defaultProps} />, clientAndSDKContextRenderOptions(mockClient, sdkContext));
screen.getByRole("button", { name: "Send message" });
});
@@ -322,11 +318,7 @@ describe("<UserInfo />", () => {
return component !== UIComponent.CreateRooms;
},
() => {
render(
<MatrixClientContext.Provider value={mockClient}>
<UserInfo {...defaultProps} />
</MatrixClientContext.Provider>,
);
render(<UserInfo {...defaultProps} />, clientAndSDKContextRenderOptions(mockClient, sdkContext));
expect(screen.queryByRole("button", { name: "Message" })).toBeNull();
},
@@ -13,9 +13,9 @@ import { render, waitFor, screen } from "jest-matrix-react";
import React from "react";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { UserInfoHeaderVerificationView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView";
import { createTestClient } from "../../../../../test-utils";
import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils";
import { TestSDKContext } from "../../../../TestSDKContext.ts";
describe("<UserInfoHeaderVerificationView />", () => {
const defaultRoomId = "!fkfk";
@@ -25,6 +25,7 @@ describe("<UserInfoHeaderVerificationView />", () => {
let mockClient: MatrixClient;
let mockCrypto: Mocked<CryptoApi>;
let sdkContext: TestSDKContext;
beforeEach(() => {
mockCrypto = mocked({
@@ -42,6 +43,8 @@ describe("<UserInfoHeaderVerificationView />", () => {
} as unknown as CryptoApi);
mockClient = createTestClient();
sdkContext = new TestSDKContext();
sdkContext._client = mockClient;
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto);
@@ -62,13 +65,11 @@ describe("<UserInfoHeaderVerificationView />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<UserInfoHeaderVerificationView member={defaultMember} devices={[device1]} />, {
wrapper: Wrapper,
});
return render(
<UserInfoHeaderVerificationView member={defaultMember} devices={[device1]} />,
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};
it("renders verified badge when user is verified", async () => {
@@ -13,10 +13,10 @@ import { fireEvent, render, screen } from "jest-matrix-react";
import React from "react";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { UserInfoHeaderView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView";
import { createTestClient } from "../../../../../test-utils";
import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils";
import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
import { TestSDKContext } from "../../../../TestSDKContext.ts";
// Mock the viewmodel hooks
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({
@@ -45,6 +45,7 @@ describe("<UserInfoHeaderView />", () => {
let mockClient: MatrixClient;
let mockCrypto: Mocked<CryptoApi>;
let sdkContext: TestSDKContext;
beforeEach(() => {
mockCrypto = mocked({
@@ -63,6 +64,8 @@ describe("<UserInfoHeaderView />", () => {
mockClient = createTestClient();
mockClient.doesServerSupportExtendedProfiles = () => Promise.resolve(false);
sdkContext = new TestSDKContext();
sdkContext._client = mockClient;
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
@@ -90,10 +93,6 @@ describe("<UserInfoHeaderView />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(
<UserInfoHeaderView
{...defaultProps}
@@ -101,9 +100,7 @@ describe("<UserInfoHeaderView />", () => {
devices={[device1]}
hideVerificationSection={props.hideVerificationSection}
/>,
{
wrapper: Wrapper,
},
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};