New room list: add empty state (#29512)

* refactor: extract room creation and right verification

* refactor: update `RoomListHeaderViewModel` to use utils

* feat(room list filter): add filter key to `PrimaryFilter` model

* feat(room list filter): return active primary filter

* feat(room list): add create room action and rights verification

* test: update room list tests

* feat(empty room list): add empty room list

* test(empty room list): add empty room list tests

* feat(room list): use empty room list in `RoomListView`

* test(room list panel): update tests

* test(e2e): add e2e tests for empty room list

* test(e2e): update room list header snapshot
This commit is contained in:
Florian Duros
2025-03-18 11:02:33 +01:00
committed by GitHub
parent 55b0b1107e
commit 7de54a385e
26 changed files with 991 additions and 155 deletions

View File

@@ -6,13 +6,12 @@
*/
import { renderHook } from "jest-matrix-react";
import { JoinRule, type MatrixClient, type Room, type RoomState, RoomType } from "matrix-js-sdk/src/matrix";
import { JoinRule, type MatrixClient, type Room, RoomType } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { useRoomListHeaderViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListHeaderViewModel";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
@@ -23,9 +22,11 @@ import {
showSpacePreferences,
showSpaceSettings,
} from "../../../../../src/utils/space";
import { createRoom, hasCreateRoomRights } from "../../../../../src/components/viewmodels/roomlist/utils";
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
hasCreateRoomRights: jest.fn().mockReturnValue(false),
createRoom: jest.fn(),
}));
jest.mock("../../../../../src/utils/space", () => ({
@@ -68,19 +69,19 @@ describe("useRoomListHeaderViewModel", () => {
});
it("should be displayComposeMenu=true and canCreateRoom=true if the user can creates room", () => {
mocked(shouldShowComponent).mockReturnValue(false);
mocked(hasCreateRoomRights).mockReturnValue(false);
const { result, rerender } = render();
expect(result.current.displayComposeMenu).toBe(false);
expect(result.current.canCreateRoom).toBe(false);
mocked(shouldShowComponent).mockReturnValue(true);
mocked(hasCreateRoomRights).mockReturnValue(true);
rerender();
expect(result.current.displayComposeMenu).toBe(true);
expect(result.current.canCreateRoom).toBe(true);
});
it("should be displayComposeMenu=true if the user can creates video room", () => {
mocked(shouldShowComponent).mockReturnValue(false);
mocked(hasCreateRoomRights).mockReturnValue(false);
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
const { result } = render();
@@ -93,25 +94,6 @@ describe("useRoomListHeaderViewModel", () => {
expect(result.current.displaySpaceMenu).toBe(true);
});
it("should be canCreateRoom=false if the user has not the right to create a room in a space", () => {
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
const { result } = render();
expect(result.current.canCreateRoom).toBe(false);
});
it("should be canCreateRoom=true if the user has the right to create a room in a space", () => {
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
jest.spyOn(space.getLiveTimeline(), "getState").mockReturnValue({
maySendStateEvent: jest.fn().mockReturnValue(true),
} as unknown as RoomState);
const { result } = render();
expect(result.current.canCreateRoom).toBe(true);
});
it("should be canInviteInSpace=true if the space join rule is public", () => {
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
jest.spyOn(space, "getJoinRule").mockReturnValue(JoinRule.Public);
@@ -150,20 +132,19 @@ describe("useRoomListHeaderViewModel", () => {
expect(spy).toHaveBeenCalledWith(Action.CreateChat);
});
it("should fire Action.CreateRoom when createRoom is called", () => {
const spy = jest.spyOn(defaultDispatcher, "fire");
it("should call createRoom from utils when createRoom is called", () => {
const { result } = render();
result.current.createRoom(new Event("click"));
expect(spy).toHaveBeenCalledWith(Action.CreateRoom);
expect(createRoom).toHaveBeenCalled();
});
it("should call showCreateNewRoom when createRoom is called in a space", () => {
it("should call createRoom from utils when createRoom is called in a space", () => {
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
const { result } = render();
result.current.createRoom(new Event("click"));
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
expect(createRoom).toHaveBeenCalledWith(space);
});
it("should fire Action.CreateRoom with RoomType.UnstableCall when createVideoRoom is called and feature_element_call_video_rooms is enabled", () => {

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { range } from "lodash";
import { act, renderHook, waitFor } from "jest-matrix-react";
import { mocked } from "jest-mock";
import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStoreV3";
import { mkStubRoom } from "../../../../test-utils";
@@ -17,6 +18,14 @@ import { SecondaryFilters } from "../../../../../src/components/viewmodels/rooml
import { SortingAlgorithm } from "../../../../../src/stores/room-list-v3/skip-list/sorters";
import { SortOption } from "../../../../../src/components/viewmodels/roomlist/useSorter";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
hasCreateRoomRights: jest.fn().mockReturnValue(false),
createRoom: jest.fn(),
}));
describe("RoomListViewModel", () => {
function mockAndCreateRooms() {
@@ -139,6 +148,19 @@ describe("RoomListViewModel", () => {
);
});
it("should return the current active primary filter", async () => {
// Let's say that the user's preferred sorting is alphabetic
mockAndCreateRooms();
const { result: vm } = renderHook(() => useRoomListViewModel());
// Toggle people filter
const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
expect(vm.current.primaryFilters[i].active).toEqual(false);
act(() => vm.current.primaryFilters[i].toggle());
// The active primary filter should be the People filter
expect(vm.current.activePrimaryFilter).toEqual(vm.current.primaryFilters[i]);
});
const testcases: Array<[string, { secondary: SecondaryFilters; filterKey: FilterKey }, string]> = [
[
"Mentions only",
@@ -240,4 +262,31 @@ describe("RoomListViewModel", () => {
expect(fn).toHaveBeenCalled();
});
});
describe("Create room and chat", () => {
it("should be canCreateRoom=false if hasCreateRoomRights=false", () => {
mocked(hasCreateRoomRights).mockReturnValue(false);
const { result } = renderHook(() => useRoomListViewModel());
expect(result.current.canCreateRoom).toBe(false);
});
it("should be canCreateRoom=true if hasCreateRoomRights=true", () => {
mocked(hasCreateRoomRights).mockReturnValue(true);
const { result } = renderHook(() => useRoomListViewModel());
expect(result.current.canCreateRoom).toBe(true);
});
it("should call createRoom", () => {
const { result } = renderHook(() => useRoomListViewModel());
result.current.createRoom();
expect(mocked(createRoom)).toHaveBeenCalled();
});
it("should dispatch Action.CreateChat", () => {
const spy = jest.spyOn(dispatcher, "fire");
const { result } = renderHook(() => useRoomListViewModel());
result.current.createChatRoom();
expect(spy).toHaveBeenCalledWith(Action.CreateChat);
});
});
});

View File

@@ -0,0 +1,69 @@
/*
* 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 { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { showCreateNewRoom } from "../../../../../src/utils/space";
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);
});
});
});