Remove SpaceStore singleton (#34088)

* Remove dead code

* Remove LegacyCallHandler singleton

Route via SDKContext to cut import cycles

* Remove SpaceStore singleton

Route via SDKContext to cut import cycles

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Iterate

* Post-merge fixup

* Iterate
This commit is contained in:
Michael Telatynski
2026-07-13 14:31:39 +00:00
committed by GitHub
parent 7206d887f9
commit 825fab6595
46 changed files with 466 additions and 403 deletions
@@ -30,7 +30,13 @@ import LoggedInView from "../../../../src/components/structures/LoggedInView";
import { SDKContext } from "../../../../src/contexts/SDKContext";
import { StandardActions } from "../../../../src/notifications/StandardActions";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
import {
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsRooms,
mockClientMethodsServer,
mockClientMethodsUser,
} from "../../../test-utils";
import { TestSDKContext } from "../../TestSDKContext";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
@@ -45,9 +51,9 @@ describe("<LoggedInView />", () => {
const userId = "@alice:domain.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getClientWellKnown: jest.fn(),
...mockClientMethodsServer(),
...mockClientMethodsRooms([]),
getAccountData: jest.fn(),
getRoom: jest.fn(),
getSyncState: jest.fn().mockReturnValue(null),
getSyncStateData: jest.fn().mockReturnValue(null),
getMediaHandler: jest.fn(),
@@ -57,6 +63,9 @@ describe("<LoggedInView />", () => {
setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
deleteExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true),
matrixRTC: {
on: jest.fn(),
},
getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")),
});
const mediaHandler = new MediaHandler(mockClient);
@@ -25,13 +25,14 @@ import {
makeBeaconInfoEvent,
mockClientMethodsCrypto,
mockClientMethodsEvents,
mockClientMethodsServer,
mockClientMethodsUser,
mockClientPushProcessor,
} from "../../../test-utils";
import type ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx";
import { SDKContextClass } from "../../../../src/contexts/SDKContextClass";
import { TestSDKContext } from "../../TestSDKContext.ts";
jest.mock("../../../../src/utils/beacon", () => ({
useBeacon: jest.fn(),
@@ -47,14 +48,15 @@ describe("MessagePanel", function () {
...mockClientMethodsEvents(),
...mockClientMethodsCrypto(),
...mockClientPushProcessor(),
...mockClientMethodsServer(),
getAccountData: jest.fn(),
isUserIgnored: jest.fn().mockReturnValue(false),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getRoom: jest.fn(),
getClientWellKnown: jest.fn().mockReturnValue({}),
supportsThreads: jest.fn().mockReturnValue(true),
getVisibleRooms: jest.fn().mockReturnValue([]),
});
let sdkContext: SDKContextClass;
let sdkContext: TestSDKContext;
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
const room = new Room(roomId, client, userId);
@@ -106,7 +108,8 @@ describe("MessagePanel", function () {
return arg === "showDisplaynameChanges";
});
sdkContext = new SDKContextClass();
sdkContext = new TestSDKContext();
sdkContext._client = client;
DMRoomMap.makeShared(client);
});
@@ -18,7 +18,7 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts";
import DMRoomMap from "../../../../src/utils/DMRoomMap.ts";
import { type IOpts } from "../../../../src/createRoom.ts";
import SpaceStore from "../../../../src/stores/spaces/SpaceStore.ts";
import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts";
describe("SpaceRoomView", () => {
let cli: MockedObject<MatrixClient>;
@@ -132,7 +132,7 @@ describe("SpaceRoomView", () => {
describe("Spaces: creating a new community space", () => {
it("asks what topics you want to discuss, creates rooms for them and offers to share", async () => {
cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" });
SpaceStore.instance.addRoomToSpace = jest.fn();
SDKContextClass.instance.spaceStore.addRoomToSpace = jest.fn();
// Given we are creating a space
const view = await renderSpaceRoomView({
@@ -243,7 +243,7 @@ describe("SpaceRoomView", () => {
describe("Spaces: creating a new private space", () => {
it("creates rooms inside a private space for a team", async () => {
cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" });
SpaceStore.instance.addRoomToSpace = jest.fn();
SDKContextClass.instance.spaceStore.addRoomToSpace = jest.fn();
// When I create a private space
const view = await renderSpaceRoomView({
@@ -10,16 +10,16 @@ import React from "react";
import { render } from "jest-matrix-react";
import { Room } from "matrix-js-sdk/src/matrix";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
import { getMockClientWithEventEmitter, mockClientMethodsRooms, mockClientMethodsUser } from "../../../../test-utils";
import ManageRestrictedJoinRuleDialog from "../../../../../src/components/views/dialogs/ManageRestrictedJoinRuleDialog";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
describe("<ManageRestrictedJoinRuleDialog />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
...mockClientMethodsRooms([]),
});
const room = new Room("!roomId:server", mockClient, userId);
mockClient.getRoom.mockReturnValue(room);
@@ -36,7 +36,7 @@ describe("<ManageRestrictedJoinRuleDialog />", () => {
it("should list spaces which are not parents of the room", () => {
const space1 = new Room("!space:server", mockClient, userId);
space1.name = "Other Space";
jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([space1]);
jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([space1]);
expect(getComponent().asFragment()).toMatchSnapshot();
});
@@ -34,7 +34,7 @@ import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import SdkConfig from "../../../../../src/SdkConfig";
import { Action } from "../../../../../src/dispatcher/actions";
import { MetaSpace } from "../../../../../src/stores/spaces";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore.ts";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
jest.useFakeTimers();
@@ -704,7 +704,7 @@ describe("Spotlight Dialog", () => {
describe("metaspaces", () => {
beforeEach(() => {
jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([
jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([
MetaSpace.Home,
MetaSpace.Orphans,
]);
@@ -73,6 +73,8 @@ describe("AppTile", () => {
r1 = new Room("r1", cli, "@name:example.com");
r2 = new Room("r2", cli, "@name:example.com");
jest.spyOn(r1, "getPendingEvents").mockReturnValue([]);
jest.spyOn(r2, "getPendingEvents").mockReturnValue([]);
jest.spyOn(cli, "getRoom").mockImplementation((roomId) => {
if (roomId === "r1") return r1;
if (roomId === "r2") return r2;
@@ -15,29 +15,29 @@ import { LinkedTextContext } from "@element-hq/web-shared-components";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import {
clientAndSDKContextRenderOptions,
filterConsole,
mkEvent,
mkRoomMemberJoinEvent,
mkThirdPartyInviteEvent,
stubClient,
} from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => {
render(
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as RoomContextType)}>
<LinkedTextContext.Provider value={{}}>
<NewRoomIntro />
</LinkedTextContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as RoomContextType)}>
<LinkedTextContext.Provider value={{}}>
<NewRoomIntro />
</LinkedTextContext.Provider>
</ScopedRoomContextProvider>,
clientAndSDKContextRenderOptions(client, SDKContextClass.instance),
);
};
@@ -77,6 +77,7 @@ describe("<CallGuestLinkButton />", () => {
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
sendStateEvent: jest.fn(),
getVisibleRooms: jest.fn().mockReturnValue([]),
});
sdkContext = new TestSDKContext();
sdkContext._client = client;
@@ -26,6 +26,8 @@ import {
clearAllModals,
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsRooms,
mockClientMethodsServer,
mockClientMethodsUser,
} from "../../../../test-utils";
import { filterBoolean } from "../../../../../src/utils/arrays";
@@ -33,14 +35,16 @@ import JoinRuleSettings, {
type JoinRuleSettingsProps,
} from "../../../../../src/components/views/settings/JoinRuleSettings";
import { PreferredRoomVersions } from "../../../../../src/utils/PreferredRoomVersions";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
import DMRoomMap from "../../../../../src/utils/DMRoomMap.ts";
describe("<JoinRuleSettings />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
...mockClientMethodsServer(),
...mockClientMethodsRooms([]),
getDomain: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
sendStateEvent: jest.fn(),
@@ -50,6 +54,7 @@ describe("<JoinRuleSettings />", () => {
isRoomEncrypted: jest.fn().mockReturnValue(false),
getRoomDirectoryVisibility: jest.fn(),
setRoomDirectoryVisibility: jest.fn(),
matrixRTC: { on: jest.fn() },
});
const roomId = "!room:server.org";
const newRoomId = "!roomUpgraded:server.org";
@@ -112,6 +117,7 @@ describe("<JoinRuleSettings />", () => {
client.upgradeRoom.mockResolvedValue({ replacement_room: newRoomId });
client.getRoom.mockReturnValue(null);
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
DMRoomMap.makeShared(client);
});
type TestCase = [string, { label: string; unsupportedRoomVersion: string; preferredRoomVersion: string }];
@@ -165,7 +171,9 @@ describe("<JoinRuleSettings />", () => {
// room that doesn't support the join rule
const room = new Room(roomId, client, userId);
const parentSpace = new Room("!parentSpace:server.org", client, userId);
jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parentSpace.roomId]));
jest.spyOn(SDKContextClass.instance.spaceStore, "getKnownParents").mockReturnValue(
new Set([parentSpace.roomId]),
);
setRoomStateEvents(room, unsupportedRoomVersion);
const memberAlice = new RoomMember(roomId, "@alice:server.org");
const memberBob = new RoomMember(roomId, "@bob:server.org");
@@ -18,7 +18,6 @@ import { shouldShowComponent } from "../../../../../src/customisations/helpers/U
import { UIComponent } from "../../../../../src/settings/UIFeature";
import { mkStubRoom, wrapInMatrixClientContext, wrapInSdkContext } from "../../../../test-utils";
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";
import SettingsStore from "../../../../../src/settings/SettingsStore";
@@ -99,10 +98,9 @@ jest.mock("../../../../../src/stores/spaces/SpaceStore", () => {
getNotificationState = () => null as SpaceNotificationState | null;
setActiveSpace = jest.fn();
moveRootSpace = jest.fn();
start = jest.fn();
}
return {
instance: new MockSpaceStore(),
};
return MockSpaceStore;
});
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
@@ -135,7 +133,7 @@ describe("<SpacePanel />", () => {
});
beforeEach(() => {
SpaceStore.instance.enabledMetaSpaces.push(MetaSpace.Home, MetaSpace.Orphans, MetaSpace.VideoRooms);
sdkContext.spaceStore.enabledMetaSpaces.push(MetaSpace.Home, MetaSpace.Orphans, MetaSpace.VideoRooms);
mocked(shouldShowComponent).mockClear().mockReturnValue(true);
});
afterEach(() => {
@@ -186,7 +184,7 @@ describe("<SpacePanel />", () => {
});
it("should allow rearranging via drag and drop", async () => {
(SpaceStore.instance.spacePanelSpaces as any) = [
(sdkContext.spaceStore.spacePanelSpaces as any) = [
mkStubRoom("!room1:server", "Room 1", mockClient),
mkStubRoom("!room2:server", "Room 2", mockClient),
mkStubRoom("!room3:server", "Room 3", mockClient),
@@ -201,7 +199,7 @@ describe("<SpacePanel />", () => {
await move(room1, DragDirection.DOWN);
await drop(room1);
expect(SpaceStore.instance.moveRootSpace).toHaveBeenCalledWith(0, 1);
expect(sdkContext.spaceStore.moveRootSpace).toHaveBeenCalledWith(0, 1);
});
it("should be able to open the user menu via dispatcher", async () => {
@@ -10,16 +10,15 @@ import React from "react";
import { fireEvent, getByTestId, render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { mkRoom, stubClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { clientAndSDKContextRenderOptions, mkRoom, stubClient } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { SpaceItem, SpaceButton } from "../../../../../src/components/views/spaces/SpaceTreeLevel";
import { MetaSpace, type SpaceKey } from "../../../../../src/stores/spaces";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import { StaticNotificationState } from "../../../../../src/stores/notifications/StaticNotificationState";
import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
jest.mock("../../../../../src/stores/spaces/SpaceStore", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -29,15 +28,16 @@ jest.mock("../../../../../src/stores/spaces/SpaceStore", () => {
setActiveSpace = jest.fn();
getChildSpaces = jest.fn();
getNotificationState = jest.fn();
start = jest.fn();
}
return { instance: new MockSpaceStore() };
return MockSpaceStore;
});
describe("SpaceButton", () => {
stubClient();
const space = mkRoom(MatrixClientPeg.safeGet(), "!1:example.org");
DMRoomMap.makeShared(MatrixClientPeg.safeGet());
const cli = stubClient();
const space = mkRoom(cli, "!1:example.org");
DMRoomMap.makeShared(cli);
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
@@ -53,11 +53,12 @@ describe("SpaceButton", () => {
data-testid="create-space-button"
size="32px"
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled();
expect(SDKContextClass.instance.spaceStore.setActiveSpace).not.toHaveBeenCalled();
fireEvent.click(getByTestId(container, "create-space-button"));
expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith("!1:example.org");
expect(SDKContextClass.instance.spaceStore.setActiveSpace).toHaveBeenCalledWith("!1:example.org");
});
it("navigates to the space home on click if already active", () => {
@@ -87,11 +88,12 @@ describe("SpaceButton", () => {
data-testid="create-space-button"
size="32px"
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled();
expect(SDKContextClass.instance.spaceStore.setActiveSpace).not.toHaveBeenCalled();
fireEvent.click(getByTestId(container, "create-space-button"));
expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home);
expect(SDKContextClass.instance.spaceStore.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home);
});
it("does nothing on click if already active", () => {
@@ -103,12 +105,13 @@ describe("SpaceButton", () => {
data-testid="create-space-button"
size="32px"
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
fireEvent.click(getByTestId(container, "create-space-button"));
expect(dispatchSpy).not.toHaveBeenCalled();
// Re-activating the metaspace is a no-op
expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home);
expect(SDKContextClass.instance.spaceStore.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home);
});
it("should render notificationState if one is provided", () => {
@@ -139,11 +142,14 @@ describe("SpaceItem", () => {
subspace.name = "Subspace";
it("should render a space with subspaces", () => {
mocked(SpaceStore.instance.getChildSpaces).mockImplementation((spaceId) =>
mocked(SDKContextClass.instance.spaceStore.getChildSpaces).mockImplementation((spaceId) =>
spaceId === space.roomId ? [subspace] : [],
);
const { asFragment, queryByText, getByLabelText } = render(<SpaceItem space={space} activeSpaces={[]} />);
const { asFragment, queryByText, getByLabelText } = render(
<SpaceItem space={space} activeSpaces={[]} />,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
expect(queryByText("Root Space")).toBeVisible();
expect(queryByText("Subspace")).toBeNull();