Room list: add default sections (#32785)

* feat: add sections to RLSV3

* feat: add sections in vms

* feat: add room list section labs flag

* fix: wrong margin for room list item when in sections

* feat: hide favourites and low priority filters

* fix: crash when changing filter

* feat: support sticky room in sections

* test: update SC snapshot

* test: update SC screenshot

* test: update RLS tests

* test: add tests to RoomListSectionHeaderViewModel

* test: fix existing test in RoomListViewModel

* test: add sections tests for RoomListViewModel

* test: add e2e tests for sections

* fix: incorrect selected room when expanding/collasping a section

* fix: typo in `roomSkipList`

* feat: use one skip list with all filters instead of one list by tag

* chore: put back comment about `roomIndexInSection`

* chore: add missing `readonly`

* chore: add doc about possible undefined value for room item vm
This commit is contained in:
Florian Duros
2026-03-31 18:43:32 +00:00
committed by GitHub
parent 1974b50213
commit 0f515f581e
28 changed files with 1299 additions and 202 deletions
@@ -0,0 +1,48 @@
/*
* 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 { RoomListSectionHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListSectionHeaderViewModel";
describe("RoomListSectionHeaderViewModel", () => {
let onToggleExpanded: jest.Mock;
beforeEach(() => {
onToggleExpanded = jest.fn();
});
it("should initialize snapshot from props", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
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",
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);
});
});
@@ -7,17 +7,20 @@
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { waitFor } from "jest-matrix-react";
import { createTestClient, flushPromises, mkStubRoom, stubClient } from "../../test-utils";
import RoomListStoreV3, { RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3";
import RoomListStoreV3, { CHATS_TAG, RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3";
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
import { FilterKey } from "../../../src/stores/room-list-v3/skip-list/filters";
import { FilterEnum } from "../../../src/stores/room-list-v3/skip-list/filters";
import dispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { RoomListViewModel } from "../../../src/viewmodels/room-list/RoomListViewModel";
import { hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
import SettingsStore from "../../../src/settings/SettingsStore";
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
hasCreateRoomRights: jest.fn().mockReturnValue(false),
@@ -46,7 +49,7 @@ describe("RoomListViewModel", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room1, room2, room3],
sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }],
});
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false);
@@ -77,12 +80,12 @@ describe("RoomListViewModel", () => {
it("should initialize with empty room list", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [],
sections: [{ tag: CHATS_TAG, rooms: [] }],
});
viewModel = new RoomListViewModel({ client: matrixClient });
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual([]);
expect(viewModel.getSnapshot().sections).toEqual([]);
expect(viewModel.getSnapshot().isRoomListEmpty).toBe(true);
});
@@ -101,7 +104,7 @@ describe("RoomListViewModel", () => {
const newRoom = mkStubRoom("!room4:server", "Room 4", matrixClient);
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room1, room2, room3, newRoom],
sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3, newRoom] }],
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
@@ -136,7 +139,7 @@ describe("RoomListViewModel", () => {
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
// View model should be still valid
expect(room1VM.isDisposed).toBe(false);
expect(room1VM!.isDisposed).toBe(false);
});
});
@@ -148,7 +151,7 @@ describe("RoomListViewModel", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "!space:server",
rooms: spaceRoomList,
sections: [{ tag: CHATS_TAG, rooms: spaceRoomList }],
});
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue("!room1:server");
@@ -163,8 +166,8 @@ describe("RoomListViewModel", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
// Get view models for visible rooms
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
const disposeSpy1 = jest.spyOn(vm1, "dispose");
const disposeSpy2 = jest.spyOn(vm2, "dispose");
@@ -172,7 +175,7 @@ describe("RoomListViewModel", () => {
// Change space
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "!space:server",
rooms: [room3],
sections: [{ tag: CHATS_TAG, rooms: [room3] }],
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
@@ -188,7 +191,7 @@ describe("RoomListViewModel", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "!space:server",
rooms: [newSpaceRoom],
sections: [{ tag: CHATS_TAG, rooms: [newSpaceRoom] }],
});
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
@@ -197,7 +200,7 @@ describe("RoomListViewModel", () => {
// New space room should be accessible
expect(() => viewModel.getRoomItemViewModel("!spaceroom:server")).not.toThrow();
// Old rooms from the home space should not be accessible
expect(() => viewModel.getRoomItemViewModel("!room1:server")).toThrow();
expect(viewModel.getRoomItemViewModel("!room1:server")).toBeUndefined();
});
});
@@ -252,7 +255,7 @@ describe("RoomListViewModel", () => {
// Simulate room list update that would move room2 to front
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room2, room1, room3], // room2 moved to front
sections: [{ tag: CHATS_TAG, rooms: [room2, room1, room3] }], // room2 moved to front
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
@@ -295,8 +298,8 @@ describe("RoomListViewModel", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room1],
filterKeys: [FilterKey.UnreadFilter],
sections: [{ tag: CHATS_TAG, rooms: [room1] }],
filterKeys: [FilterEnum.UnreadFilter],
});
viewModel.onToggleFilter("unread");
@@ -311,8 +314,8 @@ describe("RoomListViewModel", () => {
// Turn filter on
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room1],
filterKeys: [FilterKey.UnreadFilter],
sections: [{ tag: CHATS_TAG, rooms: [room1] }],
filterKeys: [FilterEnum.UnreadFilter],
});
viewModel.onToggleFilter("unread");
@@ -321,7 +324,7 @@ describe("RoomListViewModel", () => {
// Turn filter off
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room1, room2, room3],
sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }],
});
viewModel.onToggleFilter("unread");
@@ -341,7 +344,7 @@ describe("RoomListViewModel", () => {
const itemViewModel = viewModel.getRoomItemViewModel("!room1:server");
expect(itemViewModel).toBeDefined();
expect(itemViewModel.getSnapshot().room).toBe(room1);
expect(itemViewModel!.getSnapshot().room).toBe(room1);
});
it("should reuse existing room item view model", () => {
@@ -353,12 +356,10 @@ describe("RoomListViewModel", () => {
expect(itemViewModel1).toBe(itemViewModel2);
});
it("should throw error when requesting view model for non-existent room", () => {
it("should return undefined for non-existent room", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
expect(() => {
viewModel.getRoomItemViewModel("!nonexistent:server");
}).toThrow();
expect(viewModel.getRoomItemViewModel("!nonexistent:server")).toBeUndefined();
});
it("should not throw when requesting view model for a room removed from the list but still in roomsMap", () => {
@@ -367,7 +368,7 @@ describe("RoomListViewModel", () => {
// Normal list update removes room2 from the list
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
rooms: [room1, room3],
sections: [{ tag: CHATS_TAG, rooms: [room1, room3] }],
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
@@ -375,7 +376,7 @@ describe("RoomListViewModel", () => {
expect(() => viewModel.getRoomItemViewModel("!room2:server")).not.toThrow();
});
it("should throw when requesting view model for a room from old space after space change", () => {
it("should return undefined for a room from old space after space change", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const spaceRoom = mkStubRoom("!newroom:server", "New Room", matrixClient);
@@ -383,15 +384,13 @@ describe("RoomListViewModel", () => {
// Space change: new space only has spaceRoom
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "!space:server",
rooms: [spaceRoom],
sections: [{ tag: CHATS_TAG, rooms: [spaceRoom] }],
});
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
expect(() => viewModel.getRoomItemViewModel("!room1:server")).toThrow(
"Room !room1:server not found in roomsMap",
);
expect(viewModel.getRoomItemViewModel("!room1:server")).toBeUndefined();
});
it("should recover when roomsMap is stale but roomsResult has the room", () => {
@@ -407,9 +406,9 @@ describe("RoomListViewModel", () => {
it("should dispose view models for rooms no longer visible", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
const vm3 = viewModel.getRoomItemViewModel("!room3:server");
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
const vm3 = viewModel.getRoomItemViewModel("!room3:server")!;
const disposeSpy1 = jest.spyOn(vm1, "dispose");
const disposeSpy3 = jest.spyOn(vm3, "dispose");
@@ -593,8 +592,8 @@ describe("RoomListViewModel", () => {
it("should dispose all room item view models on dispose", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
const disposeSpy1 = jest.spyOn(vm1, "dispose");
const disposeSpy2 = jest.spyOn(vm2, "dispose");
@@ -604,5 +603,297 @@ describe("RoomListViewModel", () => {
expect(disposeSpy1).toHaveBeenCalled();
expect(disposeSpy2).toHaveBeenCalled();
});
describe("Sections (feature_room_list_sections)", () => {
let favRoom1: Room;
let favRoom2: Room;
let lowPriorityRoom: Room;
let regularRoom1: Room;
let regularRoom2: Room;
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "feature_room_list_sections") return true;
return false;
});
favRoom1 = mkStubRoom("!fav1:server", "Fav 1", matrixClient);
favRoom2 = mkStubRoom("!fav2:server", "Fav 2", matrixClient);
lowPriorityRoom = mkStubRoom("!low1:server", "Low 1", matrixClient);
regularRoom1 = mkStubRoom("!reg1:server", "Reg 1", matrixClient);
regularRoom2 = mkStubRoom("!reg2:server", "Reg 2", matrixClient);
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2] },
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
],
});
});
it("should initialize with multiple sections", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections).toHaveLength(3);
expect(snapshot.sections[0].id).toBe(DefaultTagID.Favourite);
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server"]);
expect(snapshot.sections[1].id).toBe(CHATS_TAG);
expect(snapshot.sections[1].roomIds).toEqual(["!reg1:server", "!reg2:server"]);
expect(snapshot.sections[2].id).toBe(DefaultTagID.LowPriority);
expect(snapshot.sections[2].roomIds).toEqual(["!low1:server"]);
});
it("should not be a flat list when multiple sections exist", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
expect(viewModel.getSnapshot().isFlatList).toBe(false);
});
it("should be a flat list when only chats section has rooms", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [] },
{ tag: CHATS_TAG, rooms: [regularRoom1] },
{ tag: DefaultTagID.LowPriority, rooms: [] },
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
expect(viewModel.getSnapshot().isFlatList).toBe(true);
expect(viewModel.getSnapshot().sections).toHaveLength(1);
expect(viewModel.getSnapshot().sections[0].id).toBe(CHATS_TAG);
});
it("should exclude favourite and low_priority from filter list", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const snapshot = viewModel.getSnapshot();
expect(snapshot.filterIds).not.toContain("favourite");
expect(snapshot.filterIds).not.toContain("low_priority");
// Other filters should still be present
expect(snapshot.filterIds).toContain("unread");
expect(snapshot.filterIds).toContain("people");
});
it("should omit empty sections from snapshot", () => {
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [] },
{ tag: CHATS_TAG, rooms: [regularRoom1] },
{ tag: DefaultTagID.LowPriority, rooms: [] },
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections).toHaveLength(1);
expect(snapshot.sections[0].id).toBe(CHATS_TAG);
});
it("should create section header view models on demand", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const headerVM = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
expect(headerVM).toBeDefined();
expect(headerVM.getSnapshot().id).toBe(DefaultTagID.Favourite);
expect(headerVM.getSnapshot().isExpanded).toBe(true);
});
it("should reuse section header view models", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const headerVM1 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
const headerVM2 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
expect(headerVM1).toBe(headerVM2);
});
it("should hide room IDs when a section is collapsed", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
// Collapse the favourite section
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
favHeader.onClick();
expect(favHeader.isExpanded).toBe(false);
const snapshot = viewModel.getSnapshot();
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
expect(favSection).toBeDefined();
// Collapsed sections have an empty roomIds list
expect(favSection!.roomIds).toEqual([]);
// Other sections remain unaffected
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
expect(chatsSection!.roomIds).toEqual(["!reg1:server", "!reg2:server"]);
});
it("should compute activeRoomIndex relative to visible rooms when a section is collapsed", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
// Collapse the favourite section (which has 2 rooms: fav1, fav2)
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
favHeader.onClick();
expect(favHeader.isExpanded).toBe(false);
// Select regularRoom1, which is the first room in the chats section
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!reg1:server");
dispatcher.dispatch({
action: Action.ActiveRoomChanged,
newRoomId: "!reg1:server",
});
await waitFor(() => {
const snapshot = viewModel.getSnapshot();
// The favourite section is collapsed so its 2 rooms are not visible.
// regularRoom1 should be at index 0 in the visible list, not index 2.
expect(snapshot.roomListState.activeRoomIndex).toBe(0);
});
});
it("should restore room IDs when a section is re-expanded", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
// Collapse then re-expand
favHeader.onClick();
favHeader.onClick();
expect(favHeader.isExpanded).toBe(true);
const snapshot = viewModel.getSnapshot();
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
expect(favSection!.roomIds).toEqual(["!fav1:server", "!fav2:server"]);
});
it("should update sections when room list changes", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
const newFav = mkStubRoom("!fav3:server", "Fav 3", matrixClient);
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2, newFav] },
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
],
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server", "!fav3:server"]);
});
it("should preserve section collapse state across list updates", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
// Collapse favourites
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
favHeader.onClick();
// Trigger a list update
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
const snapshot = viewModel.getSnapshot();
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
expect(favSection!.roomIds).toEqual([]);
});
it("should preserve section collapse state across space changes", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
// Collapse favourites
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
favHeader.onClick();
// Switch to a different space with its own rooms
const spaceFav = mkStubRoom("!spacefav:server", "Space Fav", matrixClient);
const spaceReg = mkStubRoom("!spacereg:server", "Space Reg", matrixClient);
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "!space:server",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [spaceFav] },
{ tag: CHATS_TAG, rooms: [spaceReg] },
{ tag: DefaultTagID.LowPriority, rooms: [] },
],
});
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
const snapshot = viewModel.getSnapshot();
// Favourites should still be collapsed even after the space change
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
expect(favSection).toBeDefined();
expect(favSection!.roomIds).toEqual([]);
// Other sections should remain expanded
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
expect(chatsSection!.roomIds).toEqual(["!spacereg:server"]);
});
it("should apply filters across all sections", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
// Only favRoom1 is unread
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [favRoom1] },
{ tag: CHATS_TAG, rooms: [] },
{ tag: DefaultTagID.LowPriority, rooms: [] },
],
filterKeys: [FilterEnum.UnreadFilter],
});
viewModel.onToggleFilter("unread");
const snapshot = viewModel.getSnapshot();
expect(snapshot.activeFilterId).toBe("unread");
// Only the favourite section should remain (chats and low priority are empty)
expect(snapshot.sections).toHaveLength(1);
expect(snapshot.sections[0].id).toBe(DefaultTagID.Favourite);
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server"]);
});
it("should apply sticky room within the correct section", async () => {
stubClient();
viewModel = new RoomListViewModel({ client: matrixClient });
// Select favRoom1 (index 0 globally, index 0 in favourites section)
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!fav1:server");
dispatcher.dispatch({
action: Action.ActiveRoomChanged,
newRoomId: "!fav1:server",
});
await flushPromises();
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(0);
// Room list update moves favRoom1 to second position within favourites
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [favRoom2, favRoom1] },
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
],
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
// Sticky room should keep favRoom1 at index 0 within the favourites section
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections[0].roomIds[0]).toBe("!fav1:server");
expect(snapshot.roomListState.activeRoomIndex).toBe(0);
});
});
});
});