From dc33d736a06b9c7f18258a059690077af9871167 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jul 2026 17:09:21 +0100 Subject: [PATCH] Handle SDKContextClass `client` initialisation internally (#34146) * Handle SDKContextClass `client` initialisation internally Rather than via MatrixChat - this is predominantly for Lifecycle tests as they don't use a MatrixChat and it doesn't make much sense for this component to own this state. * Fix tests --- apps/web/src/Lifecycle.ts | 7 +++--- .../src/components/structures/MatrixChat.tsx | 1 - apps/web/src/contexts/SDKContextClass.ts | 22 ++++++++++++++++--- apps/web/src/dispatcher/actions.ts | 2 +- apps/web/src/dispatcher/payloads.ts | 9 ++++++++ .../dispatcher/payloads/OnLoggedInPayload.ts | 17 ++++++++++++++ apps/web/test/unit-tests/TestSDKContext.ts | 3 +++ .../structures/LoggedInView-test.tsx | 2 +- .../structures/PipContainer-test.tsx | 2 +- .../components/structures/RightPanel-test.tsx | 8 +++---- .../components/structures/RoomView-test.tsx | 8 +++---- .../views/dialogs/InviteDialog-test.tsx | 8 +++++-- .../views/dialogs/RoomSettingsDialog-test.tsx | 8 +++---- .../views/dialogs/UserSettingsDialog-test.tsx | 8 +++---- .../components/views/elements/Pill-test.tsx | 8 +++++-- .../RoomHeader/CallGuestLinkButton-test.tsx | 16 +++++++------- .../RoomHeader/VideoRoomChatButton-test.tsx | 8 +++---- .../views/rooms/memberlist/common.tsx | 2 +- .../tabs/user/AccountUserSettingsTab-test.tsx | 8 +++---- .../user/SecurityUserSettingsTab-test.tsx | 6 ++--- .../tabs/user/SessionManagerTab-test.tsx | 8 +++---- .../views/spaces/SpacePanel-test.tsx | 7 +++--- .../unit-tests/contexts/SdkContext-test.ts | 8 ++++--- .../unit-tests/modules/ExtrasApi-test.tsx | 8 +++---- .../unit-tests/stores/MemberListStore-test.ts | 2 +- .../unit-tests/stores/RoomViewStore-test.ts | 2 +- .../unit-tests/stores/TypingStore-test.ts | 2 +- .../widgets/WidgetPermissionStore-test.ts | 2 +- .../utils/exportUtils/HTMLExport-test.ts | 7 +++++- .../menus/UserMenuViewModel-test.ts | 9 ++++++-- 30 files changed, 137 insertions(+), 71 deletions(-) create mode 100644 apps/web/src/dispatcher/payloads/OnLoggedInPayload.ts diff --git a/apps/web/src/Lifecycle.ts b/apps/web/src/Lifecycle.ts index cb2fff4b36..15e19c8b64 100644 --- a/apps/web/src/Lifecycle.ts +++ b/apps/web/src/Lifecycle.ts @@ -81,6 +81,7 @@ import { import { TokenRefresher } from "./utils/oidc/TokenRefresher"; import { checkBrowserSupport } from "./SupportedBrowser"; import { type URLParams } from "./vector/url_utils.ts"; +import { type OnLoggedInPayload } from "./dispatcher/payloads/OnLoggedInPayload.ts"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -906,9 +907,9 @@ async function doSetLoggedIn( } checkSessionLock(); - // We are now logged in, so fire this. We have yet to start the client but the - // client_started dispatch is for that. - dis.fire(Action.OnLoggedIn); + // We are now logged in, so fire this. We have yet to start the client but the client_started dispatch is for that. + // Dispatch this synchronously so SDKContextClass can set the client for other modules to consume. + dis.dispatch({ action: Action.OnLoggedIn, client }, true); const clientPegOpts: MatrixClientPegAssignOpts = {}; if (credentials.pickleKey) { diff --git a/apps/web/src/components/structures/MatrixChat.tsx b/apps/web/src/components/structures/MatrixChat.tsx index 71e8740c53..14db9e53ce 100644 --- a/apps/web/src/components/structures/MatrixChat.tsx +++ b/apps/web/src/components/structures/MatrixChat.tsx @@ -1521,7 +1521,6 @@ export default class MatrixChat extends React.PureComponent { * Handle an {@link Action.OnLoggedIn} action (i.e, we now have a client with working credentials). */ private onLoggedIn(): void { - this.stores.client = MatrixClientPeg.safeGet(); StorageManager.tryPersistStorage(); // If we're loading the app for the first time, we can now transition to a splash screen while we wait for the diff --git a/apps/web/src/contexts/SDKContextClass.ts b/apps/web/src/contexts/SDKContextClass.ts index 4b60a734ec..a6618362ab 100644 --- a/apps/web/src/contexts/SDKContextClass.ts +++ b/apps/web/src/contexts/SDKContextClass.ts @@ -26,6 +26,9 @@ import { OidcClientStore } from "../stores/oidc/OidcClientStore"; import WidgetStore from "../stores/WidgetStore"; import ResizeNotifier from "../utils/ResizeNotifier"; import { MultiRoomViewStore } from "../stores/MultiRoomViewStore"; +import { type ActionPayload, isAction } from "../dispatcher/payloads.ts"; +import { Action } from "../dispatcher/actions.ts"; +import { type OnLoggedInPayload } from "../dispatcher/payloads/OnLoggedInPayload.ts"; /** * A class which (mostly) lazily initialises stores as and when they are requested, ensuring they remain @@ -43,11 +46,13 @@ export class SDKContextClass { */ public static readonly instance = new SDKContextClass(); - // Optional as we don't have a client on initial load if unregistered. This should be set - // when the MatrixClient is first acquired in the dispatcher event Action.OnLoggedIn. + // Optional as we don't have a client on initial load if unregistered. // It is only safe to set this once, as updating this value will NOT notify components using // this Context. - public client?: MatrixClient; + protected _client?: MatrixClient; + public get client(): MatrixClient | undefined { + return this._client; + } // All protected fields to make it easier to derive test stores protected _WidgetPermissionStore?: WidgetPermissionStore; @@ -67,6 +72,16 @@ export class SDKContextClass { protected _ResizeNotifier?: ResizeNotifier; protected _MultiRoomViewStore?: MultiRoomViewStore; + public constructor() { + defaultDispatcher.register(this.onDispatch); + } + + private onDispatch = (payload: ActionPayload): void => { + if (isAction(payload, Action.OnLoggedIn)) { + this._client = payload.client; + } + }; + /** * Automatically construct stores which need to be created eagerly so they can register with * the dispatcher. @@ -193,5 +208,6 @@ export class SDKContextClass { public onLoggedOut(): void { this._UserProfilesStore = undefined; this._OidcClientStore = undefined; + this._client = undefined; } } diff --git a/apps/web/src/dispatcher/actions.ts b/apps/web/src/dispatcher/actions.ts index ec61e65391..cafee196ff 100644 --- a/apps/web/src/dispatcher/actions.ts +++ b/apps/web/src/dispatcher/actions.ts @@ -335,7 +335,7 @@ export enum Action { * access token from local storage). Note that this does not necessarily mean that a login action has happened, * just that authentication creds have been set up. * - * No additional payload information required. + * Use with a OnLoggedInPayload. */ OnLoggedIn = "on_logged_in", diff --git a/apps/web/src/dispatcher/payloads.ts b/apps/web/src/dispatcher/payloads.ts index 248a486a07..b632392db5 100644 --- a/apps/web/src/dispatcher/payloads.ts +++ b/apps/web/src/dispatcher/payloads.ts @@ -16,6 +16,15 @@ export interface ActionPayload { action: DispatcherAction; } +/** + * Type guard to check if a payload is of a specific action type. + * @param payload - the incoming payload to check + * @param action - the action to return true for + */ +export function isAction

(payload: ActionPayload, action: P["action"]): payload is P { + return payload.action === action; +} + /** * The function the dispatcher calls when ready for an AsyncActionPayload. The * single argument is used to start a dispatch. First the dispatcher calls the diff --git a/apps/web/src/dispatcher/payloads/OnLoggedInPayload.ts b/apps/web/src/dispatcher/payloads/OnLoggedInPayload.ts new file mode 100644 index 0000000000..18eca59ae0 --- /dev/null +++ b/apps/web/src/dispatcher/payloads/OnLoggedInPayload.ts @@ -0,0 +1,17 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { type ActionPayload } from "../payloads"; +import { type Action } from "../actions"; + +export interface OnLoggedInPayload extends ActionPayload { + action: Action.OnLoggedIn; + + client: MatrixClient; +} diff --git a/apps/web/test/unit-tests/TestSDKContext.ts b/apps/web/test/unit-tests/TestSDKContext.ts index 6f499a4b5b..26299944a5 100644 --- a/apps/web/test/unit-tests/TestSDKContext.ts +++ b/apps/web/test/unit-tests/TestSDKContext.ts @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; + import { SDKContextClass } from "../../src/contexts/SDKContextClass"; import { type PosthogAnalytics } from "../../src/PosthogAnalytics"; import { type SlidingSyncManager } from "../../src/SlidingSyncManager"; @@ -22,6 +24,7 @@ import type WidgetStore from "../../src/stores/WidgetStore"; * replace individual stores. This is useful for tests which need to mock out stores. */ export class TestSDKContext extends SDKContextClass { + declare public _client?: MatrixClient; declare public _RightPanelStore?: RightPanelStore; declare public _RoomNotificationStateStore?: RoomNotificationStateStore; declare public _RoomViewStore?: RoomViewStore; diff --git a/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx b/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx index 3977c9da73..c55572e197 100644 --- a/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx +++ b/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx @@ -85,7 +85,7 @@ describe("", () => { mockClient.setPushRuleActions.mockReset().mockResolvedValue({}); // @ts-expect-error mockClient.pushProcessor = new PushProcessor(mockClient); - mockSdkContext.client = mockClient; + mockSdkContext._client = mockClient; }); describe("synced push rules", () => { diff --git a/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx b/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx index c77c802cc5..8c7a7684f5 100644 --- a/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx @@ -112,7 +112,7 @@ describe("PipContainer", () => { sdkContext = new TestSDKContext(); // @ts-ignore PipContainer uses SDKContext in the constructor SDKContextClass.instance = sdkContext; - sdkContext.client = client; + sdkContext._client = client; }); afterEach(async () => { diff --git a/apps/web/test/unit-tests/components/structures/RightPanel-test.tsx b/apps/web/test/unit-tests/components/structures/RightPanel-test.tsx index e14150e700..c163ad2f4e 100644 --- a/apps/web/test/unit-tests/components/structures/RightPanel-test.tsx +++ b/apps/web/test/unit-tests/components/structures/RightPanel-test.tsx @@ -23,8 +23,8 @@ import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelS import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore"; import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore"; import { WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore"; -import { SDKContextClass } from "../../../../src/contexts/SDKContextClass"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; +import { TestSDKContext } from "../../TestSDKContext.ts"; const RightPanelBase = wrapInMatrixClientContext(_RightPanel); @@ -32,14 +32,14 @@ describe("RightPanel", () => { const resizeNotifier = new ResizeNotifier(); let cli: MockedObject; - let context: SDKContextClass; + let context: TestSDKContext; let RightPanel: React.ComponentType>; beforeEach(() => { stubClient(); cli = mocked(MatrixClientPeg.safeGet()); DMRoomMap.makeShared(cli); - context = new SDKContextClass(); - context.client = cli; + context = new TestSDKContext(); + context._client = cli; RightPanel = wrapInSdkContext(RightPanelBase, context); }); diff --git a/apps/web/test/unit-tests/components/structures/RoomView-test.tsx b/apps/web/test/unit-tests/components/structures/RoomView-test.tsx index 235947d592..d37f128795 100644 --- a/apps/web/test/unit-tests/components/structures/RoomView-test.tsx +++ b/apps/web/test/unit-tests/components/structures/RoomView-test.tsx @@ -56,7 +56,7 @@ import { DirectoryMember } from "../../../../src/utils/direct-messages"; import { createDmLocalRoom } from "../../../../src/utils/dm/createDmLocalRoom"; import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore"; import { SDKContext } from "../../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../TestSDKContext.ts"; import WidgetUtils from "../../../../src/utils/WidgetUtils"; import { WidgetType } from "../../../../src/widgets/WidgetType"; import WidgetStore from "../../../../src/stores/WidgetStore"; @@ -86,7 +86,7 @@ describe("RoomView", () => { let cli: MockedObject; let room: Room; let rooms: Map; - let stores: SDKContextClass; + let stores: TestSDKContext; let crypto: CryptoApi; // mute some noise @@ -111,8 +111,8 @@ describe("RoomView", () => { room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args)); DMRoomMap.makeShared(cli); - stores = new SDKContextClass(); - stores.client = cli; + stores = new TestSDKContext(); + stores._client = cli; stores.rightPanelStore.useUnitTestClient(cli); crypto = cli.getCrypto()!; diff --git a/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx index 219556abc2..1c603ab165 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx @@ -33,6 +33,7 @@ import { type IConfigOptions } from "../../../../../src/IConfigOptions"; import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass"; import { type IProfileInfo } from "../../../../../src/hooks/useProfileInfo"; import { DirectoryMember, startDmOnFirstMessage } from "../../../../../src/utils/direct-messages"; +import { TestSDKContext } from "../../../TestSDKContext.ts"; const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken"); jest.mock("../../../../../src/IdentityAuthClient", () => @@ -94,6 +95,7 @@ const bobProfileInfo: IProfileInfo = { describe("InviteDialog", () => { let mockClient: Mocked; let room: Room; + let sdkContext: TestSDKContext; filterConsole( "Error retrieving profile for userId @carol:example.com", @@ -178,13 +180,15 @@ describe("InviteDialog", () => { mockClient.getRooms.mockReturnValue([room]); mockClient.getRoom.mockReturnValue(room); - SDKContextClass.instance.client = mockClient; + sdkContext = new TestSDKContext(); + // @ts-ignore UserMenuViewModel uses SDKContext in the constructor + SDKContextClass.instance = sdkContext; + sdkContext._client = mockClient; }); afterEach(async () => { await clearAllModals(); SDKContextClass.instance.onLoggedOut(); - SDKContextClass.instance.client = undefined; }); afterAll(() => { diff --git a/apps/web/test/unit-tests/components/views/dialogs/RoomSettingsDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/RoomSettingsDialog-test.tsx index a91812a92e..5240bfbe5b 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/RoomSettingsDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/RoomSettingsDialog-test.tsx @@ -24,7 +24,7 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext import SettingsStore from "../../../../../src/settings/SettingsStore"; import { UIFeature } from "../../../../../src/settings/UIFeature"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; -import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../TestSDKContext.ts"; describe("", () => { const userId = "@alice:server.org"; @@ -44,7 +44,7 @@ describe("", () => { const room2 = new Room("!room2:server.org", mockClient, userId); room2.name = "Another Room"; - let sdkContext: SDKContextClass; + let sdkContext: TestSDKContext; jest.spyOn(SettingsStore, "getValue"); @@ -57,8 +57,8 @@ describe("", () => { return null; }); - sdkContext = new SDKContextClass(); - sdkContext.client = mockClient; + sdkContext = new TestSDKContext(); + sdkContext._client = mockClient; jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false); diff --git a/apps/web/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx index a68cbdac90..cb2b761757 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx @@ -26,7 +26,7 @@ import { } from "../../../../test-utils"; import { UIFeature } from "../../../../../src/settings/UIFeature"; import { SettingLevel } from "../../../../../src/settings/SettingLevel"; -import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../TestSDKContext.ts"; import { type FeatureSettingKey } from "../../../../../src/settings/Settings.tsx"; import { mockOpenIdConfiguration } from "../../../../test-utils/oidc.ts"; @@ -57,7 +57,7 @@ describe("", () => { const mockSettingsStore = mocked(SettingsStore); let mockClient!: MockedObject; - let sdkContext: SDKContextClass; + let sdkContext: TestSDKContext; const defaultProps = { onFinished: jest.fn() }; const getComponent = ( props: Partial }> = {}, @@ -76,8 +76,8 @@ describe("", () => { getMediaConfig: jest.fn(), getAuthMetadata: jest.fn().mockResolvedValue(mockOpenIdConfiguration()), }); - sdkContext = new SDKContextClass(); - sdkContext.client = mockClient; + sdkContext = new TestSDKContext(); + sdkContext._client = mockClient; mockSettingsStore.getValue.mockReturnValue(false); mockSettingsStore.getValueAt.mockReturnValue(false); mockSettingsStore.getFeatureSettingNames.mockReturnValue([]); diff --git a/apps/web/test/unit-tests/components/views/elements/Pill-test.tsx b/apps/web/test/unit-tests/components/views/elements/Pill-test.tsx index 630e70ec2c..79251c9687 100644 --- a/apps/web/test/unit-tests/components/views/elements/Pill-test.tsx +++ b/apps/web/test/unit-tests/components/views/elements/Pill-test.tsx @@ -48,11 +48,12 @@ describe("", () => { const user3Id = "@user3:example.com"; let renderResult: RenderResult; let pillParentClickHandler: (e: ButtonEvent) => void; + let sdkContext: TestSDKContext; const renderPill = (props: PillProps): void => { const cli = MatrixClientPeg.safeGet(); const mockSdkContext = new TestSDKContext(); - mockSdkContext.client = cli; + mockSdkContext._client = cli; const withDefault = { inMessage: true, @@ -79,7 +80,10 @@ describe("", () => { beforeEach(() => { client = mocked(stubClient()); - SDKContextClass.instance.client = client; + sdkContext = new TestSDKContext(); + // @ts-ignore Pill uses the SDKContext global + SDKContextClass.instance = sdkContext; + sdkContext._client = client; DMRoomMap.makeShared(client); room1 = new Room(room1Id, client, user1Id); room1.name = "Room 1"; diff --git a/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx b/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx index 73da646362..59572e0d96 100644 --- a/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx @@ -12,7 +12,7 @@ import { type EventTimeline, JoinRule, Room } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { SDKContext } from "../../../../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../../TestSDKContext.ts"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils"; import { CallGuestLinkButton, @@ -26,7 +26,7 @@ import SettingsStore from "../../../../../../src/settings/SettingsStore"; describe("", () => { const roomId = "!room:server.org"; - let sdkContext!: SDKContextClass; + let sdkContext!: TestSDKContext; let modalSpy: jest.SpyInstance; let modalResolve: (value: unknown[] | PromiseLike) => void; let room: Room; @@ -78,8 +78,8 @@ describe("", () => { ...mockClientMethodsUser(), sendStateEvent: jest.fn(), }); - sdkContext = new SDKContextClass(); - sdkContext.client = client; + sdkContext = new TestSDKContext(); + sdkContext._client = client; const modalPromise = new Promise((resolve) => { modalResolve = resolve; }); @@ -94,7 +94,7 @@ describe("", () => { return oldGet(key); }); jest.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(true); - jest.spyOn(SDKContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); + jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true); }); afterEach(() => { jest.restoreAllMocks(); @@ -152,7 +152,7 @@ describe("", () => { it("don't show external conference button if now guest spa link is configured", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); - jest.spyOn(SDKContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); + jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true); jest.spyOn(SdkConfig, "get").mockImplementation((key) => { if (key === "element_call") { @@ -179,7 +179,7 @@ describe("", () => { it("opens the share dialog with the correct share link in an encrypted room", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); - jest.spyOn(SDKContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); + jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true); getComponent(room); const modalSpy = jest.spyOn(Modal, "createDialog"); @@ -201,7 +201,7 @@ describe("", () => { it("share dialog has correct link in an unencrypted room", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); jest.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(false); - jest.spyOn(SDKContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); + jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true); getComponent(room); const modalSpy = jest.spyOn(Modal, "createDialog"); diff --git a/apps/web/test/unit-tests/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx b/apps/web/test/unit-tests/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx index abaad76150..5a31ddd4c0 100644 --- a/apps/web/test/unit-tests/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx @@ -13,7 +13,7 @@ import { fireEvent, render, screen, waitFor } from "jest-matrix-react"; import { VideoRoomChatButton } from "../../../../../../src/components/views/rooms/RoomHeader/VideoRoomChatButton"; import { SDKContext } from "../../../../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../../TestSDKContext.ts"; import type RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils"; import { RoomNotificationState } from "../../../../../../src/stores/notifications/RoomNotificationState"; @@ -23,7 +23,7 @@ import { RightPanelPhases } from "../../../../../../src/stores/right-panel/Right describe("", () => { const roomId = "!room:server.org"; - let sdkContext!: SDKContextClass; + let sdkContext!: TestSDKContext; let rightPanelStore!: MockedObject; /** @@ -59,8 +59,8 @@ describe("", () => { rightPanelStore = { showOrHidePhase: jest.fn(), } as unknown as MockedObject; - sdkContext = new SDKContextClass(); - sdkContext.client = client; + sdkContext = new TestSDKContext(); + sdkContext._client = client; jest.spyOn(sdkContext, "rightPanelStore", "get").mockReturnValue(rightPanelStore); }); diff --git a/apps/web/test/unit-tests/components/views/rooms/memberlist/common.tsx b/apps/web/test/unit-tests/components/views/rooms/memberlist/common.tsx index e0dd44b495..72bd2b3674 100644 --- a/apps/web/test/unit-tests/components/views/rooms/memberlist/common.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/memberlist/common.tsx @@ -122,7 +122,7 @@ export async function renderMemberList( } const context = new TestSDKContext(); - context.client = client; + context._client = client; context.memberListStore.isPresenceEnabled = jest.fn().mockReturnValue(enablePresence); const root = render( diff --git a/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx b/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx index e1eca9e8b0..7106032762 100644 --- a/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx @@ -16,7 +16,7 @@ import { ToastContext, ToastRack } from "@element-hq/web-shared-components"; import AccountUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab"; import { SDKContext } from "../../../../../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../../../TestSDKContext.ts"; import SettingsStore from "../../../../../../../src/settings/SettingsStore"; import { getMockClientWithEventEmitter, @@ -51,7 +51,7 @@ describe("", () => { const userId = "@alice:server.org"; let mockClient: MockedObject; - let stores: SDKContextClass; + let stores: TestSDKContext; const getComponent = () => ( @@ -87,8 +87,8 @@ describe("", () => { id_server_unbind_result: "success", }); - stores = new SDKContextClass(); - stores.client = mockClient; + stores = new TestSDKContext(); + stores._client = mockClient; // stub out this store completely to avoid mocking initialisation const mockOidcClientStore = {} as unknown as OidcClientStore; jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); diff --git a/apps/web/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/apps/web/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx index 7ae2ccdaa4..b46531773a 100644 --- a/apps/web/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -20,7 +20,7 @@ import { mockPlatformPeg, } from "../../../../../../test-utils"; import { SDKContext } from "../../../../../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../../../TestSDKContext.ts"; import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher"; import { UIFeature } from "../../../../../../../src/settings/UIFeature"; import SettingsStore from "../../../../../../../src/settings/SettingsStore"; @@ -42,8 +42,8 @@ describe("", () => { setIgnoredUsers, }); - const sdkContext = new SDKContextClass(); - sdkContext.client = mockClient; + const sdkContext = new TestSDKContext(); + sdkContext._client = mockClient; const getComponent = () => ( diff --git a/apps/web/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/apps/web/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 35a4d04d3f..75160f6ce6 100644 --- a/apps/web/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -57,7 +57,7 @@ import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../../src/components/view import SettingsStore from "../../../../../../../src/settings/SettingsStore"; import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation"; import { SDKContext } from "../../../../../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../../../TestSDKContext.ts"; import { type OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore"; import { makeDelegatedAuthConfig } from "../../../../../../test-utils/oidc"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; @@ -134,7 +134,7 @@ describe("", () => { } as unknown as CryptoApi); let mockClient!: MockedObject; - let sdkContext: SDKContextClass; + let sdkContext: TestSDKContext; const defaultProps = {}; const getComponent = (props = {}): React.ReactElement => ( @@ -250,8 +250,8 @@ describe("", () => { } }); - sdkContext = new SDKContextClass(); - sdkContext.client = mockClient; + sdkContext = new TestSDKContext(); + sdkContext._client = mockClient; // @ts-ignore allow delete of non-optional prop delete window.location; diff --git a/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx b/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx index 1970e7505a..48a6353848 100644 --- a/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx +++ b/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx @@ -17,7 +17,7 @@ import { MetaSpace, type SpaceKey } from "../../../../../src/stores/spaces"; import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; import { UIComponent } from "../../../../../src/settings/UIFeature"; import { mkStubRoom, wrapInMatrixClientContext, wrapInSdkContext } from "../../../../test-utils"; -import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../../TestSDKContext.ts"; import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { type SpaceNotificationState } from "../../../../../src/stores/notifications/SpaceNotificationState"; @@ -124,12 +124,13 @@ describe("", () => { isVersionSupported: jest.fn().mockResolvedValue(true), doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false), } as unknown as MatrixClient; - const SpacePanel = wrapInSdkContext(wrapInMatrixClientContext(UnwrappedSpacePanel), SDKContextClass.instance); + const sdkContext = new TestSDKContext(); + const SpacePanel = wrapInSdkContext(wrapInMatrixClientContext(UnwrappedSpacePanel), sdkContext); beforeAll(() => { jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); - SDKContextClass.instance.client = mockClient; + sdkContext._client = mockClient; }); beforeEach(() => { diff --git a/apps/web/test/unit-tests/contexts/SdkContext-test.ts b/apps/web/test/unit-tests/contexts/SdkContext-test.ts index dc0cb3208e..153d9c6f45 100644 --- a/apps/web/test/unit-tests/contexts/SdkContext-test.ts +++ b/apps/web/test/unit-tests/contexts/SdkContext-test.ts @@ -12,9 +12,10 @@ import { SDKContextClass } from "../../../src/contexts/SDKContextClass"; import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore"; import { UserProfilesStore } from "../../../src/stores/UserProfilesStore"; import { createTestClient } from "../../test-utils"; +import { TestSDKContext } from "../TestSDKContext.ts"; describe("SDKContextClass", () => { - let sdkContext = SDKContextClass.instance; + let sdkContext: TestSDKContext; let client: MatrixClient; beforeAll(() => { @@ -22,7 +23,7 @@ describe("SDKContextClass", () => { }); beforeEach(() => { - sdkContext = new SDKContextClass(); + sdkContext = new TestSDKContext(); }); it("instance should always return the same instance", () => { @@ -40,7 +41,7 @@ describe("SDKContextClass", () => { describe("when SDKContext has a client", () => { beforeEach(() => { - sdkContext.client = client; + sdkContext._client = client; }); it("userProfilesStore should return a UserProfilesStore", () => { @@ -53,6 +54,7 @@ describe("SDKContextClass", () => { it("onLoggedOut should clear the UserProfilesStore", () => { const store = sdkContext.userProfilesStore; sdkContext.onLoggedOut(); + sdkContext._client = client; expect(sdkContext.userProfilesStore).not.toBe(store); }); diff --git a/apps/web/test/unit-tests/modules/ExtrasApi-test.tsx b/apps/web/test/unit-tests/modules/ExtrasApi-test.tsx index 5edc12464c..cb895d2b6a 100644 --- a/apps/web/test/unit-tests/modules/ExtrasApi-test.tsx +++ b/apps/web/test/unit-tests/modules/ExtrasApi-test.tsx @@ -13,7 +13,7 @@ import { EventEmitter } from "events"; import { stubClient } from "../../test-utils"; import DMRoomMap from "../../../src/utils/DMRoomMap"; import { SDKContext } from "../../../src/contexts/SDKContext"; -import { SDKContextClass } from "../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../TestSDKContext.ts"; import { ScopedRoomContextProvider } from "../../../src/contexts/ScopedRoomContext"; import RoomContext, { type RoomContextType } from "../../../src/contexts/RoomContext"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; @@ -22,7 +22,7 @@ import { ModuleApi } from "../../../src/modules/Api"; describe("ExtrasApi", () => { let client: MatrixClient; - let sdkContext: SDKContextClass; + let sdkContext: TestSDKContext; let room: Room; let roomContext: RoomContextType; @@ -31,8 +31,8 @@ describe("ExtrasApi", () => { room = new Room("!test:room", client, "@alice:example.org", { pendingEventOrdering: PendingEventOrdering.Detached, }); - sdkContext = new SDKContextClass(); - sdkContext.client = client; + sdkContext = new TestSDKContext(); + sdkContext._client = client; jest.spyOn(sdkContext.roomViewStore, "getRoomId").mockReturnValue(room.roomId); const mockRoomViewStore = new (class extends EventEmitter { diff --git a/apps/web/test/unit-tests/stores/MemberListStore-test.ts b/apps/web/test/unit-tests/stores/MemberListStore-test.ts index fac5f438f7..1c6f5375f3 100644 --- a/apps/web/test/unit-tests/stores/MemberListStore-test.ts +++ b/apps/web/test/unit-tests/stores/MemberListStore-test.ts @@ -29,7 +29,7 @@ describe("MemberListStore", () => { const context = new TestSDKContext(); client = stubClient(); client.baseUrl = "https://invalid.base.url.here"; - context.client = client; + context._client = client; store = new MemberListStore(context); // alice is joined to the room. room = new Room(roomId, client, client.getUserId()!); diff --git a/apps/web/test/unit-tests/stores/RoomViewStore-test.ts b/apps/web/test/unit-tests/stores/RoomViewStore-test.ts index 06249972bd..5c52122219 100644 --- a/apps/web/test/unit-tests/stores/RoomViewStore-test.ts +++ b/apps/web/test/unit-tests/stores/RoomViewStore-test.ts @@ -194,7 +194,7 @@ describe("RoomViewStore", function () { dis = new MatrixDispatcher(); slidingSyncManager = new MockSlidingSyncManager(); stores = new TestSDKContext(); - stores.client = mockClient; + stores._client = mockClient; stores._SlidingSyncManager = slidingSyncManager; stores._PosthogAnalytics = new MockPosthogAnalytics(); // @ts-expect-error diff --git a/apps/web/test/unit-tests/stores/TypingStore-test.ts b/apps/web/test/unit-tests/stores/TypingStore-test.ts index 9b3710f43c..44d127215b 100644 --- a/apps/web/test/unit-tests/stores/TypingStore-test.ts +++ b/apps/web/test/unit-tests/stores/TypingStore-test.ts @@ -30,7 +30,7 @@ describe("TypingStore", () => { sendTyping: jest.fn(), } as unknown as MatrixClient; const context = new TestSDKContext(); - context.client = mockClient; + context._client = mockClient; typingStore = new TypingStore(context); jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { return name === "sendTypingNotifications"; diff --git a/apps/web/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts b/apps/web/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts index 7031e5f686..af8abe0f4e 100644 --- a/apps/web/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts +++ b/apps/web/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts @@ -54,7 +54,7 @@ describe("WidgetPermissionStore", () => { ); mockClient = stubClient(); const context = new TestSDKContext(); - context.client = mockClient; + context._client = mockClient; widgetPermissionStore = new WidgetPermissionStore(context); }); diff --git a/apps/web/test/unit-tests/utils/exportUtils/HTMLExport-test.ts b/apps/web/test/unit-tests/utils/exportUtils/HTMLExport-test.ts index 49656a9495..86628c63b2 100644 --- a/apps/web/test/unit-tests/utils/exportUtils/HTMLExport-test.ts +++ b/apps/web/test/unit-tests/utils/exportUtils/HTMLExport-test.ts @@ -33,6 +33,7 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap"; import { mediaFromMxc } from "../../../../src/customisations/Media"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { SDKContextClass } from "../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../TestSDKContext.ts"; jest.mock("jszip"); jest.mock("../../../../src/settings/SettingsStore"); @@ -93,6 +94,7 @@ const EVENT_MENTION: IRoomEvent = { describe("HTMLExport", () => { let client: jest.Mocked; let room: Room; + let sdkContext: TestSDKContext; filterConsole( "Starting export", @@ -111,7 +113,10 @@ describe("HTMLExport", () => { jest.setSystemTime(REPEATABLE_DATE); client = stubClient() as jest.Mocked; - SDKContextClass.instance.client = client; + sdkContext = new TestSDKContext(); + // @ts-ignore HTMLExport uses SDKContext in the constructor + SDKContextClass.instance = sdkContext; + sdkContext._client = client; DMRoomMap.makeShared(client); room = new Room("!myroom:example.org", client, "@me:example.org"); diff --git a/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts b/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts index 0dba5425d3..c8010b750c 100644 --- a/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts +++ b/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts @@ -17,10 +17,13 @@ import { Action } from "../../../src/dispatcher/actions"; import { UserTab } from "../../../src/components/views/dialogs/UserTab"; import Modal from "../../../src/Modal"; import FeedbackDialog from "../../../src/components/views/dialogs/FeedbackDialog"; +import { TestSDKContext } from "../../unit-tests/TestSDKContext.ts"; describe("UserMenuViewModel", () => { let dispatcher: MatrixDispatcher; let client: MockedObject; + let sdkContext: TestSDKContext; + beforeEach(() => { dispatcher = new MatrixDispatcher(); client = getMockClientWithEventEmitter({ @@ -29,13 +32,15 @@ describe("UserMenuViewModel", () => { getAuthMetadata: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)), setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined), }); - SDKContextClass.instance.client = client; + sdkContext = new TestSDKContext(); + // @ts-ignore UserMenuViewModel uses SDKContext in the constructor + SDKContextClass.instance = sdkContext; + sdkContext._client = client; }); afterEach(() => { jest.resetAllMocks(); SdkConfig.reset(); SDKContextClass.instance.onLoggedOut(); - SDKContextClass.instance.client = undefined; }); it("should generate a menu options for a logged in client", () => {