/* 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 { EventEmitter } from "events"; import { mocked } from "jest-mock"; import { EventType, RoomMember, RoomStateEvent, ClientEvent, MatrixEvent, Room, RoomEvent, JoinRule, RoomState, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { defer } from "matrix-js-sdk/src/utils"; import SpaceStore from "../../../src/stores/spaces/SpaceStore"; import { MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../src/stores/spaces"; import * as testUtils from "../../test-utils"; import { mkEvent, stubClient } from "../../test-utils"; import DMRoomMap from "../../../src/utils/DMRoomMap"; import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { Action } from "../../../src/dispatcher/actions"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import RoomListStore from "../../../src/stores/room-list/RoomListStore"; import { DefaultTagID } from "../../../src/stores/room-list/models"; import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore"; import { NotificationLevel } from "../../../src/stores/notifications/NotificationLevel"; jest.useFakeTimers(); const testUserId = "@test:user"; const fav1 = "!fav1:server"; const fav2 = "!fav2:server"; const fav3 = "!fav3:server"; const dm1 = "!dm1:server"; const dm1Partner = new RoomMember(dm1, "@dm1Partner:server"); dm1Partner.membership = KnownMembership.Join; const dm2 = "!dm2:server"; const dm2Partner = new RoomMember(dm2, "@dm2Partner:server"); dm2Partner.membership = KnownMembership.Join; const dm3 = "!dm3:server"; const dm3Partner = new RoomMember(dm3, "@dm3Partner:server"); dm3Partner.membership = KnownMembership.Join; const orphan1 = "!orphan1:server"; const orphan2 = "!orphan2:server"; const invite1 = "!invite1:server"; const invite2 = "!invite2:server"; const room1 = "!room1:server"; const room2 = "!room2:server"; const room3 = "!room3:server"; const room4 = "!room4:server"; const videoRoomPrivate = "!videoRoomPrivate:server"; const videoRoomPublic = "!videoRoomPublic:server"; const space1 = "!space1:server"; const space2 = "!space2:server"; const space3 = "!space3:server"; const space4 = "!space4:server"; const getUserIdForRoomId = jest.fn((roomId: string) => { return { [dm1]: dm1Partner.userId, [dm2]: dm2Partner.userId, [dm3]: dm3Partner.userId, }[roomId]; }); const getDMRoomsForUserId = jest.fn((userId) => { switch (userId) { case dm1Partner.userId: return [dm1]; case dm2Partner.userId: return [dm2]; case dm3Partner.userId: return [dm3]; default: return []; } }); // @ts-ignore DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId }; describe("SpaceStore", () => { stubClient(); const store = SpaceStore.instance; const client = MatrixClientPeg.safeGet(); const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); let rooms: Room[] = []; const mkRoom = (roomId: string) => testUtils.mkRoom(client, roomId, rooms); const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children); const viewRoom = (roomId: string) => defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: roomId }, true); const run = async () => { mocked(client).getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null); mocked(client).getRoomUpgradeHistory.mockImplementation((roomId) => { const room = rooms.find((room) => room.roomId === roomId); return room ? [room] : []; }); await testUtils.setupAsyncStoreWithClient(store, client); jest.runOnlyPendingTimers(); }; const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, value); jest.runOnlyPendingTimers(); // run async dispatch await emitProm; }; beforeEach(async () => { jest.runOnlyPendingTimers(); // run async dispatch mocked(client).getVisibleRooms.mockReturnValue((rooms = [])); await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, { [MetaSpace.Home]: true, [MetaSpace.Favourites]: true, [MetaSpace.People]: true, [MetaSpace.Orphans]: true, }); spyDispatcher.mockClear(); }); afterEach(async () => { await testUtils.resetAsyncStoreWithClient(store); }); describe("static hierarchy resolution tests", () => { it("handles no spaces", async () => { await run(); expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([]); }); it("handles 3 joined top level spaces", async () => { mkSpace("!space1:server"); mkSpace("!space2:server"); mkSpace("!space3:server"); await run(); expect(store.spacePanelSpaces.sort()).toStrictEqual(client.getVisibleRooms().sort()); expect(store.invitedSpaces).toStrictEqual([]); }); it("handles a basic hierarchy", async () => { mkSpace("!space1:server"); mkSpace("!space2:server"); mkSpace("!company:server", [ mkSpace("!company_dept1:server", [mkSpace("!company_dept1_group1:server").roomId]).roomId, mkSpace("!company_dept2:server").roomId, ]); await run(); expect(store.spacePanelSpaces.map((r) => r.roomId).sort()).toStrictEqual( ["!space1:server", "!space2:server", "!company:server"].sort(), ); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildRooms("!space1:server")).toStrictEqual([]); expect(store.getChildSpaces("!space1:server")).toStrictEqual([]); expect(store.getChildRooms("!space2:server")).toStrictEqual([]); expect(store.getChildSpaces("!space2:server")).toStrictEqual([]); expect(store.getChildRooms("!company:server")).toStrictEqual([]); expect(store.getChildSpaces("!company:server")).toStrictEqual([ client.getRoom("!company_dept1:server"), client.getRoom("!company_dept2:server"), ]); expect(store.getChildRooms("!company_dept1:server")).toStrictEqual([]); expect(store.getChildSpaces("!company_dept1:server")).toStrictEqual([ client.getRoom("!company_dept1_group1:server"), ]); expect(store.getChildRooms("!company_dept1_group1:server")).toStrictEqual([]); expect(store.getChildSpaces("!company_dept1_group1:server")).toStrictEqual([]); expect(store.getChildRooms("!company_dept2:server")).toStrictEqual([]); expect(store.getChildSpaces("!company_dept2:server")).toStrictEqual([]); }); it("handles a sub-space existing in multiple places in the space tree", async () => { const subspace = mkSpace("!subspace:server"); mkSpace("!space1:server"); mkSpace("!space2:server"); mkSpace("!company:server", [ mkSpace("!company_dept1:server", [mkSpace("!company_dept1_group1:server", [subspace.roomId]).roomId]) .roomId, mkSpace("!company_dept2:server", [subspace.roomId]).roomId, subspace.roomId, ]); await run(); expect(store.spacePanelSpaces.map((r) => r.roomId).sort()).toStrictEqual( ["!space1:server", "!space2:server", "!company:server"].sort(), ); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildRooms("!space1:server")).toStrictEqual([]); expect(store.getChildSpaces("!space1:server")).toStrictEqual([]); expect(store.getChildRooms("!space2:server")).toStrictEqual([]); expect(store.getChildSpaces("!space2:server")).toStrictEqual([]); expect(store.getChildRooms("!company:server")).toStrictEqual([]); expect(store.getChildSpaces("!company:server")).toStrictEqual([ client.getRoom("!company_dept1:server"), client.getRoom("!company_dept2:server"), subspace, ]); expect(store.getChildRooms("!company_dept1:server")).toStrictEqual([]); expect(store.getChildSpaces("!company_dept1:server")).toStrictEqual([ client.getRoom("!company_dept1_group1:server"), ]); expect(store.getChildRooms("!company_dept1_group1:server")).toStrictEqual([]); expect(store.getChildSpaces("!company_dept1_group1:server")).toStrictEqual([subspace]); expect(store.getChildRooms("!company_dept2:server")).toStrictEqual([]); expect(store.getChildSpaces("!company_dept2:server")).toStrictEqual([subspace]); }); it("handles full cycles", async () => { mkSpace("!a:server", [mkSpace("!b:server", [mkSpace("!c:server", ["!a:server"]).roomId]).roomId]); await run(); expect(store.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual(["!a:server"]); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildRooms("!a:server")).toStrictEqual([]); expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!b:server")]); expect(store.getChildRooms("!b:server")).toStrictEqual([]); expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!c:server")]); expect(store.getChildRooms("!c:server")).toStrictEqual([]); expect(store.getChildSpaces("!c:server")).toStrictEqual([client.getRoom("!a:server")]); }); it("handles partial cycles", async () => { mkSpace("!b:server", [mkSpace("!a:server", [mkSpace("!c:server", ["!a:server"]).roomId]).roomId]); await run(); expect(store.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual(["!b:server"]); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildRooms("!b:server")).toStrictEqual([]); expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!a:server")]); expect(store.getChildRooms("!a:server")).toStrictEqual([]); expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!c:server")]); expect(store.getChildRooms("!c:server")).toStrictEqual([]); expect(store.getChildSpaces("!c:server")).toStrictEqual([client.getRoom("!a:server")]); }); it("handles partial cycles with additional spaces coming off them", async () => { // TODO this test should be failing right now mkSpace("!a:server", [ mkSpace("!b:server", [mkSpace("!c:server", ["!a:server", mkSpace("!d:server").roomId]).roomId]).roomId, ]); await run(); expect(store.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual(["!a:server"]); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildRooms("!a:server")).toStrictEqual([]); expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!b:server")]); expect(store.getChildRooms("!b:server")).toStrictEqual([]); expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!c:server")]); expect(store.getChildRooms("!c:server")).toStrictEqual([]); expect(store.getChildSpaces("!c:server")).toStrictEqual([ client.getRoom("!a:server"), client.getRoom("!d:server"), ]); expect(store.getChildRooms("!d:server")).toStrictEqual([]); expect(store.getChildSpaces("!d:server")).toStrictEqual([]); }); it("invite to a subspace is only shown at the top level", async () => { mkSpace(invite1).getMyMembership.mockReturnValue(KnownMembership.Invite); mkSpace(space1, [invite1]); await run(); expect(store.spacePanelSpaces).toStrictEqual([client.getRoom(space1)]); expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildRooms(space1)).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([client.getRoom(invite1)]); }); describe("test fixture 1", () => { beforeEach(async () => { [ fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3, room4, videoRoomPrivate, videoRoomPublic, ].forEach(mkRoom); mkSpace(space1, [fav1, room1]); mkSpace(space2, [fav1, fav2, fav3, room1]); mkSpace(space3, [invite2]); mkSpace(space4, [room4, fav2, space2, space3]); mocked(client).getRoom.mockImplementation( (roomId) => rooms.find((room) => room.roomId === roomId) || null, ); [fav1, fav2, fav3].forEach((roomId) => { client.getRoom(roomId)!.tags = { "m.favourite": { order: 0.5, }, }; }); [invite1, invite2].forEach((roomId) => { mocked(client.getRoom(roomId)!).getMyMembership.mockReturnValue(KnownMembership.Invite); }); // have dmPartner1 be in space1 with you const mySpace1Member = new RoomMember(space1, testUserId); mySpace1Member.membership = KnownMembership.Join; (rooms.find((r) => r.roomId === space1)!.getMembers as jest.Mock).mockReturnValue([ mySpace1Member, dm1Partner, ]); // have dmPartner2 be in space2 with you const mySpace2Member = new RoomMember(space2, testUserId); mySpace2Member.membership = KnownMembership.Join; (rooms.find((r) => r.roomId === space2)!.getMembers as jest.Mock).mockReturnValue([ mySpace2Member, dm2Partner, ]); // dmPartner3 is not in any common spaces with you // room 2 claims to be a child of space2 and is so via a valid m.space.parent const cliRoom2 = client.getRoom(room2); const room2MockStateEvents = testUtils.mockStateEventImplementation([ mkEvent({ event: true, type: EventType.SpaceParent, room: room2, user: client.getUserId()!, skey: space2, content: { via: [], canonical: true }, ts: Date.now(), }) as MatrixEvent, ]); mocked(cliRoom2!.currentState).getStateEvents.mockImplementation(room2MockStateEvents); const cliSpace2 = client.getRoom(space2); mocked(cliSpace2!.currentState).maySendStateEvent.mockImplementation( (evType: string, userId: string) => { if (evType === EventType.SpaceChild) { return userId === client.getUserId(); } return true; }, ); // room 3 claims to be a child of space3 but is not due to invalid m.space.parent (permissions) const cliRoom3 = client.getRoom(room3); mocked(cliRoom3!.currentState).getStateEvents.mockImplementation( testUtils.mockStateEventImplementation([ mkEvent({ event: true, type: EventType.SpaceParent, room: room3, user: client.getUserId()!, skey: space3, content: { via: [], canonical: true }, ts: Date.now(), }), ]), ); const cliSpace3 = client.getRoom(space3); mocked(cliSpace3!.currentState).maySendStateEvent.mockImplementation( (evType: string, userId: string) => { if (evType === EventType.SpaceChild) { return false; } return true; }, ); [videoRoomPrivate, videoRoomPublic].forEach((roomId) => { const videoRoom = client.getRoom(roomId); (videoRoom!.isCallRoom as jest.Mock).mockReturnValue(true); }); const videoRoomPublicRoom = client.getRoom(videoRoomPublic); (videoRoomPublicRoom!.getJoinRule as jest.Mock).mockReturnValue(JoinRule.Public); await run(); }); describe("isRoomInSpace()", () => { it("home space contains orphaned rooms", () => { expect(store.isRoomInSpace(MetaSpace.Home, orphan1)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Home, orphan2)).toBeTruthy(); }); it("home space does not contain all favourites", () => { expect(store.isRoomInSpace(MetaSpace.Home, fav1)).toBeFalsy(); expect(store.isRoomInSpace(MetaSpace.Home, fav2)).toBeFalsy(); expect(store.isRoomInSpace(MetaSpace.Home, fav3)).toBeFalsy(); }); it("home space contains dm rooms", () => { expect(store.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Home, dm2)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Home, dm3)).toBeTruthy(); }); it("home space contains invites", () => { expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy(); }); it("home space contains invites even if they are also shown in a space", () => { expect(store.isRoomInSpace(MetaSpace.Home, invite2)).toBeTruthy(); }); it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => { await setShowAllRooms(true); expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeTruthy(); }); it("favourites space does contain favourites even if they are also shown in a space", async () => { expect(store.isRoomInSpace(MetaSpace.Favourites, fav1)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Favourites, fav2)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Favourites, fav3)).toBeTruthy(); }); it("people space does contain people even if they are also shown in a space", async () => { expect(store.isRoomInSpace(MetaSpace.People, dm1)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.People, dm2)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.People, dm3)).toBeTruthy(); }); it("orphans space does contain orphans even if they are also shown in all rooms", async () => { await setShowAllRooms(true); expect(store.isRoomInSpace(MetaSpace.Orphans, orphan1)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Orphans, orphan2)).toBeTruthy(); }); it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => { await setShowAllRooms(false); expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy(); }); it("video room space contains all video rooms", async () => { await setShowAllRooms(false); expect(store.isRoomInSpace(MetaSpace.VideoRooms, videoRoomPublic)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.VideoRooms, videoRoomPrivate)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.VideoRooms, room1)).toBeFalsy(); }); it("updates the video room space when the room type changes", async () => { expect(store.isRoomInSpace(MetaSpace.VideoRooms, videoRoomPrivate)).toBeTruthy(); (client.getRoom(videoRoomPublic)!.isCallRoom as jest.Mock).mockReturnValue(false); client.emit( RoomStateEvent.Events, { getRoomId: () => videoRoomPublic, getType: () => EventType.RoomCreate, } as unknown as MatrixEvent, {} as unknown as RoomState, null, ); expect(store.isRoomInSpace(MetaSpace.VideoRooms, videoRoomPublic)).toBeFalsy(); }); it("space contains child rooms", () => { expect(store.isRoomInSpace(space1, fav1)).toBeTruthy(); expect(store.isRoomInSpace(space1, room1)).toBeTruthy(); }); it("returns true for all sub-space child rooms when includeSubSpaceRooms is undefined", () => { expect(store.isRoomInSpace(space4, room4)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav2)).toBeTruthy(); // space2's rooms expect(store.isRoomInSpace(space4, fav1)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav3)).toBeTruthy(); expect(store.isRoomInSpace(space4, room1)).toBeTruthy(); // space3's rooms expect(store.isRoomInSpace(space4, invite2)).toBeTruthy(); }); it("returns true for all sub-space child rooms when includeSubSpaceRooms is true", () => { expect(store.isRoomInSpace(space4, room4, true)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav2, true)).toBeTruthy(); // space2's rooms expect(store.isRoomInSpace(space4, fav1, true)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav3, true)).toBeTruthy(); expect(store.isRoomInSpace(space4, room1, true)).toBeTruthy(); // space3's rooms expect(store.isRoomInSpace(space4, invite2, true)).toBeTruthy(); }); it("returns false for all sub-space child rooms when includeSubSpaceRooms is false", () => { // direct children expect(store.isRoomInSpace(space4, room4, false)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav2, false)).toBeTruthy(); // space2's rooms expect(store.isRoomInSpace(space4, fav1, false)).toBeFalsy(); expect(store.isRoomInSpace(space4, fav3, false)).toBeFalsy(); expect(store.isRoomInSpace(space4, room1, false)).toBeFalsy(); // space3's rooms expect(store.isRoomInSpace(space4, invite2, false)).toBeFalsy(); }); it("space contains all sub-space's child rooms", () => { expect(store.isRoomInSpace(space4, room4)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav2)).toBeTruthy(); // space2's rooms expect(store.isRoomInSpace(space4, fav1)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav3)).toBeTruthy(); expect(store.isRoomInSpace(space4, room1)).toBeTruthy(); // space3's rooms expect(store.isRoomInSpace(space4, invite2)).toBeTruthy(); }); it("space contains child favourites", () => { expect(store.isRoomInSpace(space2, fav1)).toBeTruthy(); expect(store.isRoomInSpace(space2, fav2)).toBeTruthy(); expect(store.isRoomInSpace(space2, fav3)).toBeTruthy(); expect(store.isRoomInSpace(space2, room1)).toBeTruthy(); }); it("space contains child invites", () => { expect(store.isRoomInSpace(space3, invite2)).toBeTruthy(); }); it("spaces contain dms which you have with members of that space", () => { expect(store.isRoomInSpace(space1, dm1)).toBeTruthy(); expect(store.isRoomInSpace(space2, dm1)).toBeFalsy(); expect(store.isRoomInSpace(space3, dm1)).toBeFalsy(); expect(store.isRoomInSpace(space1, dm2)).toBeFalsy(); expect(store.isRoomInSpace(space2, dm2)).toBeTruthy(); expect(store.isRoomInSpace(space3, dm2)).toBeFalsy(); expect(store.isRoomInSpace(space1, dm3)).toBeFalsy(); expect(store.isRoomInSpace(space2, dm3)).toBeFalsy(); expect(store.isRoomInSpace(space3, dm3)).toBeFalsy(); }); it("uses cached aggregated rooms", () => { const rooms = store.getSpaceFilteredRoomIds(space4, true); expect(store.isRoomInSpace(space4, fav1)).toBeTruthy(); expect(store.isRoomInSpace(space4, fav3)).toBeTruthy(); expect(store.isRoomInSpace(space4, room1)).toBeTruthy(); // isRoomInSpace calls didn't rebuild room set expect(rooms).toStrictEqual(store.getSpaceFilteredRoomIds(space4, true)); }); }); it("dms are only added to Notification States for only the People Space", async () => { [dm1, dm2, dm3].forEach((d) => { expect( store .getNotificationState(MetaSpace.People) .rooms.map((r) => r.roomId) .includes(d), ).toBeTruthy(); }); [space1, space2, space3, MetaSpace.Home, MetaSpace.Orphans, MetaSpace.Favourites].forEach((s) => { [dm1, dm2, dm3].forEach((d) => { expect( store .getNotificationState(s) .rooms.map((r) => r.roomId) .includes(d), ).toBeFalsy(); }); }); }); it("orphan rooms are added to Notification States for only the Home Space", async () => { await setShowAllRooms(false); [orphan1, orphan2].forEach((d) => { expect( store .getNotificationState(MetaSpace.Home) .rooms.map((r) => r.roomId) .includes(d), ).toBeTruthy(); }); [space1, space2, space3].forEach((s) => { [orphan1, orphan2].forEach((d) => { expect( store .getNotificationState(s) .rooms.map((r) => r.roomId) .includes(d), ).toBeFalsy(); }); }); }); it("favourites are added to Notification States for all spaces containing the room inc Home", () => { // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better // [fav1, fav2, fav3].forEach(d => { // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy(); // }); expect( store .getNotificationState(space1) .rooms.map((r) => r.roomId) .includes(fav1), ).toBeTruthy(); expect( store .getNotificationState(space1) .rooms.map((r) => r.roomId) .includes(fav2), ).toBeFalsy(); expect( store .getNotificationState(space1) .rooms.map((r) => r.roomId) .includes(fav3), ).toBeFalsy(); expect( store .getNotificationState(space2) .rooms.map((r) => r.roomId) .includes(fav1), ).toBeTruthy(); expect( store .getNotificationState(space2) .rooms.map((r) => r.roomId) .includes(fav2), ).toBeTruthy(); expect( store .getNotificationState(space2) .rooms.map((r) => r.roomId) .includes(fav3), ).toBeTruthy(); expect( store .getNotificationState(space3) .rooms.map((r) => r.roomId) .includes(fav1), ).toBeFalsy(); expect( store .getNotificationState(space3) .rooms.map((r) => r.roomId) .includes(fav2), ).toBeFalsy(); expect( store .getNotificationState(space3) .rooms.map((r) => r.roomId) .includes(fav3), ).toBeFalsy(); }); it("other rooms are added to Notification States for all spaces containing the room exc Home", () => { // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(room1)).toBeFalsy(); expect( store .getNotificationState(space1) .rooms.map((r) => r.roomId) .includes(room1), ).toBeTruthy(); expect( store .getNotificationState(space2) .rooms.map((r) => r.roomId) .includes(room1), ).toBeTruthy(); expect( store .getNotificationState(space3) .rooms.map((r) => r.roomId) .includes(room1), ).toBeFalsy(); }); it("honours m.space.parent if sender has permission in parent space", () => { expect(store.isRoomInSpace(space2, room2)).toBeTruthy(); }); it("does not honour m.space.parent if sender does not have permission in parent space", () => { expect(store.isRoomInSpace(space3, room3)).toBeFalsy(); }); }); }); it("should add new DM Invites to the People Space Notification State", async () => { mkRoom(dm1); mocked(client.getRoom(dm1)!).getMyMembership.mockReturnValue(KnownMembership.Join); mocked(client).getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null); await run(); mkRoom(dm2); const cliDm2 = client.getRoom(dm2)!; mocked(cliDm2).getMyMembership.mockReturnValue(KnownMembership.Invite); mocked(client).getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null); client.emit(RoomEvent.MyMembership, cliDm2, KnownMembership.Invite); [dm1, dm2].forEach((d) => { expect( store .getNotificationState(MetaSpace.People) .rooms.map((r) => r.roomId) .includes(d), ).toBeTruthy(); }); }); describe("hierarchy resolution update tests", () => { it("updates state when spaces are joined", async () => { await run(); expect(store.spacePanelSpaces).toStrictEqual([]); const space = mkSpace(space1); const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); client.emit(ClientEvent.Room, space); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); expect(store.invitedSpaces).toStrictEqual([]); }); it("updates state when spaces are left", async () => { const space = mkSpace(space1); await run(); expect(store.spacePanelSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue(KnownMembership.Leave); const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); client.emit(RoomEvent.MyMembership, space, KnownMembership.Leave, KnownMembership.Join); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); }); it("updates state when space invite comes in", async () => { await run(); expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([]); const space = mkSpace(space1); space.getMyMembership.mockReturnValue(KnownMembership.Invite); const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES); client.emit(ClientEvent.Room, space); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); }); it("updates state when space invite is accepted", async () => { const space = mkSpace(space1); space.getMyMembership.mockReturnValue(KnownMembership.Invite); await run(); expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue(KnownMembership.Join); const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); client.emit(RoomEvent.MyMembership, space, KnownMembership.Join, KnownMembership.Invite); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); expect(store.invitedSpaces).toStrictEqual([]); }); it("updates state when space invite is rejected", async () => { const space = mkSpace(space1); space.getMyMembership.mockReturnValue(KnownMembership.Invite); await run(); expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue(KnownMembership.Leave); const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES); client.emit(RoomEvent.MyMembership, space, KnownMembership.Leave, KnownMembership.Invite); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([]); }); it("room invite gets added to relevant space filters", async () => { const space = mkSpace(space1, [invite1]); await run(); expect(store.spacePanelSpaces).toStrictEqual([space]); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildRooms(space1)).toStrictEqual([]); expect(store.isRoomInSpace(space1, invite1)).toBeFalsy(); expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeFalsy(); const invite = mkRoom(invite1); invite.getMyMembership.mockReturnValue(KnownMembership.Invite); const prom = testUtils.emitPromise(store, space1); client.emit(ClientEvent.Room, space); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildRooms(space1)).toStrictEqual([invite]); expect(store.isRoomInSpace(space1, invite1)).toBeTruthy(); expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy(); }); describe("onRoomsUpdate()", () => { beforeEach(() => { [ fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3, room4, ].forEach(mkRoom); mkSpace(space2, [fav1, fav2, fav3, room1]); mkSpace(space3, [invite2]); mkSpace(space4, [room4, fav2, space2, space3]); mkSpace(space1, [fav1, room1, space4]); }); const addChildRoom = (spaceId: string, childId: string) => { const childEvent = mkEvent({ event: true, type: EventType.SpaceChild, room: spaceId, user: client.getUserId()!, skey: childId, content: { via: [], canonical: true }, ts: Date.now(), }); const spaceRoom = client.getRoom(spaceId)!; mocked(spaceRoom.currentState).getStateEvents.mockImplementation( testUtils.mockStateEventImplementation([childEvent]), ); client.emit(RoomStateEvent.Events, childEvent, spaceRoom.currentState, null); }; const addMember = (spaceId: string, user: RoomMember) => { const memberEvent = mkEvent({ event: true, type: EventType.RoomMember, room: spaceId, user: client.getUserId()!, skey: user.userId, content: { membership: KnownMembership.Join }, ts: Date.now(), }); const spaceRoom = client.getRoom(spaceId)!; mocked(spaceRoom.currentState).getStateEvents.mockImplementation( testUtils.mockStateEventImplementation([memberEvent]), ); mocked(spaceRoom).getMember.mockReturnValue(user); client.emit(RoomStateEvent.Members, memberEvent, spaceRoom.currentState, user); }; it("emits events for parent spaces when child room is added", async () => { await run(); const room5 = mkRoom("!room5:server"); const emitSpy = jest.spyOn(store, "emit").mockClear(); // add room5 into space2 addChildRoom(space2, room5.roomId); expect(emitSpy).toHaveBeenCalledWith(space2); // space2 is subspace of space4 expect(emitSpy).toHaveBeenCalledWith(space4); // space4 is a subspace of space1 expect(emitSpy).toHaveBeenCalledWith(space1); expect(emitSpy).not.toHaveBeenCalledWith(space3); }); it("updates rooms state when a child room is added", async () => { await run(); const room5 = mkRoom("!room5:server"); expect(store.isRoomInSpace(space2, room5.roomId)).toBeFalsy(); expect(store.isRoomInSpace(space4, room5.roomId)).toBeFalsy(); // add room5 into space2 addChildRoom(space2, room5.roomId); expect(store.isRoomInSpace(space2, room5.roomId)).toBeTruthy(); // space2 is subspace of space4 expect(store.isRoomInSpace(space4, room5.roomId)).toBeTruthy(); // space4 is subspace of space1 expect(store.isRoomInSpace(space1, room5.roomId)).toBeTruthy(); }); it("emits events for parent spaces when a member is added", async () => { await run(); const emitSpy = jest.spyOn(store, "emit").mockClear(); // add into space2 addMember(space2, dm1Partner); expect(emitSpy).toHaveBeenCalledWith(space2); // space2 is subspace of space4 expect(emitSpy).toHaveBeenCalledWith(space4); // space4 is a subspace of space1 expect(emitSpy).toHaveBeenCalledWith(space1); expect(emitSpy).not.toHaveBeenCalledWith(space3); }); it("updates users state when a member is added", async () => { await run(); expect(store.getSpaceFilteredUserIds(space2)).toEqual(new Set([])); // add into space2 addMember(space2, dm1Partner); expect(store.getSpaceFilteredUserIds(space2)).toEqual(new Set([dm1Partner.userId])); expect(store.getSpaceFilteredUserIds(space4)).toEqual(new Set([dm1Partner.userId])); expect(store.getSpaceFilteredUserIds(space1)).toEqual(new Set([dm1Partner.userId])); }); }); }); describe("active space switching tests", () => { const fn = jest.spyOn(store, "emit"); beforeEach(async () => { mkRoom(room1); // not a space mkSpace(space1, [mkSpace(space2).roomId]); mkSpace(space3).getMyMembership.mockReturnValue(KnownMembership.Invite); await run(); store.setActiveSpace(MetaSpace.Home); expect(store.activeSpace).toBe(MetaSpace.Home); }); afterEach(() => { fn.mockClear(); }); it("switch to home space", async () => { store.setActiveSpace(space1); fn.mockClear(); store.setActiveSpace(MetaSpace.Home); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, MetaSpace.Home); expect(store.activeSpace).toBe(MetaSpace.Home); }); it("switch to invited space", async () => { store.setActiveSpace(space3); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space3); expect(store.activeSpace).toBe(space3); }); it("switch to top level space", async () => { store.setActiveSpace(space1); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space1); expect(store.activeSpace).toBe(space1); }); it("switch to subspace", async () => { store.setActiveSpace(space2); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space2); expect(store.activeSpace).toBe(space2); }); it("switch to unknown space is a nop", async () => { expect(store.activeSpace).toBe(MetaSpace.Home); const space = client.getRoom(room1)!; // not a space store.setActiveSpace(space.roomId); expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space.roomId); expect(store.activeSpace).toBe(MetaSpace.Home); }); }); it("does not race with lazy loading", async () => { store.setActiveSpace(MetaSpace.Home); mkRoom(room1); const space = mkSpace(space1, [room1]); // seed the context for space1 to be room1 window.localStorage.setItem(`mx_space_context_${space1}`, room1); await run(); const deferred = defer(); space.loadMembersIfNeeded.mockImplementation(() => { const event = mkEvent({ event: true, type: EventType.RoomMember, content: { membership: KnownMembership.Join }, skey: dm1Partner.userId, user: dm1Partner.userId, room: space1, }); space.getMember.mockImplementation((userId) => { if (userId === dm1Partner.userId) { const member = new RoomMember(space1, dm1Partner.userId); member.membership = KnownMembership.Join; return member; } return null; }); client.emit(RoomStateEvent.Members, event, space.currentState, dm1Partner); return deferred.resolve(true) as unknown as Promise; }); spyDispatcher.mockClear(); const getCurrentRoom = () => { for (let i = spyDispatcher.mock.calls.length - 1; i >= 0; i--) { if (spyDispatcher.mock.calls[i][0].action === Action.ViewRoom) { return spyDispatcher.mock.calls[i][0]["room_id"]; } } }; // set up space with LL where loadMembersIfNeeded emits membership events which trip switchSpaceIfNeeded expect(space.loadMembersIfNeeded).not.toHaveBeenCalled(); store.setActiveSpace(space1, true); jest.runOnlyPendingTimers(); expect(space.loadMembersIfNeeded).toHaveBeenCalled(); jest.runAllTimers(); expect(store.activeSpace).toBe(space1); expect(getCurrentRoom()).toBe(room1); await deferred.promise; expect(store.activeSpace).toBe(space1); expect(getCurrentRoom()).toBe(room1); jest.runAllTimers(); expect(store.activeSpace).toBe(space1); expect(getCurrentRoom()).toBe(room1); }); describe("context switching tests", () => { let dispatcherRef: string; let currentRoom: Room | null = null; beforeEach(async () => { [room1, room2, orphan1].forEach(mkRoom); mkSpace(space1, [room1, room2]); mkSpace(space2, [room2]); await run(); dispatcherRef = defaultDispatcher.register((payload) => { if (payload.action === Action.ViewRoom || payload.action === Action.ViewHomePage) { currentRoom = payload.room_id || null; } }); }); afterEach(() => { localStorage.clear(); localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true"); defaultDispatcher.unregister(dispatcherRef); }); const getCurrentRoom = () => { jest.runOnlyPendingTimers(); return currentRoom; }; it("last viewed room in target space is the current viewed and in both spaces", async () => { store.setActiveSpace(space1); viewRoom(room2); store.setActiveSpace(space2); viewRoom(room2); store.setActiveSpace(space1); expect(getCurrentRoom()).toBe(room2); }); it("last viewed room in target space is in the current space", async () => { store.setActiveSpace(space1); viewRoom(room2); store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); store.setActiveSpace(space1); expect(getCurrentRoom()).toBe(room2); }); it("last viewed room in target space is not in the current space", async () => { store.setActiveSpace(space1); viewRoom(room1); store.setActiveSpace(space2); viewRoom(room2); store.setActiveSpace(space1); expect(getCurrentRoom()).toBe(room1); }); it("last viewed room is target space is not known", async () => { store.setActiveSpace(space1); viewRoom(room1); localStorage.setItem(`mx_space_context_${space2}`, orphan2); store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); }); it("last viewed room is target space is no longer in that space", async () => { store.setActiveSpace(space1); viewRoom(room1); localStorage.setItem(`mx_space_context_${space2}`, room1); store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); // Space home instead of room1 }); it("no last viewed room in target space", async () => { store.setActiveSpace(space1); viewRoom(room1); store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); }); it("no last viewed room in home space", async () => { store.setActiveSpace(space1); viewRoom(room1); store.setActiveSpace(MetaSpace.Home); expect(getCurrentRoom()).toBeNull(); // Home }); }); describe("space auto switching tests", () => { beforeEach(async () => { [room1, room2, room3, orphan1].forEach(mkRoom); mkSpace(space1, [room1, room2, room3]); mkSpace(space2, [room1, room2]); const cliRoom2 = client.getRoom(room2)!; mocked(cliRoom2.currentState).getStateEvents.mockImplementation( testUtils.mockStateEventImplementation([ mkEvent({ event: true, type: EventType.SpaceParent, room: room2, user: testUserId, skey: space2, content: { via: [], canonical: true }, ts: Date.now(), }), ]), ); await run(); }); it("no switch required, room is in current space", async () => { viewRoom(room1); store.setActiveSpace(space1, false); viewRoom(room2); expect(store.activeSpace).toBe(space1); }); it("switch to canonical parent space for room", async () => { viewRoom(room1); store.setActiveSpace(space2, false); viewRoom(room2); expect(store.activeSpace).toBe(space2); }); it("switch to first containing space for room", async () => { viewRoom(room2); store.setActiveSpace(space2, false); viewRoom(room3); expect(store.activeSpace).toBe(space1); }); it("switch to other rooms for orphaned room", async () => { viewRoom(room1); store.setActiveSpace(space1, false); viewRoom(orphan1); expect(store.activeSpace).toBe(MetaSpace.Orphans); }); it("switch to first valid space when selected metaspace is disabled", async () => { store.setActiveSpace(MetaSpace.People, false); expect(store.activeSpace).toBe(MetaSpace.People); await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, { [MetaSpace.Home]: false, [MetaSpace.Favourites]: true, [MetaSpace.People]: false, [MetaSpace.Orphans]: true, }); jest.runAllTimers(); expect(store.activeSpace).toBe(MetaSpace.Orphans); }); it("when switching rooms in the all rooms home space don't switch to related space", async () => { await setShowAllRooms(true); viewRoom(room2); store.setActiveSpace(MetaSpace.Home, false); viewRoom(room1); expect(store.activeSpace).toBe(MetaSpace.Home); }); }); describe("traverseSpace", () => { beforeEach(() => { mkSpace("!a:server", [ mkSpace("!b:server", [ mkSpace("!c:server", [ "!a:server", mkRoom("!c-child:server").roomId, mkRoom("!shared-child:server").roomId, ]).roomId, mkRoom("!b-child:server").roomId, ]).roomId, mkRoom("!a-child:server").roomId, "!shared-child:server", ]); }); it("avoids cycles", () => { const fn = jest.fn(); store.traverseSpace("!b:server", fn); expect(fn).toHaveBeenCalledTimes(3); expect(fn).toHaveBeenCalledWith("!a:server"); expect(fn).toHaveBeenCalledWith("!b:server"); expect(fn).toHaveBeenCalledWith("!c:server"); }); it("including rooms", () => { const fn = jest.fn(); store.traverseSpace("!b:server", fn, true); expect(fn).toHaveBeenCalledTimes(8); // twice for shared-child expect(fn).toHaveBeenCalledWith("!a:server"); expect(fn).toHaveBeenCalledWith("!a-child:server"); expect(fn).toHaveBeenCalledWith("!b:server"); expect(fn).toHaveBeenCalledWith("!b-child:server"); expect(fn).toHaveBeenCalledWith("!c:server"); expect(fn).toHaveBeenCalledWith("!c-child:server"); expect(fn).toHaveBeenCalledWith("!shared-child:server"); }); it("excluding rooms", () => { const fn = jest.fn(); store.traverseSpace("!b:server", fn, false); expect(fn).toHaveBeenCalledTimes(3); expect(fn).toHaveBeenCalledWith("!a:server"); expect(fn).toHaveBeenCalledWith("!b:server"); expect(fn).toHaveBeenCalledWith("!c:server"); }); }); it("test user flow", async () => { // init the store await run(); await setShowAllRooms(false); // receive invite to space const rootSpace = mkSpace(space1, [room1, room2, space2]); rootSpace.getMyMembership.mockReturnValue(KnownMembership.Invite); client.emit(ClientEvent.Room, rootSpace); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.invitedSpaces).toStrictEqual([rootSpace]); expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([]); // accept invite to space rootSpace.getMyMembership.mockReturnValue(KnownMembership.Join); client.emit(RoomEvent.MyMembership, rootSpace, KnownMembership.Join, KnownMembership.Invite); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); // join room in space expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeFalsy(); const rootSpaceRoom1 = mkRoom(room1); rootSpaceRoom1.getMyMembership.mockReturnValue(KnownMembership.Join); client.emit(ClientEvent.Room, rootSpaceRoom1); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeTruthy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Favourites, room1)).toBeFalsy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.People, room1)).toBeFalsy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room1)).toBeFalsy(); // receive room invite expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeFalsy(); const rootSpaceRoom2 = mkRoom(room2); rootSpaceRoom2.getMyMembership.mockReturnValue(KnownMembership.Invite); client.emit(ClientEvent.Room, rootSpaceRoom2); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeTruthy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room2)).toBeTruthy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Favourites, room2)).toBeFalsy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.People, room2)).toBeFalsy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room2)).toBeFalsy(); // start DM in space const myRootSpaceMember = new RoomMember(space1, testUserId); myRootSpaceMember.membership = KnownMembership.Join; const rootSpaceFriend = new RoomMember(space1, dm1Partner.userId); rootSpaceFriend.membership = KnownMembership.Join; rootSpace.getMembers.mockReturnValue([myRootSpaceMember, rootSpaceFriend]); rootSpace.getMember.mockImplementation((userId) => { switch (userId) { case testUserId: return myRootSpaceMember; case dm1Partner.userId: return rootSpaceFriend; } return null; }); expect(SpaceStore.instance.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeFalsy(); const memberEvent = mkEvent({ event: true, type: EventType.RoomMember, content: { membership: KnownMembership.Join, }, skey: dm1Partner.userId, user: dm1Partner.userId, room: space1, }); client.emit(RoomStateEvent.Members, memberEvent, rootSpace.currentState, dm1Partner); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeTruthy(); const dm1Room = mkRoom(dm1); dm1Room.getMyMembership.mockReturnValue(KnownMembership.Join); client.emit(ClientEvent.Room, dm1Room); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); expect(SpaceStore.instance.isRoomInSpace(space1, dm1)).toBeTruthy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Favourites, dm1)).toBeFalsy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.People, dm1)).toBeTruthy(); expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, dm1)).toBeFalsy(); // join subspace const subspace = mkSpace(space2); subspace.getMyMembership.mockReturnValue(KnownMembership.Join); const prom = testUtils.emitPromise(SpaceStore.instance, space1); client.emit(ClientEvent.Room, subspace); jest.runOnlyPendingTimers(); expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); expect(SpaceStore.instance.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual([rootSpace.roomId]); await prom; }); it("correctly emits events for metaspace changes during onReady", async () => { // similar to useEventEmitterState, but for use inside of tests function testEventEmitterState( emitter: EventEmitter | undefined, eventName: string | symbol, callback: (...args: any[]) => void, ): () => void { callback(); emitter?.addListener(eventName, callback); return () => emitter?.removeListener(eventName, callback); } let metaSpaces; const removeListener = testEventEmitterState(store, UPDATE_TOP_LEVEL_SPACES, () => { metaSpaces = store.enabledMetaSpaces; }); expect(metaSpaces).toEqual(store.enabledMetaSpaces); await run(); expect(metaSpaces).toEqual(store.enabledMetaSpaces); removeListener(); }); describe("when feature_dynamic_room_predecessors is not enabled", () => { beforeAll(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation( (settingName) => settingName === "Spaces.allRoomsInHome", ); // @ts-ignore calling a private function SpaceStore.instance.onAction({ action: Action.SettingUpdated, settingName: "feature_dynamic_room_predecessors", roomId: null, level: SettingLevel.ACCOUNT, newValueAtLevel: SettingLevel.ACCOUNT, newValue: false, }); }); beforeEach(() => { jest.clearAllMocks(); }); it("passes that value in calls to getVisibleRooms and getRoomUpgradeHistory during startup", async () => { // When we create an instance, which calls onReady and rebuildSpaceHierarchy mkSpace("!dynspace:example.com", [mkRoom("!dynroom:example.com").roomId]); await run(); // Then we pass through the correct value of the feature flag to // everywhere that needs it. expect(client.getVisibleRooms).toHaveBeenCalledWith(false); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(true); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(); expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(expect.anything(), expect.anything(), false); expect(client.getRoomUpgradeHistory).not.toHaveBeenCalledWith(expect.anything(), expect.anything(), true); expect(client.getRoomUpgradeHistory).not.toHaveBeenCalledWith(expect.anything(), expect.anything()); }); it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => { // Given a store const store = SpaceStore.testInstance(); // When we ask for filtered room ids store.getSpaceFilteredRoomIds(MetaSpace.Home); // Then we pass the correct feature flag expect(client.getVisibleRooms).toHaveBeenCalledWith(false); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(true); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(); }); }); describe("when feature_dynamic_room_predecessors is enabled", () => { beforeAll(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation( (settingName) => settingName === "Spaces.allRoomsInHome" || settingName === "feature_dynamic_room_predecessors", ); // @ts-ignore calling a private function SpaceStore.instance.onAction({ action: Action.SettingUpdated, settingName: "feature_dynamic_room_predecessors", roomId: null, level: SettingLevel.ACCOUNT, newValueAtLevel: SettingLevel.ACCOUNT, newValue: true, }); }); beforeEach(() => { jest.clearAllMocks(); }); it("passes that value in calls to getVisibleRooms and getRoomUpgradeHistory during startup", async () => { // When we create an instance, which calls onReady and rebuildSpaceHierarchy mkSpace("!dynspace:example.com", [mkRoom("!dynroom:example.com").roomId]); await run(); // Then we pass through the correct value of the feature flag to // everywhere that needs it. expect(client.getVisibleRooms).toHaveBeenCalledWith(true); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(false); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(); expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(expect.anything(), expect.anything(), true); expect(client.getRoomUpgradeHistory).not.toHaveBeenCalledWith(expect.anything(), expect.anything(), false); expect(client.getRoomUpgradeHistory).not.toHaveBeenCalledWith(expect.anything(), expect.anything()); }); it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => { // Given a store const store = SpaceStore.testInstance(); // When we ask for filtered room ids store.getSpaceFilteredRoomIds(MetaSpace.Home); // Then we pass the correct feature flag expect(client.getVisibleRooms).toHaveBeenCalledWith(true); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(false); expect(client.getVisibleRooms).not.toHaveBeenCalledWith(); }); }); describe("setActiveRoomInSpace", () => { it("should work with Home as all rooms space", async () => { const room = mkRoom(room1); const state = RoomNotificationStateStore.instance.getRoomState(room); // @ts-ignore state._level = NotificationLevel.Notification; jest.spyOn(RoomListStore.instance, "orderedLists", "get").mockReturnValue({ [DefaultTagID.Untagged]: [room], }); // init the store await run(); await setShowAllRooms(true); store.setActiveRoomInSpace(MetaSpace.Home); expect(spyDispatcher).toHaveBeenCalledWith( expect.objectContaining({ action: "view_room", room_id: room.roomId, }), ); }); }); });