Skip to content

Commit f24db7f

Browse files
committed
test: add sections tests for RoomListViewModel
1 parent 4f6705e commit f24db7f

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { SdkContextClass } from "../../../src/contexts/SDKContext";
1818
import DMRoomMap from "../../../src/utils/DMRoomMap";
1919
import { RoomListViewModel } from "../../../src/viewmodels/room-list/RoomListViewModel";
2020
import { hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
21+
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
22+
import SettingsStore from "../../../src/settings/SettingsStore";
2123

2224
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
2325
hasCreateRoomRights: jest.fn().mockReturnValue(false),
@@ -600,5 +602,274 @@ describe("RoomListViewModel", () => {
600602
expect(disposeSpy1).toHaveBeenCalled();
601603
expect(disposeSpy2).toHaveBeenCalled();
602604
});
605+
606+
describe("Sections (feature_room_list_sections)", () => {
607+
let favRoom1: Room;
608+
let favRoom2: Room;
609+
let lowPriorityRoom: Room;
610+
let regularRoom1: Room;
611+
let regularRoom2: Room;
612+
613+
beforeEach(() => {
614+
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
615+
if (setting === "feature_room_list_sections") return true;
616+
return false;
617+
});
618+
619+
favRoom1 = mkStubRoom("!fav1:server", "Fav 1", matrixClient);
620+
favRoom2 = mkStubRoom("!fav2:server", "Fav 2", matrixClient);
621+
lowPriorityRoom = mkStubRoom("!low1:server", "Low 1", matrixClient);
622+
regularRoom1 = mkStubRoom("!reg1:server", "Reg 1", matrixClient);
623+
regularRoom2 = mkStubRoom("!reg2:server", "Reg 2", matrixClient);
624+
625+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
626+
spaceId: "home",
627+
sections: [
628+
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2] },
629+
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
630+
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
631+
],
632+
});
633+
});
634+
635+
it("should initialize with multiple sections", () => {
636+
viewModel = new RoomListViewModel({ client: matrixClient });
637+
638+
const snapshot = viewModel.getSnapshot();
639+
expect(snapshot.sections).toHaveLength(3);
640+
expect(snapshot.sections[0].id).toBe(DefaultTagID.Favourite);
641+
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server"]);
642+
expect(snapshot.sections[1].id).toBe(CHATS_TAG);
643+
expect(snapshot.sections[1].roomIds).toEqual(["!reg1:server", "!reg2:server"]);
644+
expect(snapshot.sections[2].id).toBe(DefaultTagID.LowPriority);
645+
expect(snapshot.sections[2].roomIds).toEqual(["!low1:server"]);
646+
});
647+
648+
it("should not be a flat list when multiple sections exist", () => {
649+
viewModel = new RoomListViewModel({ client: matrixClient });
650+
651+
expect(viewModel.getSnapshot().isFlatList).toBe(false);
652+
});
653+
654+
it("should be a flat list when only chats section has rooms", () => {
655+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
656+
spaceId: "home",
657+
sections: [
658+
{ tag: DefaultTagID.Favourite, rooms: [] },
659+
{ tag: CHATS_TAG, rooms: [regularRoom1] },
660+
{ tag: DefaultTagID.LowPriority, rooms: [] },
661+
],
662+
});
663+
664+
viewModel = new RoomListViewModel({ client: matrixClient });
665+
666+
expect(viewModel.getSnapshot().isFlatList).toBe(true);
667+
expect(viewModel.getSnapshot().sections).toHaveLength(1);
668+
expect(viewModel.getSnapshot().sections[0].id).toBe(CHATS_TAG);
669+
});
670+
671+
it("should exclude favourite and low_priority from filter list", () => {
672+
viewModel = new RoomListViewModel({ client: matrixClient });
673+
674+
const snapshot = viewModel.getSnapshot();
675+
expect(snapshot.filterIds).not.toContain("favourite");
676+
expect(snapshot.filterIds).not.toContain("low_priority");
677+
// Other filters should still be present
678+
expect(snapshot.filterIds).toContain("unread");
679+
expect(snapshot.filterIds).toContain("people");
680+
});
681+
682+
it("should omit empty sections from snapshot", () => {
683+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
684+
spaceId: "home",
685+
sections: [
686+
{ tag: DefaultTagID.Favourite, rooms: [] },
687+
{ tag: CHATS_TAG, rooms: [regularRoom1] },
688+
{ tag: DefaultTagID.LowPriority, rooms: [] },
689+
],
690+
});
691+
692+
viewModel = new RoomListViewModel({ client: matrixClient });
693+
694+
const snapshot = viewModel.getSnapshot();
695+
expect(snapshot.sections).toHaveLength(1);
696+
expect(snapshot.sections[0].id).toBe(CHATS_TAG);
697+
});
698+
699+
it("should create section header view models on demand", () => {
700+
viewModel = new RoomListViewModel({ client: matrixClient });
701+
702+
const headerVM = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
703+
expect(headerVM).toBeDefined();
704+
expect(headerVM.getSnapshot().id).toBe(DefaultTagID.Favourite);
705+
expect(headerVM.getSnapshot().isExpanded).toBe(true);
706+
});
707+
708+
it("should reuse section header view models", () => {
709+
viewModel = new RoomListViewModel({ client: matrixClient });
710+
711+
const headerVM1 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
712+
const headerVM2 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
713+
expect(headerVM1).toBe(headerVM2);
714+
});
715+
716+
it("should hide room IDs when a section is collapsed", () => {
717+
viewModel = new RoomListViewModel({ client: matrixClient });
718+
719+
// Collapse the favourite section
720+
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
721+
favHeader.onClick();
722+
expect(favHeader.isExpanded).toBe(false);
723+
724+
const snapshot = viewModel.getSnapshot();
725+
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
726+
expect(favSection).toBeDefined();
727+
// Collapsed sections have an empty roomIds list
728+
expect(favSection!.roomIds).toEqual([]);
729+
730+
// Other sections remain unaffected
731+
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
732+
expect(chatsSection!.roomIds).toEqual(["!reg1:server", "!reg2:server"]);
733+
});
734+
735+
it("should restore room IDs when a section is re-expanded", () => {
736+
viewModel = new RoomListViewModel({ client: matrixClient });
737+
738+
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
739+
740+
// Collapse then re-expand
741+
favHeader.onClick();
742+
favHeader.onClick();
743+
expect(favHeader.isExpanded).toBe(true);
744+
745+
const snapshot = viewModel.getSnapshot();
746+
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
747+
expect(favSection!.roomIds).toEqual(["!fav1:server", "!fav2:server"]);
748+
});
749+
750+
it("should update sections when room list changes", () => {
751+
viewModel = new RoomListViewModel({ client: matrixClient });
752+
753+
const newFav = mkStubRoom("!fav3:server", "Fav 3", matrixClient);
754+
755+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
756+
spaceId: "home",
757+
sections: [
758+
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2, newFav] },
759+
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
760+
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
761+
],
762+
});
763+
764+
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
765+
766+
const snapshot = viewModel.getSnapshot();
767+
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server", "!fav3:server"]);
768+
});
769+
770+
it("should preserve section collapse state across list updates", () => {
771+
viewModel = new RoomListViewModel({ client: matrixClient });
772+
773+
// Collapse favourites
774+
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
775+
favHeader.onClick();
776+
777+
// Trigger a list update
778+
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
779+
780+
const snapshot = viewModel.getSnapshot();
781+
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
782+
expect(favSection!.roomIds).toEqual([]);
783+
});
784+
785+
it("should preserve section collapse state across space changes", () => {
786+
viewModel = new RoomListViewModel({ client: matrixClient });
787+
788+
// Collapse favourites
789+
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
790+
favHeader.onClick();
791+
792+
// Switch to a different space with its own rooms
793+
const spaceFav = mkStubRoom("!spacefav:server", "Space Fav", matrixClient);
794+
const spaceReg = mkStubRoom("!spacereg:server", "Space Reg", matrixClient);
795+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
796+
spaceId: "!space:server",
797+
sections: [
798+
{ tag: DefaultTagID.Favourite, rooms: [spaceFav] },
799+
{ tag: CHATS_TAG, rooms: [spaceReg] },
800+
{ tag: DefaultTagID.LowPriority, rooms: [] },
801+
],
802+
});
803+
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
804+
805+
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
806+
807+
const snapshot = viewModel.getSnapshot();
808+
// Favourites should still be collapsed even after the space change
809+
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
810+
expect(favSection).toBeDefined();
811+
expect(favSection!.roomIds).toEqual([]);
812+
813+
// Other sections should remain expanded
814+
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
815+
expect(chatsSection!.roomIds).toEqual(["!spacereg:server"]);
816+
});
817+
818+
it("should apply filters across all sections", () => {
819+
viewModel = new RoomListViewModel({ client: matrixClient });
820+
821+
// Only favRoom1 is unread
822+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
823+
spaceId: "home",
824+
sections: [
825+
{ tag: DefaultTagID.Favourite, rooms: [favRoom1] },
826+
{ tag: CHATS_TAG, rooms: [] },
827+
{ tag: DefaultTagID.LowPriority, rooms: [] },
828+
],
829+
filterKeys: [FilterEnum.UnreadFilter],
830+
});
831+
832+
viewModel.onToggleFilter("unread");
833+
834+
const snapshot = viewModel.getSnapshot();
835+
expect(snapshot.activeFilterId).toBe("unread");
836+
// Only the favourite section should remain (chats and low priority are empty)
837+
expect(snapshot.sections).toHaveLength(1);
838+
expect(snapshot.sections[0].id).toBe(DefaultTagID.Favourite);
839+
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server"]);
840+
});
841+
842+
it("should apply sticky room within the correct section", async () => {
843+
stubClient();
844+
viewModel = new RoomListViewModel({ client: matrixClient });
845+
846+
// Select favRoom1 (index 0 globally, index 0 in favourites section)
847+
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!fav1:server");
848+
dispatcher.dispatch({
849+
action: Action.ActiveRoomChanged,
850+
newRoomId: "!fav1:server",
851+
});
852+
await flushPromises();
853+
854+
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(0);
855+
856+
// Room list update moves favRoom1 to second position within favourites
857+
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
858+
spaceId: "home",
859+
sections: [
860+
{ tag: DefaultTagID.Favourite, rooms: [favRoom2, favRoom1] },
861+
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
862+
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
863+
],
864+
});
865+
866+
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
867+
868+
// Sticky room should keep favRoom1 at index 0 within the favourites section
869+
const snapshot = viewModel.getSnapshot();
870+
expect(snapshot.sections[0].roomIds[0]).toBe("!fav1:server");
871+
expect(snapshot.roomListState.activeRoomIndex).toBe(0);
872+
});
873+
});
603874
});
604875
});

0 commit comments

Comments
 (0)