feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { JoinRule, type MatrixClient, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RoomListHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListHeaderViewModel";
|
||||
import { MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces";
|
||||
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { SortingAlgorithm } from "../../../src/stores/room-list-v3/skip-list/sorters";
|
||||
import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import {
|
||||
shouldShowSpaceSettings,
|
||||
showCreateNewRoom,
|
||||
showSpaceInvite,
|
||||
showSpacePreferences,
|
||||
showSpaceSettings,
|
||||
} from "../../../src/utils/space";
|
||||
import { createTestClient, mkSpace } from "../../test-utils";
|
||||
import { createRoom, hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
|
||||
import PosthogTrackers from "../../../src/PosthogTrackers";
|
||||
|
||||
jest.mock("../../../src/PosthogTrackers", () => ({
|
||||
trackInteraction: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/utils/space", () => ({
|
||||
shouldShowSpaceSettings: jest.fn(),
|
||||
showCreateNewRoom: jest.fn(),
|
||||
showSpaceInvite: jest.fn(),
|
||||
showSpacePreferences: jest.fn(),
|
||||
showSpaceSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
createRoom: jest.fn(),
|
||||
hasCreateRoomRights: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomListHeaderViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let mockSpace: Room;
|
||||
let vm: RoomListHeaderViewModel;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
|
||||
mockSpace = mkSpace(matrixClient, "!space:server");
|
||||
|
||||
mocked(hasCreateRoomRights).mockReturnValue(true);
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.preferredSorting") return SortingAlgorithm.Recency;
|
||||
if (settingName === "feature_video_rooms") return true;
|
||||
if (settingName === "feature_element_call_video_rooms") return true;
|
||||
if (settingName === "RoomList.OrderedCustomSections") return [];
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
describe("snapshot", () => {
|
||||
it("should compute snapshot for Home space", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(MetaSpace.Home);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.title).toBe("Home");
|
||||
expect(snapshot.displayComposeMenu).toBe(true);
|
||||
expect(snapshot.displaySpaceMenu).toBe(false);
|
||||
expect(snapshot.canCreateRoom).toBe(true);
|
||||
expect(snapshot.canCreateVideoRoom).toBe(true);
|
||||
expect(snapshot.activeSortOption).toBe("recent");
|
||||
});
|
||||
|
||||
it("should compute snapshot for active space", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.title).toBe(mockSpace.roomId);
|
||||
});
|
||||
|
||||
it("should hide video room option when feature is disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "feature_video_rooms") return false;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canCreateVideoRoom).toBe(false);
|
||||
});
|
||||
|
||||
it("should show alphabetical sort option when RoomList.preferredSorting is Alphabetic", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.preferredSorting") return SortingAlgorithm.Alphabetic;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().activeSortOption).toBe("alphabetical");
|
||||
});
|
||||
|
||||
it("should hide compose menu when user cannot create rooms", () => {
|
||||
mocked(hasCreateRoomRights).mockReturnValue(false);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.displayComposeMenu).toBe(false);
|
||||
expect(snapshot.canCreateRoom).toBe(false);
|
||||
});
|
||||
|
||||
it("should show invite option when space is public", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
jest.spyOn(mockSpace, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canInviteInSpace).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide invite option when user cannot invite", () => {
|
||||
mocked(mockSpace.canInvite).mockReturnValue(false);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canInviteInSpace).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide space settings when user cannot access them", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(false);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canAccessSpaceSettings).toBe(false);
|
||||
});
|
||||
|
||||
it("should show message preview when RoomList.showMessagePreview is enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.showMessagePreview") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, true, false],
|
||||
[false, false, true],
|
||||
])(
|
||||
"when feature_room_list_sections is %s: canCreateSection=%s, useComposeIcon=%s",
|
||||
(featureEnabled, expectedCanCreateSection, expectedUseComposeIcon) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "feature_room_list_sections") return featureEnabled;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canCreateSection).toBe(expectedCanCreateSection);
|
||||
expect(vm.getSnapshot().useComposeIcon).toBe(expectedUseComposeIcon);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("event listeners", () => {
|
||||
it.each([UPDATE_SELECTED_SPACE, UPDATE_HOME_BEHAVIOUR])(
|
||||
"should update snapshot when %s event is emitted",
|
||||
(event) => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(MetaSpace.Home);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
SpaceStore.instance.emit(event);
|
||||
|
||||
expect(vm.getSnapshot().title).toBe(mockSpace.roomId);
|
||||
},
|
||||
);
|
||||
|
||||
it("should update snapshot when space name changes", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
mockSpace.name = "new name";
|
||||
mockSpace.emit(RoomEvent.Name, mockSpace);
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("new name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("actions", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
});
|
||||
|
||||
it("should fire CreateChat action when createChatRoom is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
vm.createChatRoom(new Event("click"));
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.CreateChat);
|
||||
});
|
||||
|
||||
it("should call createRoom with active space when in a space", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.createRoom(new Event("click"));
|
||||
|
||||
expect(createRoom).toHaveBeenCalledWith(mockSpace);
|
||||
});
|
||||
|
||||
it("should show create video room dialog for space when createVideoRoom is called", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "feature_element_call_video_rooms") return false;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.createVideoRoom();
|
||||
expect(showCreateNewRoom).toHaveBeenCalledWith(mockSpace, RoomType.ElementVideo);
|
||||
});
|
||||
|
||||
it("should use UnstableCall type when element_call_video_rooms is enabled", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
|
||||
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.createVideoRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CreateRoom,
|
||||
type: RoomType.UnstableCall,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch ViewRoom action when openSpaceHome is called", () => {
|
||||
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.openSpaceHome();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "!space:server",
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should show space invite dialog when inviteInSpace is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.inviteInSpace();
|
||||
|
||||
expect(showSpaceInvite).toHaveBeenCalledWith(mockSpace);
|
||||
});
|
||||
|
||||
it("should show space preferences dialog when openSpacePreferences is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.openSpacePreferences();
|
||||
|
||||
expect(showSpacePreferences).toHaveBeenCalledWith(mockSpace);
|
||||
});
|
||||
|
||||
it("should show space settings dialog when openSpaceSettings is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.openSpaceSettings();
|
||||
|
||||
expect(showSpaceSettings).toHaveBeenCalledWith(mockSpace);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["recent" as const, SortingAlgorithm.Recency],
|
||||
["alphabetical" as const, SortingAlgorithm.Alphabetic],
|
||||
["unread-first" as const, SortingAlgorithm.Unread],
|
||||
])("should resort when sort is called with '%s'", (option, expectedAlgorithm) => {
|
||||
const resortSpy = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn());
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.sort(option);
|
||||
expect(resortSpy).toHaveBeenCalledWith(expectedAlgorithm);
|
||||
});
|
||||
|
||||
it("should track analytics on resort", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "activeSortAlgorithm", "get").mockReturnValue(
|
||||
SortingAlgorithm.Alphabetic,
|
||||
);
|
||||
PosthogTrackers.trackRoomListSortingAlgorithmChange = jest.fn();
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn());
|
||||
vm.sort("unread-first");
|
||||
|
||||
expect(PosthogTrackers.trackRoomListSortingAlgorithmChange).toHaveBeenCalledWith(
|
||||
SortingAlgorithm.Alphabetic,
|
||||
SortingAlgorithm.Unread,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call createSection on RoomListStoreV3 when createSection is called", () => {
|
||||
const createSectionSpy = jest
|
||||
.spyOn(RoomListStoreV3.instance, "createSection")
|
||||
.mockResolvedValue("element.io.section.work");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.createSection();
|
||||
expect(createSectionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("collapseOrExpandSections", () => {
|
||||
it("should dispatch RoomListCollapseAllSections when collapseSections is not 'expand'", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
vm.collapseOrExpandSections();
|
||||
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.RoomListCollapseAllSections);
|
||||
});
|
||||
|
||||
it("should dispatch RoomListExpandAllSections when collapseSections is 'expand'", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
// Drive the VM into the "expand" state by simulating all sections collapsed
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(vm.getSnapshot().collapseSections).toBe("expand");
|
||||
vm.collapseOrExpandSections();
|
||||
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.RoomListExpandAllSections);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RoomListSectionsCollapseStateChanged handling", () => {
|
||||
it("should set collapseSections to 'expand' when collapseSections is collapse", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().collapseSections).toBe("expand");
|
||||
});
|
||||
|
||||
it("should set collapseSections to 'collapse' when collapseSections is expand", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "expand",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().collapseSections).toBe("collapse");
|
||||
});
|
||||
|
||||
it("should set collapseSections to undefined when collapseSections is undefined", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
// First drive it into a non-undefined state
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(vm.getSnapshot().collapseSections).toBe("expand");
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: undefined,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().collapseSections).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should toggle message preview from enabled to disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.showMessagePreview") return true;
|
||||
return false;
|
||||
});
|
||||
const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockImplementation(jest.fn());
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true);
|
||||
|
||||
vm.toggleMessagePreview();
|
||||
|
||||
expect(setValueSpy).toHaveBeenCalledWith("RoomList.showMessagePreview", null, expect.anything(), false);
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,711 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
PendingEventOrdering,
|
||||
type RoomMember,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, flushPromises } from "../../test-utils";
|
||||
import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
|
||||
import { type MessagePreview, MessagePreviewStore } from "../../../src/stores/message-preview";
|
||||
import SettingsStore, { type CallbackFn } from "../../../src/settings/SettingsStore";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
|
||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { CallEvent, type Call } from "../../../src/models/Call";
|
||||
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
|
||||
import RoomListStoreV3, { CHATS_TAG } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import * as tagRoomModule from "../../../src/utils/room/tagRoom";
|
||||
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(true),
|
||||
hasAccessToNotificationMenu: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/stores/CallStore", () => ({
|
||||
__esModule: true,
|
||||
CallStore: {
|
||||
instance: {
|
||||
getCall: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
},
|
||||
CallStoreEvent: {
|
||||
ConnectedCalls: "connected_calls",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("RoomListItemViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
let notificationState: RoomNotificationState;
|
||||
let viewModel: RoomListItemViewModel;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
room = new Room("!room:server", matrixClient, matrixClient.getSafeUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
// Set room name
|
||||
room.name = "Test Room";
|
||||
|
||||
notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn().mockReturnValue(undefined),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.showMessagePreview") return false;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
||||
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||
jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
viewModel?.dispose();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Initialization", () => {
|
||||
it("should initialize with room data", async () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
// Wait for async initialization
|
||||
await flushPromises();
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.id).toBe("!room:server");
|
||||
expect(snapshot.name).toBe("Test Room");
|
||||
});
|
||||
|
||||
it("should load message preview when enabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Hello world!",
|
||||
} as MessagePreview);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
// Wait for async message preview load
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().messagePreview).toBe("Hello world!");
|
||||
});
|
||||
|
||||
it("should not load message preview when disabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().messagePreview).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Notification state", () => {
|
||||
it("should reflect notification state", async () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(5);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.notification.hasAnyNotificationOrActivity).toBe(true);
|
||||
expect(snapshot.notification.count).toBe(5);
|
||||
});
|
||||
|
||||
it("should update when notification state changes", async () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().notification.count).toBe(0);
|
||||
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(3);
|
||||
notificationState.emit(NotificationStateEvents.Update);
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().notification.count).toBe(3);
|
||||
});
|
||||
|
||||
it("should show bold text when has notifications", async () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().isBold).toBe(true);
|
||||
});
|
||||
|
||||
it("should show mention badge", async () => {
|
||||
jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.isMention).toBe(true);
|
||||
});
|
||||
|
||||
it("should show invitation state", async () => {
|
||||
jest.spyOn(notificationState, "invited", "get").mockReturnValue(true);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.invited).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Message preview", () => {
|
||||
it("should update message preview when store emits update", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Initial message",
|
||||
} as MessagePreview);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().messagePreview).toBe("Initial message");
|
||||
|
||||
// Update preview
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Updated message",
|
||||
} as MessagePreview);
|
||||
|
||||
MessagePreviewStore.instance.emit(MessagePreviewStore.getPreviewChangedEventName(room));
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().messagePreview).toBe("Updated message");
|
||||
});
|
||||
|
||||
it("should show/hide preview when setting changes", async () => {
|
||||
let showPreview = false;
|
||||
let watchCallback: any;
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => showPreview);
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
|
||||
if (setting === "RoomList.showMessagePreview") watchCallback = callback;
|
||||
return "watcher-id";
|
||||
});
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Test message",
|
||||
} as MessagePreview);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().messagePreview).toBeUndefined();
|
||||
|
||||
// Enable previews
|
||||
showPreview = true;
|
||||
watchCallback(null, "device", true);
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().messagePreview).toBe("Test message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room tags", () => {
|
||||
it("should reflect favorite tag", async () => {
|
||||
room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().isFavourite).toBe(true);
|
||||
});
|
||||
|
||||
it("should reflect low priority tag", async () => {
|
||||
room.tags = { [DefaultTagID.LowPriority]: { order: 0 } };
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().isLowPriority).toBe(true);
|
||||
});
|
||||
|
||||
it("should update when room tags change", async () => {
|
||||
room.tags = {};
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().isFavourite).toBe(false);
|
||||
|
||||
room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
|
||||
const tagEvent = {
|
||||
getContent: () => ({ tags: { [DefaultTagID.Favourite]: { order: 0 } } }),
|
||||
} as MatrixEvent;
|
||||
room.emit(RoomEvent.Tags, tagEvent, room);
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().isFavourite).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Call state", () => {
|
||||
it("should show voice call indicator and participant data", async () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([[matrixClient.getUserId()!, {}]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
expect(viewModel.getSnapshot().callParticipants).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
userId: matrixClient.getUserId()!,
|
||||
displayName: matrixClient.getUserId()!,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show video call indicator", async () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Video,
|
||||
participants: new Map([[matrixClient.getUserId()!, {}]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("video");
|
||||
});
|
||||
|
||||
it("should not show call indicator when no participants", async () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map(),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should listen to call participant changes", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map(),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().notification.callType).toBeUndefined();
|
||||
|
||||
// Get the callback registered for call state changes
|
||||
const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls;
|
||||
const callStateCallback = mockCalls[mockCalls.length - 1][1];
|
||||
callStateCallback();
|
||||
|
||||
// Simulate participant joining
|
||||
mockCall.participants.set(matrixClient.getUserId()! as unknown as RoomMember, new Set());
|
||||
|
||||
// Get the callback registered for participant changes
|
||||
const participantsChangeCallback = mockCall.on.mock.calls[0][1];
|
||||
participantsChangeCallback();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
});
|
||||
|
||||
it("should update the item when there is already an active call and participants join", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([[matrixClient.getUserId()! as unknown as RoomMember, new Set<string>()]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
// Trigger onCallStateChanged so the call is tracked and the participant listener is registered
|
||||
const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls;
|
||||
const callStateCallback = mockCalls[mockCalls.length - 1][1];
|
||||
callStateCallback();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
|
||||
// Simulate another participant joining while the call is already active
|
||||
mockCall.participants.set("@other:server" as unknown as RoomMember, new Set<string>());
|
||||
const participantsChangeCallback = mockCall.on.mock.calls[0][1];
|
||||
participantsChangeCallback(mockCall.participants);
|
||||
|
||||
expect(viewModel.getSnapshot().callParticipants).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ userId: "@other:server", displayName: "@other:server" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should react to participant changes when a call already exists at instantiation time", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().notification.callType).toBeUndefined();
|
||||
|
||||
// Simulate participant joining
|
||||
mockCall.participants.set(matrixClient.getUserId()! as unknown as RoomMember, new Set());
|
||||
|
||||
// Get the callback registered for participant changes
|
||||
const participantsChangeCallback = mockCall.on.mock.calls[0][1];
|
||||
participantsChangeCallback();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
});
|
||||
|
||||
it("should unsubscribe from old call participants when the call changes", () => {
|
||||
const firstCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([[matrixClient.getUserId()! as unknown as RoomMember, new Set<string>()]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
const secondCall = {
|
||||
callType: CallType.Video,
|
||||
participants: new Map([[matrixClient.getUserId()! as unknown as RoomMember, new Set<string>()]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(firstCall as unknown as Call);
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
// Trigger onCallStateChanged to register the first call
|
||||
const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls;
|
||||
const callStateCallback = mockCalls[mockCalls.length - 1][1];
|
||||
callStateCallback();
|
||||
|
||||
const participantsCallback = firstCall.on.mock.calls[0][1];
|
||||
expect(firstCall.on).toHaveBeenCalledWith("participants", participantsCallback);
|
||||
|
||||
// Now switch to a different call
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(secondCall as unknown as Call);
|
||||
callStateCallback();
|
||||
|
||||
// The old call's listener must have been removed
|
||||
expect(firstCall.off).toHaveBeenCalledWith("participants", participantsCallback);
|
||||
// The new call must have a listener registered
|
||||
expect(secondCall.on).toHaveBeenCalledWith("participants", expect.any(Function));
|
||||
});
|
||||
|
||||
it("should listen to call type changes", async () => {
|
||||
// Start with a voice call
|
||||
let callType = CallType.Voice;
|
||||
const mockCall = new (class extends EventEmitter {
|
||||
get callType() {
|
||||
return callType;
|
||||
}
|
||||
participants = new Map([[matrixClient.getUserId()!, {}]]);
|
||||
})() as unknown as Call;
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
|
||||
// Now turn it into a video call
|
||||
callType = CallType.Video;
|
||||
mockCall.emit(CallEvent.CallTypeChanged, callType);
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("video");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room name updates", () => {
|
||||
it("should update when room name changes", async () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().name).toBe("Test Room");
|
||||
|
||||
room.name = "Updated Room";
|
||||
room.emit(RoomEvent.Name, room);
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().name).toBe("Updated Room");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DM detection", () => {
|
||||
it("should detect DM rooms", async () => {
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
jest.spyOn(dmRoomMap, "getUserIdForRoomId").mockReturnValue("@user:server");
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// DM rooms should not show copy room link option
|
||||
expect(viewModel.getSnapshot().canCopyRoomLink).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect non-DM rooms", async () => {
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
jest.spyOn(dmRoomMap, "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().canCopyRoomLink).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canMoveToSection", () => {
|
||||
it.each([
|
||||
[true, true],
|
||||
[false, false],
|
||||
])("should be %s when feature_room_list_sections is %s", (featureEnabled, expected) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return featureEnabled;
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().canMoveToSection).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Actions", () => {
|
||||
it("should dispatch view room action on openRoom", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.onOpenRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "!room:server",
|
||||
metricsTrigger: "RoomList",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return room object", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().room).toBe(room);
|
||||
});
|
||||
|
||||
it("should dispatch view_invite action when onInvite is called", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.onInvite();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: "view_invite",
|
||||
roomId: "!room:server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch copy_room action when onCopyRoomLink is called", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.onCopyRoomLink();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: "copy_room",
|
||||
room_id: "!room:server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch leave_room action when onLeaveRoom is called for normal room", () => {
|
||||
room.tags = {};
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.onLeaveRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: "leave_room",
|
||||
room_id: "!room:server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch forget_room action when onLeaveRoom is called for archived room", () => {
|
||||
room.tags = { [DefaultTagID.Archived]: { order: 0 } };
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.onLeaveRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: "forget_room",
|
||||
room_id: "!room:server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should call createSection on RoomListStoreV3 when onCreateSection is called", async () => {
|
||||
const createSectionSpy = jest
|
||||
.spyOn(RoomListStoreV3.instance, "createSection")
|
||||
.mockResolvedValue("element.io.section.work");
|
||||
const tagRoomSpy = jest.spyOn(tagRoomModule, "tagRoom").mockImplementation(() => {});
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
viewModel.onCreateSection();
|
||||
expect(createSectionSpy).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => expect(tagRoomSpy).toHaveBeenCalledWith(room, "element.io.section.work"));
|
||||
});
|
||||
|
||||
it("should call tagRoom when onToggleSection is called", () => {
|
||||
const tagRoomSpy = jest.spyOn(tagRoomModule, "tagRoom").mockImplementation(() => {});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
viewModel.onToggleSection(DefaultTagID.Favourite);
|
||||
|
||||
expect(tagRoomSpy).toHaveBeenCalledWith(room, DefaultTagID.Favourite);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sections", () => {
|
||||
const customTag = "element.io.section.custom1";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([
|
||||
DefaultTagID.Favourite,
|
||||
customTag,
|
||||
CHATS_TAG,
|
||||
DefaultTagID.LowPriority,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should include sections from orderedSectionTags excluding CHATS_TAG, favourite, and low priority", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const sections = viewModel.getSnapshot().sections;
|
||||
expect(sections.map((s) => s.tag)).toEqual([customTag]);
|
||||
});
|
||||
|
||||
it("should mark the room current section as selected", () => {
|
||||
room.tags = { [customTag]: { order: 0 } };
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const sections = viewModel.getSnapshot().sections;
|
||||
expect(sections.find((s) => s.tag === customTag)?.isSelected).toBe(true);
|
||||
});
|
||||
|
||||
it("should use custom section name from CustomSectionData", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.CustomSectionData")
|
||||
return { [customTag]: { name: "My Custom Section", tag: customTag } };
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const section = viewModel.getSnapshot().sections.find((s) => s.tag === customTag);
|
||||
expect(section?.name).toBe("My Custom Section");
|
||||
});
|
||||
|
||||
it("should update sections when OrderedCustomSections setting changes", () => {
|
||||
let watchCallback: CallbackFn<"RoomList.OrderedCustomSections"> = () => {};
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
|
||||
if (setting === "RoomList.OrderedCustomSections") watchCallback = callback;
|
||||
return "watcher-id";
|
||||
});
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().sections).toHaveLength(1);
|
||||
|
||||
// Simulate reordering: custom section removed
|
||||
jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([
|
||||
DefaultTagID.Favourite,
|
||||
CHATS_TAG,
|
||||
DefaultTagID.LowPriority,
|
||||
]);
|
||||
watchCallback("RoomList.OrderedCustomSections", null, null as any, null, null);
|
||||
|
||||
expect(viewModel.getSnapshot().sections.map((s) => s.tag)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cleanup", () => {
|
||||
it("should unsubscribe from all events on dispose", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const offSpy = jest.spyOn(notificationState, "off");
|
||||
|
||||
viewModel.dispose();
|
||||
|
||||
expect(offSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { RoomListSearchViewModel } from "../../../src/viewmodels/room-list/RoomListSearchViewModel";
|
||||
import { MetaSpace } from "../../../src/stores/spaces";
|
||||
import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../src/LegacyCallHandler";
|
||||
|
||||
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/PosthogTrackers", () => ({
|
||||
trackInteraction: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomListSearchViewModel", () => {
|
||||
beforeEach(() => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("snapshot", () => {
|
||||
it("should show explore button in Home space when UIComponent.ExploreRooms is enabled", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
expect(vm.getSnapshot().displayExploreButton).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide explore button when not in Home space", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.VideoRooms });
|
||||
|
||||
expect(vm.getSnapshot().displayExploreButton).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide explore button when UIComponent.ExploreRooms is disabled", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
expect(vm.getSnapshot().displayExploreButton).toBe(false);
|
||||
});
|
||||
|
||||
it("should show dial button when PSTN protocol is supported", () => {
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide dial button when PSTN protocol is not supported", () => {
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("actions", () => {
|
||||
it("should fire OpenSpotlight action when onSearchClick is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
vm.onSearchClick();
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.OpenSpotlight);
|
||||
});
|
||||
|
||||
it("should fire OpenDialPad action when onDialPadClick is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
vm.onDialPadClick();
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.OpenDialPad);
|
||||
});
|
||||
|
||||
it("should fire ViewRoomDirectory action and track interaction when onExploreClick is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
const mockEvent = {} as React.MouseEvent<HTMLButtonElement>;
|
||||
vm.onExploreClick(mockEvent);
|
||||
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.ViewRoomDirectory);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update snapshot when PSTN protocol support changes", () => {
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(false);
|
||||
|
||||
// Simulate PSTN protocol support change
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(true);
|
||||
LegacyCallHandler.instance.emit(LegacyCallHandlerEvent.ProtocolSupport);
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(true);
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RoomListSectionHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListSectionHeaderViewModel";
|
||||
import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
|
||||
import { createTestClient, mkRoom } from "../../test-utils";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import RoomListStoreV3, { CHATS_TAG } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
|
||||
|
||||
describe("RoomListSectionHeaderViewModel", () => {
|
||||
let onToggleExpanded: jest.Mock;
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
onToggleExpanded = jest.fn();
|
||||
matrixClient = createTestClient();
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockReturnValue("watcher-id");
|
||||
jest.spyOn(SettingsStore, "unwatchSetting").mockReturnValue(undefined);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should initialize snapshot from props", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.id).toBe("m.favourite");
|
||||
expect(snapshot.title).toBe("Favourites");
|
||||
expect(snapshot.isExpanded).toBe(true);
|
||||
});
|
||||
|
||||
it("should toggle expanded state on click", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
expect(vm.isExpanded).toBe(true);
|
||||
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(false);
|
||||
expect(vm.getSnapshot().isExpanded).toBe(false);
|
||||
expect(onToggleExpanded).toHaveBeenCalledWith(false);
|
||||
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(true);
|
||||
expect(vm.getSnapshot().isExpanded).toBe(true);
|
||||
expect(onToggleExpanded).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should track expanded state per space", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
// Default space: collapse
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(false);
|
||||
|
||||
// Switch to a different space: should default to expanded
|
||||
vm.setSpace("!space2:server");
|
||||
expect(vm.isExpanded).toBe(true);
|
||||
|
||||
// Collapse in the new space
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(false);
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(true);
|
||||
|
||||
// Switch to the other space: should still be collapsed
|
||||
vm.setSpace("!space:server");
|
||||
expect(vm.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
describe("displaySectionMenu", () => {
|
||||
it.each([
|
||||
[DefaultTagID.Favourite, false],
|
||||
[DefaultTagID.LowPriority, false],
|
||||
[CHATS_TAG, false],
|
||||
["element.io.section.custom", true],
|
||||
])("should be %s for tag %s", (tag, expected) => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "Section",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
expect(vm.getSnapshot().displaySectionMenu).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCustomSectionDataChange", () => {
|
||||
let watchCallback: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => {
|
||||
if (settingName === "RoomList.CustomSectionData") watchCallback = callback as () => void;
|
||||
return "watcher-id";
|
||||
});
|
||||
});
|
||||
|
||||
it("should update title when custom section data changes", () => {
|
||||
const tag = "element.io.section.custom";
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "Old Title",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
expect(vm.getSnapshot().title).toBe("Old Title");
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({ [tag]: { tag, name: "New Title" } });
|
||||
watchCallback();
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("New Title");
|
||||
});
|
||||
|
||||
it("should not update title when section data is missing", () => {
|
||||
const tag = "element.io.section.custom";
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "My Section",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
||||
watchCallback();
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("My Section");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editSection", () => {
|
||||
it("should delegate to RoomListStoreV3.instance.editSection", async () => {
|
||||
const editSectionSpy = jest.spyOn(RoomListStoreV3.instance, "editSection").mockResolvedValue(undefined);
|
||||
const tag = "element.io.section.custom";
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "Section",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
await vm.editSection();
|
||||
expect(editSectionSpy).toHaveBeenCalledWith(tag);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeSection", () => {
|
||||
beforeEach(() => {
|
||||
const mockState = {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
hasAnyNotificationOrActivity: false,
|
||||
} as unknown as RoomNotificationState;
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(mockState);
|
||||
});
|
||||
|
||||
it("should delegate to RoomListStoreV3.instance.removeSection with isEmpty=true when no rooms", async () => {
|
||||
const removeSectionSpy = jest.spyOn(RoomListStoreV3.instance, "removeSection").mockResolvedValue(undefined);
|
||||
const tag = "element.io.section.custom";
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "Section",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
await vm.removeSection();
|
||||
expect(removeSectionSpy).toHaveBeenCalledWith(tag, true);
|
||||
});
|
||||
|
||||
it("should delegate to RoomListStoreV3.instance.removeSection with isEmpty=false when rooms exist", async () => {
|
||||
const removeSectionSpy = jest.spyOn(RoomListStoreV3.instance, "removeSection").mockResolvedValue(undefined);
|
||||
const tag = "element.io.section.custom";
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "Section",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([mkRoom(matrixClient, "!room:server")]);
|
||||
|
||||
await vm.removeSection();
|
||||
expect(removeSectionSpy).toHaveBeenCalledWith(tag, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unread status", () => {
|
||||
let room: Room;
|
||||
let notificationState: RoomNotificationState;
|
||||
|
||||
beforeEach(() => {
|
||||
room = mkRoom(matrixClient, "!room:server");
|
||||
notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
});
|
||||
|
||||
it("should set isUnread to false when no rooms have notifications", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().isUnread).toBe(false);
|
||||
});
|
||||
|
||||
it("should set isUnread to true when a room has notifications", () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().isUnread).toBe(true);
|
||||
});
|
||||
|
||||
it("should subscribe to new rooms and unsubscribe from removed rooms", () => {
|
||||
const room2 = mkRoom(matrixClient, "!room2:server");
|
||||
const notificationState2 = new RoomNotificationState(room2, false);
|
||||
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState")
|
||||
.mockReturnValueOnce(notificationState)
|
||||
.mockReturnValue(notificationState2);
|
||||
|
||||
jest.spyOn(notificationState, "on");
|
||||
jest.spyOn(notificationState, "off");
|
||||
jest.spyOn(notificationState2, "on");
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(notificationState.on).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
|
||||
|
||||
vm.setRooms([room2]);
|
||||
|
||||
expect(notificationState.off).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
|
||||
expect(notificationState2.on).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
|
||||
|
||||
// Calling setRooms again with the same room should not re-subscribe
|
||||
vm.setRooms([room2]);
|
||||
expect(notificationState2.on).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should update isUnread when a notification state update event fires", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().isUnread).toBe(false);
|
||||
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
notificationState.emit(NotificationStateEvents.Update);
|
||||
|
||||
expect(vm.getSnapshot().isUnread).toBe(true);
|
||||
});
|
||||
|
||||
it("should unsubscribe from all notification states on dispose", () => {
|
||||
jest.spyOn(notificationState, "off");
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
vm.dispose();
|
||||
expect(notificationState.off).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import type { MatrixClient, Room, RoomState } from "matrix-js-sdk/src/matrix";
|
||||
import { createTestClient, mkStubRoom } from "../../test-utils";
|
||||
import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { showCreateNewRoom } from "../../../src/utils/space";
|
||||
import { hasCreateRoomRights, createRoom, hasAccessToNotificationMenu } from "../../../src/viewmodels/room-list/utils";
|
||||
|
||||
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/utils/space", () => ({
|
||||
showCreateNewRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("utils", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let space: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
space = mkStubRoom("spaceId", "spaceName", matrixClient);
|
||||
});
|
||||
|
||||
describe("createRoom", () => {
|
||||
it("should fire Action.CreateRoom when createRoom is called without a space", async () => {
|
||||
const spy = jest.spyOn(defaultDispatcher, "fire");
|
||||
await createRoom();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(Action.CreateRoom);
|
||||
});
|
||||
|
||||
it("should call showCreateNewRoom when createRoom is called in a space", async () => {
|
||||
await createRoom(space);
|
||||
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasCreateRoomRights", () => {
|
||||
it("should return false when UIComponent.CreateRooms is disabled", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
expect(hasCreateRoomRights(matrixClient, space)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when UIComponent.CreateRooms is enabled and no space", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
expect(hasCreateRoomRights(matrixClient)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false in space when UIComponent.CreateRooms is enabled and the user doesn't have the rights", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
jest.spyOn(space.getLiveTimeline(), "getState").mockReturnValue({
|
||||
maySendStateEvent: jest.fn().mockReturnValue(true),
|
||||
} as unknown as RoomState);
|
||||
|
||||
expect(hasCreateRoomRights(matrixClient)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("hasAccessToNotificationMenu", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
const room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
const isGuest = false;
|
||||
const isArchived = false;
|
||||
|
||||
expect(hasAccessToNotificationMenu(room, isGuest, isArchived)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user