element-web/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts
Florian Duros 121c2d18e9
Room list: fix expanded/collapse state of sections (#33074)
* fix: section being empty in flat list mode

When switching space (or removing a section later), if the Chat section
is collpased and the room list is in flat list mode in the other space,
the room list is empty.

The fix forces the section to be in expanded state if in flat list mode

* fix: store section expanded state by space
2026-04-08 13:44:52 +00:00

191 lines
6.8 KiB
TypeScript

/*
* 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";
describe("RoomListSectionHeaderViewModel", () => {
let onToggleExpanded: jest.Mock;
let matrixClient: MatrixClient;
beforeEach(() => {
onToggleExpanded = jest.fn();
matrixClient = createTestClient();
});
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("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));
});
});
});