* Add new settings to enable/disable the sections * Add `RoomList.showSections` settings to preference user settings * Hide sections ui in room list header - Hide the collapse/expand button and the *new section* entry in the menu. - Display the *start chat* button instead of the compose menu when the only action available is to create a DM * Hide sections ui in context menu of room item * Listen to show section settings in room list item vm * Listen to show sections setting in room list header vm * Put all the rooms inside the chat section when sections are disabled When the chat section is the only section, the room list is in "flat list mode". Putting all the rooms inside the chat section. The section filters (aka custom section, favourites etc) are not passed to the skip list. * Update preferences settings screenshot * Add e2e test for RoomList.showSections toggle * Hide "Chat moved" toast when a room is tagged when RoomList.showSections is disabled
711 lines
28 KiB
TypeScript
711 lines
28 KiB
TypeScript
/*
|
|
* 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 from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
|
import * as tagRoomModule from "../../../src/utils/room/tagRoom";
|
|
import { CHATS_TAG } from "../../../src/stores/room-list-v3/section";
|
|
|
|
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 [];
|
|
if (setting === "RoomList.CustomSectionData") return {};
|
|
return false;
|
|
});
|
|
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
|
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", 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");
|
|
});
|
|
|
|
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 not 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");
|
|
|
|
// Record the snapshot version before the participant event fires
|
|
const snapshotBefore = viewModel.getSnapshot();
|
|
|
|
// 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);
|
|
|
|
// Snapshot should not have changed
|
|
expect(viewModel.getSnapshot()).toBe(snapshotBefore);
|
|
});
|
|
|
|
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("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", () => {
|
|
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 } };
|
|
|
|
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 === "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";
|
|
});
|
|
|
|
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([]);
|
|
});
|
|
|
|
it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => {
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
|
if (setting === "RoomList.showSections") return true;
|
|
return false;
|
|
});
|
|
|
|
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
|
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true);
|
|
});
|
|
|
|
it("should update areSectionsEnabled when RoomList.showSections setting changes", () => {
|
|
let watchCallback: CallbackFn<"RoomList.showSections"> = () => {};
|
|
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
|
|
if (setting === "RoomList.showSections") watchCallback = callback;
|
|
return "watcher-id";
|
|
});
|
|
|
|
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
|
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(false);
|
|
|
|
// Enable sections
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
|
if (setting === "RoomList.showSections") return true;
|
|
return false;
|
|
});
|
|
watchCallback("RoomList.showSections", null, null as any, null, null);
|
|
|
|
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|