element-web/test/stores/SpaceStore-test.ts
Timo a24aa7e0f7
Video call meta space (#12297)
* add video room meta space button

Signed-off-by: Timo K <toger5@hotmail.de>

* Add videoRoomsSpace to meta space configuration

Signed-off-by: Timo K <toger5@hotmail.de>

* temp

Signed-off-by: Timo K <toger5@hotmail.de>

* dont show ppl section in video room space

Signed-off-by: Timo K <toger5@hotmail.de>

* add i18n strings

Signed-off-by: Timo K <toger5@hotmail.de>

* revert waitForIframe=false (this is part of another PR)

Signed-off-by: Timo K <toger5@hotmail.de>

* fix missing mock room method

Signed-off-by: Timo K <toger5@hotmail.de>

* test snapshot: add video room meta space

Signed-off-by: Timo K <toger5@hotmail.de>

* rename Conferences -> Conference

Signed-off-by: Timo K <toger5@hotmail.de>

* space panel snap test

Signed-off-by: Timo K <toger5@hotmail.de>

* update snapshot

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* add video room space tests

Signed-off-by: Timo K <toger5@hotmail.de>

* better logic

Signed-off-by: Timo K <toger5@hotmail.de>

* Add Video MetaSpace Test

Signed-off-by: Timo K <toger5@hotmail.de>

* make room join rule update reactive for the video room meta space

Signed-off-by: Timo K <toger5@hotmail.de>

* temp

Signed-off-by: Timo K <toger5@hotmail.de>

* fix description for meta space video room settings

Signed-off-by: Timo K <toger5@hotmail.de>

* tests

Signed-off-by: Timo K <toger5@hotmail.de>

* update snapshot

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* i18n

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests

Signed-off-by: Timo K <toger5@hotmail.de>

* put video meta space behind "feature_video_rooms" labs flag

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* update space store on RoomCreate state event

Signed-off-by: Timo K <toger5@hotmail.de>

* test for updating video room space on room type update

Signed-off-by: Timo K <toger5@hotmail.de>

* remove comment

Signed-off-by: Timo K <toger5@hotmail.de>

* also make knock join rule rooms part of the conference section

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-03-25 18:35:31 +00:00

1524 lines
67 KiB
TypeScript

/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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<boolean>();
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<boolean>;
});
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,
}),
);
});
});
});