Migrate the room list view to shared components (#31921)
* Add NotificationDecoration component Add the NotificationDecoration component to shared-components. This is a leaf component that renders notification badges and indicators for rooms/items including mentions, unread counts, call indicators, etc. * Add RoomListItem component Add the RoomListItem component to shared-components. Includes context menu, hover menu, notification menu, and more options menu. * Add RoomListPrimaryFilters component Add filter chips component for filtering the room list by unread, people, rooms, favourites, mentions, invites, and low priority. * Update VirtualizedList component Update VirtualizedList to support the room list virtualization requirements. * Add RoomList component Add RoomList component that renders a virtualized list of room items. Includes story mocks for testing. * Add RoomListView component Add RoomListView component that composes RoomList with filters, empty states, and loading skeleton. * Export room-list components from shared-components Add exports for RoomListView, RoomListItem, RoomListPrimaryFilters, and RoomList. Include i18n strings for room list components. * Add RoomListItemViewModel Add view model for individual room list items. Manages per-room subscriptions and updates only when specific room data changes. * Add RoomListViewViewModel Add view model for the room list view. Manages room list state, filtering, keyboard navigation, and child view models. * Integrate shared components into RoomListView Update RoomListView to use the new ViewModels and shared components. Includes i18n string updates for element-web. * Remove old room list implementation Remove old ViewModels, hooks, and view components that are now replaced by the shared-components implementation. * Update sliding-sync playwright test Update test expectations for new room list implementation. * Add figma links * Move viewModels to the right folder * Rename to RoomListEmptyStateView * Update VirtualizedRoomListView naming * Update screenshots and snapshots * Move viewmodel tests to the right location and fix some imports * lint * Use unknown as an Opaque type rather than any. It discourages property access within shared components and can still be cast back in EW. * Update screenshots for new shared component rendering params * Make room order tests deterministic
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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 { renderHook, waitFor } from "jest-matrix-react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||
import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
|
||||
import { useMessagePreviewViewModel } from "../../../../../src/components/viewmodels/roomlist/MessagePreviewViewModel";
|
||||
|
||||
describe("MessagePreviewViewModel", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const matrixClient = createTestClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
});
|
||||
|
||||
it("should do an initial fetch of the message preview", async () => {
|
||||
// Mock the store to return some text.
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockImplementation(async (room) => {
|
||||
return { text: "Hello world!" } as MessagePreview;
|
||||
});
|
||||
|
||||
const { result: vm } = renderHook(() => useMessagePreviewViewModel(room));
|
||||
|
||||
// Eventually, vm.message should have the text from the store.
|
||||
await waitFor(() => {
|
||||
expect(vm.current.message).toEqual("Hello world!");
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch message preview again on update from store", async () => {
|
||||
// Mock the store to return the text in variable message.
|
||||
let message = "Hello World!";
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockImplementation(async (room) => {
|
||||
return { text: message } as MessagePreview;
|
||||
});
|
||||
jest.spyOn(MessagePreviewStore, "getPreviewChangedEventName").mockImplementation((room) => {
|
||||
return "UPDATE";
|
||||
});
|
||||
|
||||
const { result: vm } = renderHook(() => useMessagePreviewViewModel(room));
|
||||
|
||||
// Let's assume the message changed.
|
||||
message = "New message!";
|
||||
MessagePreviewStore.instance.emit("UPDATE");
|
||||
|
||||
/// vm.message should be the updated message.
|
||||
await waitFor(() => {
|
||||
expect(vm.current.message).toEqual(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* 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 { renderHook } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
|
||||
import { useRoomListItemMenuViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel";
|
||||
import {
|
||||
hasAccessToNotificationMenu,
|
||||
hasAccessToOptionsMenu,
|
||||
} from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
|
||||
import { useUnreadNotifications } from "../../../../../src/hooks/useUnreadNotifications";
|
||||
import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
|
||||
import { clearRoomNotification, setMarkedUnreadState } from "../../../../../src/utils/notifications";
|
||||
import { tagRoom } from "../../../../../src/utils/room/tagRoom";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { useNotificationState } from "../../../../../src/hooks/useRoomNotificationState";
|
||||
import { RoomNotifState } from "../../../../../src/RoomNotifs";
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
||||
hasAccessToNotificationMenu: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/hooks/useUnreadNotifications", () => ({
|
||||
useUnreadNotifications: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/hooks/useRoomNotificationState", () => ({
|
||||
useNotificationState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/notifications", () => ({
|
||||
clearRoomNotification: jest.fn(),
|
||||
setMarkedUnreadState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/room/tagRoom", () => ({
|
||||
tagRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomListItemMenuViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
|
||||
mocked(useUnreadNotifications).mockReturnValue({ symbol: null, count: 0, level: NotificationLevel.None });
|
||||
mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, jest.fn()]);
|
||||
jest.spyOn(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function render() {
|
||||
return renderHook(() => useRoomListItemMenuViewModel(room), withClientContextRenderOptions(matrixClient));
|
||||
}
|
||||
|
||||
it("default", () => {
|
||||
const { result } = render();
|
||||
expect(result.current.showMoreOptionsMenu).toBe(false);
|
||||
expect(result.current.canInvite).toBe(false);
|
||||
expect(result.current.isFavourite).toBe(false);
|
||||
expect(result.current.canCopyRoomLink).toBe(true);
|
||||
expect(result.current.canMarkAsRead).toBe(false);
|
||||
expect(result.current.canMarkAsUnread).toBe(true);
|
||||
});
|
||||
|
||||
it("should has showMoreOptionsMenu to be true", () => {
|
||||
mocked(hasAccessToOptionsMenu).mockReturnValue(true);
|
||||
const { result } = render();
|
||||
expect(result.current.showMoreOptionsMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should has showNotificationMenu to be true", () => {
|
||||
mocked(hasAccessToNotificationMenu).mockReturnValue(true);
|
||||
const { result } = render();
|
||||
expect(result.current.showNotificationMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should be able to invite", () => {
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
const { result } = render();
|
||||
expect(result.current.canInvite).toBe(true);
|
||||
});
|
||||
|
||||
it("should be a favourite", () => {
|
||||
room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
|
||||
const { result } = render();
|
||||
expect(result.current.isFavourite).toBe(true);
|
||||
});
|
||||
|
||||
it("should not be able to copy the room link", () => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("userId");
|
||||
const { result } = render();
|
||||
expect(result.current.canCopyRoomLink).toBe(false);
|
||||
});
|
||||
|
||||
it("should be able to mark as read", () => {
|
||||
// Add a notification
|
||||
mocked(useUnreadNotifications).mockReturnValue({
|
||||
symbol: null,
|
||||
count: 1,
|
||||
level: NotificationLevel.Notification,
|
||||
});
|
||||
const { result } = render();
|
||||
expect(result.current.canMarkAsRead).toBe(true);
|
||||
expect(result.current.canMarkAsUnread).toBe(false);
|
||||
});
|
||||
|
||||
it("should has isNotificationAllMessage to be true", () => {
|
||||
const { result } = render();
|
||||
expect(result.current.isNotificationAllMessage).toBe(true);
|
||||
});
|
||||
|
||||
it("should has isNotificationAllMessageLoud to be true", () => {
|
||||
mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessagesLoud, jest.fn()]);
|
||||
const { result } = render();
|
||||
expect(result.current.isNotificationAllMessageLoud).toBe(true);
|
||||
});
|
||||
|
||||
it("should has isNotificationMentionOnly to be true", () => {
|
||||
mocked(useNotificationState).mockReturnValue([RoomNotifState.MentionsOnly, jest.fn()]);
|
||||
const { result } = render();
|
||||
expect(result.current.isNotificationMentionOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("should has isNotificationMute to be true", () => {
|
||||
mocked(useNotificationState).mockReturnValue([RoomNotifState.Mute, jest.fn()]);
|
||||
const { result } = render();
|
||||
expect(result.current.isNotificationMute).toBe(true);
|
||||
});
|
||||
|
||||
// Actions
|
||||
|
||||
it("should mark as read", () => {
|
||||
const { result } = render();
|
||||
result.current.markAsRead(new Event("click"));
|
||||
expect(mocked(clearRoomNotification)).toHaveBeenCalledWith(room, matrixClient);
|
||||
});
|
||||
|
||||
it("should mark as unread", () => {
|
||||
const { result } = render();
|
||||
result.current.markAsUnread(new Event("click"));
|
||||
expect(mocked(setMarkedUnreadState)).toHaveBeenCalledWith(room, matrixClient, true);
|
||||
});
|
||||
|
||||
it("should tag a room as favourite", () => {
|
||||
const { result } = render();
|
||||
result.current.toggleFavorite(new Event("click"));
|
||||
expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.Favourite);
|
||||
});
|
||||
|
||||
it("should tag a room as low priority", () => {
|
||||
const { result } = render();
|
||||
result.current.toggleLowPriority();
|
||||
expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.LowPriority);
|
||||
});
|
||||
|
||||
it("should dispatch invite action", () => {
|
||||
const { result } = render();
|
||||
result.current.invite(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_invite",
|
||||
roomId: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch a copy room action", () => {
|
||||
const { result } = render();
|
||||
result.current.copyRoomLink(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "copy_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch forget room action", () => {
|
||||
// forget room is only available for archived rooms
|
||||
room.tags = { [DefaultTagID.Archived]: { order: 0 } };
|
||||
|
||||
const { result } = render();
|
||||
result.current.leaveRoom(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "forget_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch leave room action", () => {
|
||||
const { result } = render();
|
||||
result.current.leaveRoom(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "leave_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call setRoomNotifState", () => {
|
||||
const setRoomNotifState = jest.fn();
|
||||
mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, setRoomNotifState]);
|
||||
const { result } = render();
|
||||
result.current.setRoomNotifState(RoomNotifState.Mute);
|
||||
expect(setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute);
|
||||
});
|
||||
});
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* 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 { renderHook, waitFor } from "jest-matrix-react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { useRoomListItemViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel";
|
||||
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
|
||||
import {
|
||||
hasAccessToNotificationMenu,
|
||||
hasAccessToOptionsMenu,
|
||||
} from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import * as UseCallModule from "../../../../../src/hooks/useCall";
|
||||
import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { useMessagePreviewToggle } from "../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle";
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
||||
hasAccessToNotificationMenu: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle", () => ({
|
||||
useMessagePreviewToggle: jest.fn().mockReturnValue({ shouldShowMessagePreview: true }),
|
||||
}));
|
||||
|
||||
describe("RoomListItemViewModel", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const matrixClient = createTestClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
|
||||
mocked(useMessagePreviewToggle).mockReturnValue({
|
||||
shouldShowMessagePreview: false,
|
||||
toggleMessagePreview: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should dispatch view room action on openRoom", async () => {
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
|
||||
const fn = jest.spyOn(dispatcher, "dispatch");
|
||||
vm.current.openRoom();
|
||||
expect(fn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: "RoomList",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show context menu if user has access to options menu", async () => {
|
||||
mocked(hasAccessToOptionsMenu).mockReturnValue(true);
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showContextMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should show hover menu if user has access to options menu", async () => {
|
||||
mocked(hasAccessToOptionsMenu).mockReturnValue(true);
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showHoverMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should show hover menu if user has access to notification menu", async () => {
|
||||
mocked(hasAccessToNotificationMenu).mockReturnValue(true);
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showHoverMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should not show hover menu if user has an invitation notification", async () => {
|
||||
mocked(hasAccessToOptionsMenu).mockReturnValue(true);
|
||||
|
||||
const notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
jest.spyOn(notificationState, "invited", "get").mockReturnValue(false);
|
||||
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showHoverMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should return a message preview if one is available and they are enabled", async () => {
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Message look like this",
|
||||
} as MessagePreview);
|
||||
mocked(useMessagePreviewToggle).mockReturnValue({
|
||||
shouldShowMessagePreview: true,
|
||||
toggleMessagePreview: jest.fn(),
|
||||
});
|
||||
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this"));
|
||||
});
|
||||
|
||||
it("should hide message previews when disabled", async () => {
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Message look like this",
|
||||
} as MessagePreview);
|
||||
|
||||
const { result: vm, rerender } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
|
||||
// This doesn't seem to test that the hook actually triggers an update,
|
||||
// but I can't see how to test that.
|
||||
rerender();
|
||||
|
||||
expect(vm.current.messagePreview).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should check message preview when room change", async () => {
|
||||
const otherRoom = mkStubRoom("roomId2", "roomName2", room.client);
|
||||
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
|
||||
text: "Message look like this",
|
||||
} as MessagePreview);
|
||||
mocked(useMessagePreviewToggle).mockReturnValue({
|
||||
shouldShowMessagePreview: true,
|
||||
toggleMessagePreview: jest.fn(),
|
||||
});
|
||||
|
||||
const { result: vm, rerender } = renderHook((props) => useRoomListItemViewModel(props), {
|
||||
initialProps: room,
|
||||
...withClientContextRenderOptions(room.client),
|
||||
});
|
||||
await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this"));
|
||||
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
||||
rerender(otherRoom);
|
||||
await waitFor(() => expect(vm.current.messagePreview).toBe(undefined));
|
||||
});
|
||||
|
||||
describe("notification", () => {
|
||||
let notificationState: RoomNotificationState;
|
||||
beforeEach(() => {
|
||||
notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
});
|
||||
|
||||
it("should show notification decoration if there is call has participant", () => {
|
||||
jest.spyOn(UseCallModule, "useParticipantCount").mockReturnValue(1);
|
||||
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showNotificationDecoration).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "hasAnyNotificationOrActivity",
|
||||
mock: () => jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true),
|
||||
},
|
||||
{ label: "muted", mock: () => jest.spyOn(notificationState, "muted", "get").mockReturnValue(true) },
|
||||
])("should show notification decoration if $label=true", ({ mock }) => {
|
||||
mock();
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showNotificationDecoration).toBe(true);
|
||||
});
|
||||
|
||||
it("should be bold if there is a notification", () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.isBold).toBe(true);
|
||||
});
|
||||
|
||||
it("should recompute notification state when room changes", () => {
|
||||
const newRoom = mkStubRoom("room2", "Room 2", room.client);
|
||||
const newNotificationState = new RoomNotificationState(newRoom, false);
|
||||
|
||||
const { result, rerender } = renderHook((room) => useRoomListItemViewModel(room), {
|
||||
...withClientContextRenderOptions(room.client),
|
||||
initialProps: room,
|
||||
});
|
||||
|
||||
expect(result.current.showNotificationDecoration).toBe(false);
|
||||
|
||||
jest.spyOn(newNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(newNotificationState);
|
||||
rerender(newRoom);
|
||||
|
||||
expect(result.current.showNotificationDecoration).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a11yLabel", () => {
|
||||
let notificationState: RoomNotificationState;
|
||||
beforeEach(() => {
|
||||
notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "unsent message",
|
||||
mock: () => jest.spyOn(notificationState, "isUnsentMessage", "get").mockReturnValue(true),
|
||||
expected: "Open room roomName with an unsent message.",
|
||||
},
|
||||
{
|
||||
label: "invitation",
|
||||
mock: () => jest.spyOn(notificationState, "invited", "get").mockReturnValue(true),
|
||||
expected: "Open room roomName invitation.",
|
||||
},
|
||||
{
|
||||
label: "mention",
|
||||
mock: () => {
|
||||
jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(3);
|
||||
},
|
||||
expected: "Open room roomName with 3 unread messages including mentions.",
|
||||
},
|
||||
{
|
||||
label: "unread",
|
||||
mock: () => {
|
||||
jest.spyOn(notificationState, "hasUnreadCount", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(3);
|
||||
},
|
||||
expected: "Open room roomName with 3 unread messages.",
|
||||
},
|
||||
{
|
||||
label: "default",
|
||||
expected: "Open room roomName",
|
||||
},
|
||||
])("should return the $label label", ({ mock, expected }) => {
|
||||
mock?.();
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.a11yLabel).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,341 +0,0 @@
|
||||
/*
|
||||
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 { range } from "lodash";
|
||||
import { act, renderHook, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import RoomListStoreV3, { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import { mkStubRoom } from "../../../../test-utils";
|
||||
import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../../../../src/stores/spaces";
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
hasCreateRoomRights: jest.fn().mockReturnValue(false),
|
||||
createRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomListViewModel", () => {
|
||||
function mockAndCreateRooms() {
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
const fn = jest
|
||||
.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace")
|
||||
.mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] }));
|
||||
return { rooms, fn };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return a list of rooms", async () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
expect(vm.current.roomsResult.rooms).toHaveLength(10);
|
||||
for (const room of rooms) {
|
||||
expect(vm.current.roomsResult.rooms).toContain(room);
|
||||
}
|
||||
});
|
||||
|
||||
it("should update list of rooms on event from room list store", async () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
const newRoom = mkStubRoom("bar:matrix.org", "Bar", undefined);
|
||||
rooms.push(newRoom);
|
||||
await act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vm.current.roomsResult.rooms).toContain(newRoom);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filters", () => {
|
||||
it("should provide list of available filters", () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
// should have 6 filters
|
||||
expect(vm.current.primaryFilters).toHaveLength(7);
|
||||
// check the order
|
||||
for (const [i, name] of [
|
||||
"Unreads",
|
||||
"People",
|
||||
"Rooms",
|
||||
"Favourites",
|
||||
"Mentions",
|
||||
"Invites",
|
||||
"Low priority",
|
||||
].entries()) {
|
||||
expect(vm.current.primaryFilters[i].name).toEqual(name);
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("should get filtered rooms from RLS on toggle", () => {
|
||||
const { fn } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
// Let's say we toggle the People toggle
|
||||
const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
|
||||
act(() => {
|
||||
vm.current.primaryFilters[i].toggle();
|
||||
});
|
||||
expect(fn).toHaveBeenCalledWith([FilterKey.PeopleFilter]);
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(true);
|
||||
});
|
||||
|
||||
it("should change active property on toggle", () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
// Let's say we toggle the 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();
|
||||
});
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(true);
|
||||
|
||||
// Let's say that we toggle the Favourite filter
|
||||
const j = vm.current.primaryFilters.findIndex((f) => f.name === "Favourites");
|
||||
act(() => {
|
||||
vm.current.primaryFilters[j].toggle();
|
||||
});
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(false);
|
||||
expect(vm.current.primaryFilters[j].active).toEqual(true);
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
|
||||
it("should not remove all filters when active space is changed", async () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
// Let's first toggle the People filter
|
||||
const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
|
||||
act(() => {
|
||||
vm.current.primaryFilters[i].toggle();
|
||||
});
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(true);
|
||||
|
||||
// Simulate a space change
|
||||
await act(() => SpaceStore.instance.emit(UPDATE_SELECTED_SPACE));
|
||||
|
||||
// Primary filter should remain unchanged
|
||||
expect(vm.current.activePrimaryFilter?.name).toEqual("People");
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sticky room and active index", () => {
|
||||
function expectActiveRoom(vm: ReturnType<typeof useRoomListViewModel>, i: number, roomId: string) {
|
||||
expect(vm.activeIndex).toEqual(i);
|
||||
expect(vm.roomsResult.rooms[i].roomId).toEqual(roomId);
|
||||
}
|
||||
|
||||
it("active index is calculated with the last opened room in a space", () => {
|
||||
// Let's say there's two spaces: !space1:matrix.org and !space2:matrix.org
|
||||
// Let's also say that the current active space is !space1:matrix.org
|
||||
let currentSpace = "!space1:matrix.org";
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => currentSpace);
|
||||
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
// Let's say all the rooms are in space1
|
||||
const roomsInSpace1 = { spaceId: currentSpace, rooms: [...rooms] };
|
||||
// Let's say all rooms with even index are in space 2
|
||||
const roomsInSpace2 = { spaceId: "!space2:matrix.org", rooms: [...rooms].filter((_, i) => i % 2 === 0) };
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockImplementation(() =>
|
||||
currentSpace === "!space1:matrix.org" ? roomsInSpace1 : roomsInSpace2,
|
||||
);
|
||||
|
||||
// Let's say that the room at index 4 is currently active
|
||||
const roomId = rooms[4].roomId;
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expect(vm.current.activeIndex).toEqual(4);
|
||||
|
||||
// Let's say that space is changed to "!space2:matrix.org"
|
||||
currentSpace = "!space2:matrix.org";
|
||||
// Let's say that room[6] is active in space 2
|
||||
const activeRoomIdInSpace2 = rooms[6].roomId;
|
||||
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockImplementation(
|
||||
() => activeRoomIdInSpace2,
|
||||
);
|
||||
act(() => {
|
||||
RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT);
|
||||
});
|
||||
|
||||
// Active index should be 3 even without the room change event.
|
||||
expectActiveRoom(vm.current, 3, activeRoomIdInSpace2);
|
||||
});
|
||||
|
||||
it("active room and active index are retained on order change", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
|
||||
// Let's say that the room at index 5 is active
|
||||
const roomId = rooms[5].roomId;
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expect(vm.current.activeIndex).toEqual(5);
|
||||
|
||||
// Let's say that room at index 9 moves to index 5
|
||||
const room9 = rooms[9];
|
||||
rooms.splice(9, 1);
|
||||
rooms.splice(5, 0, room9);
|
||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
|
||||
// Active room index should still be 5
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
|
||||
// Let's add 2 new rooms from index 0
|
||||
const newRoom1 = mkStubRoom("bar1:matrix.org", "Bar 1", undefined);
|
||||
const newRoom2 = mkStubRoom("bar2:matrix.org", "Bar 2", undefined);
|
||||
rooms.unshift(newRoom1, newRoom2);
|
||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
|
||||
// Active room index should still be 5
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
});
|
||||
|
||||
it("active room and active index are updated when another room is opened", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
const roomId = rooms[5].roomId;
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
|
||||
// Let's say that room at index 9 becomes active
|
||||
const room = rooms[9];
|
||||
act(() => {
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
action: Action.ActiveRoomChanged,
|
||||
oldRoomId: null,
|
||||
newRoomId: room.roomId,
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// Active room index should change to reflect new room
|
||||
expectActiveRoom(vm.current, 9, room.roomId);
|
||||
});
|
||||
|
||||
it("active room and active index are updated when active index spills out of rooms array bounds", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
// Let's say that the room at index 5 is active
|
||||
const roomId = rooms[5].roomId;
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
|
||||
// Let's say that we remove rooms from the start of the array
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
// We should be able to do 4 deletions before we run out of rooms
|
||||
rooms.splice(0, 1);
|
||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
}
|
||||
|
||||
// If we remove one more room from the start, there's not going to be enough rooms
|
||||
// to maintain the active index.
|
||||
rooms.splice(0, 1);
|
||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
expectActiveRoom(vm.current, 0, roomId);
|
||||
});
|
||||
|
||||
it("active room and active index are retained when rooms that appear after the active room are deleted", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
// Let's say that the room at index 5 is active
|
||||
const roomId = rooms[5].roomId;
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
|
||||
// Let's say that we remove rooms from the start of the array
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
// Deleting rooms after index 5 (active) should not update the active index
|
||||
rooms.splice(6, 1);
|
||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
}
|
||||
});
|
||||
|
||||
it("active room index becomes undefined when active room is deleted", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
// Let's say that the room at index 5 is active
|
||||
let roomId: string | null = rooms[5].roomId;
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expectActiveRoom(vm.current, 5, roomId);
|
||||
|
||||
// Let's remove the active room (i.e room at index 5)
|
||||
rooms.splice(5, 1);
|
||||
roomId = null;
|
||||
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
expect(vm.current.activeIndex).toBeUndefined();
|
||||
});
|
||||
|
||||
it("active room index is initially undefined", () => {
|
||||
mockAndCreateRooms();
|
||||
|
||||
// Let's say that there's no active room currently
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => null);
|
||||
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
expect(vm.current.activeIndex).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* 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 { renderHook } from "jest-matrix-react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { waitFor } from "@testing-library/dom";
|
||||
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { mkStubRoom, stubClient } from "../../../../test-utils";
|
||||
import { useRoomListNavigation } from "../../../../../src/components/viewmodels/roomlist/useRoomListNavigation";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||
|
||||
describe("useRoomListNavigation", () => {
|
||||
let rooms: Room[];
|
||||
|
||||
beforeEach(() => {
|
||||
const matrixClient = stubClient();
|
||||
rooms = [
|
||||
mkStubRoom("room1", "Room 1", matrixClient),
|
||||
mkStubRoom("room2", "Room 2", matrixClient),
|
||||
mkStubRoom("room3", "Room 3", matrixClient),
|
||||
];
|
||||
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
jest.spyOn(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should navigate to the next room based on delta", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1");
|
||||
|
||||
renderHook(() => useRoomListNavigation(rooms));
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: 1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "room2",
|
||||
show_room_tile: true,
|
||||
metricsTrigger: "WebKeyboardShortcut",
|
||||
metricsViaKeyboard: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should navigate to the previous room based on delta", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room2");
|
||||
|
||||
renderHook(() => useRoomListNavigation(rooms));
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: -1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "room1",
|
||||
show_room_tile: true,
|
||||
metricsTrigger: "WebKeyboardShortcut",
|
||||
metricsViaKeyboard: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should wrap around to the first room when navigating past the last room", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room3");
|
||||
|
||||
renderHook(() => useRoomListNavigation(rooms));
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: 1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "room1",
|
||||
show_room_tile: true,
|
||||
metricsTrigger: "WebKeyboardShortcut",
|
||||
metricsViaKeyboard: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should wrap around to the last room when navigating before the first room", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1");
|
||||
|
||||
renderHook(() => useRoomListNavigation(rooms));
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: -1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "room3",
|
||||
show_room_tile: true,
|
||||
metricsTrigger: "WebKeyboardShortcut",
|
||||
metricsViaKeyboard: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter rooms to only unread when unread=true", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1");
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation(
|
||||
(room) =>
|
||||
({
|
||||
isUnread: room.roomId !== "room1",
|
||||
}) as RoomNotificationState,
|
||||
);
|
||||
|
||||
renderHook(() => useRoomListNavigation(rooms));
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: 1,
|
||||
unread: true,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "room2",
|
||||
show_room_tile: true,
|
||||
metricsTrigger: "WebKeyboardShortcut",
|
||||
metricsViaKeyboard: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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 React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { EmptyRoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/EmptyRoomList";
|
||||
import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
|
||||
describe("<EmptyRoomList />", () => {
|
||||
let vm: RoomListViewState;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms: [] },
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
canCreateRoom: true,
|
||||
activeIndex: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
test("should render the default placeholder when there is no filter", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { asFragment } = render(<EmptyRoomList vm={vm} />);
|
||||
expect(screen.getByText("No chats yet")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Start chat" }));
|
||||
expect(vm.createChatRoom).toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "New room" }));
|
||||
expect(vm.createRoom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should not render the new room button if the user doesn't have the rights to create a room", async () => {
|
||||
const newState = { ...vm, canCreateRoom: false };
|
||||
|
||||
const { asFragment } = render(<EmptyRoomList vm={newState} />);
|
||||
expect(screen.queryByRole("button", { name: "New room" })).toBeNull();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ key: FilterKey.UnreadFilter, name: "unread", action: "Show all chats" },
|
||||
{ key: FilterKey.MentionsFilter, name: "mention", action: "See all activity" },
|
||||
{ key: FilterKey.InvitesFilter, name: "invite", action: "See all activity" },
|
||||
{ key: FilterKey.LowPriorityFilter, name: "low priority", action: "See all activity" },
|
||||
])("should display the empty state for the $name filter", async ({ key, name, action }) => {
|
||||
const user = userEvent.setup();
|
||||
const activePrimaryFilter = {
|
||||
toggle: jest.fn(),
|
||||
active: true,
|
||||
name,
|
||||
key,
|
||||
};
|
||||
const newState = {
|
||||
...vm,
|
||||
activePrimaryFilter,
|
||||
};
|
||||
|
||||
const { asFragment } = render(<EmptyRoomList vm={newState} />);
|
||||
await user.click(screen.getByRole("button", { name: action }));
|
||||
expect(activePrimaryFilter.toggle).toHaveBeenCalled();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ key: FilterKey.FavouriteFilter, name: "favourite" },
|
||||
{ key: FilterKey.PeopleFilter, name: "people" },
|
||||
{ key: FilterKey.RoomsFilter, name: "rooms" },
|
||||
])("should display empty state for filter $name", ({ name, key }) => {
|
||||
const activePrimaryFilter = {
|
||||
toggle: jest.fn(),
|
||||
active: true,
|
||||
name,
|
||||
key,
|
||||
};
|
||||
const newState = { ...vm, activePrimaryFilter };
|
||||
const { asFragment } = render(<EmptyRoomList vm={newState} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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 React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { fireEvent } from "@testing-library/dom";
|
||||
import { VirtuosoMockContext } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
|
||||
import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
|
||||
describe("<RoomList />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let vm: RoomListViewState;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`));
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms },
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
canCreateRoom: true,
|
||||
activeIndex: undefined,
|
||||
};
|
||||
|
||||
// Needed to render a room list cell
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("should render a room list", () => {
|
||||
const { asFragment } = render(<RoomList vm={vm} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
// At the moment the context prop on Virtuoso gets rendered in the dom as "[object Object]".
|
||||
// This is a general issue with the react-virtuoso library.
|
||||
// TODO: Update the snapshot when the following issue is resolved: https://github.com/petyosi/react-virtuoso/issues/1281
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ shortcut: { key: "F6", ctrlKey: true, shiftKey: true }, isPreviousLandmark: true, label: "PreviousLandmark" },
|
||||
{ shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" },
|
||||
])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => {
|
||||
const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue();
|
||||
const { getByTestId } = render(<RoomList vm={vm} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
const roomList = getByTestId("room-list");
|
||||
fireEvent.keyDown(roomList, shortcut);
|
||||
|
||||
expect(spyFindLandmark).toHaveBeenCalledWith(Landmark.ROOM_LIST, isPreviousLandmark);
|
||||
});
|
||||
});
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
* 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 React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
type RoomListItemMenuViewState,
|
||||
useRoomListItemMenuViewModel,
|
||||
} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel";
|
||||
import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
import { RoomListItemMenuView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemMenuView";
|
||||
import { RoomNotifState } from "../../../../../../src/RoomNotifs";
|
||||
|
||||
jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel", () => ({
|
||||
useRoomListItemMenuViewModel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<RoomListItemMenuView />", () => {
|
||||
const defaultValue: RoomListItemMenuViewState = {
|
||||
showMoreOptionsMenu: true,
|
||||
showNotificationMenu: true,
|
||||
isFavourite: true,
|
||||
isLowPriority: true,
|
||||
canInvite: true,
|
||||
canMarkAsUnread: true,
|
||||
canMarkAsRead: true,
|
||||
canCopyRoomLink: true,
|
||||
isNotificationAllMessage: true,
|
||||
isNotificationMentionOnly: true,
|
||||
isNotificationAllMessageLoud: true,
|
||||
isNotificationMute: true,
|
||||
copyRoomLink: jest.fn(),
|
||||
markAsUnread: jest.fn(),
|
||||
markAsRead: jest.fn(),
|
||||
leaveRoom: jest.fn(),
|
||||
toggleLowPriority: jest.fn(),
|
||||
toggleFavorite: jest.fn(),
|
||||
invite: jest.fn(),
|
||||
setRoomNotifState: jest.fn(),
|
||||
};
|
||||
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(useRoomListItemMenuViewModel).mockReturnValue(defaultValue);
|
||||
matrixClient = stubClient();
|
||||
room = mkRoom(matrixClient, "room1");
|
||||
});
|
||||
|
||||
function renderMenu() {
|
||||
return render(<RoomListItemMenuView room={room} />);
|
||||
}
|
||||
|
||||
it("should render the more options menu", () => {
|
||||
const { asFragment } = renderMenu();
|
||||
expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the notification options menu", () => {
|
||||
const { asFragment } = renderMenu();
|
||||
expect(screen.getByRole("button", { name: "Notification options" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not render the more options menu when showMoreOptionsMenu is false", () => {
|
||||
mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showMoreOptionsMenu: false });
|
||||
renderMenu();
|
||||
expect(screen.queryByRole("button", { name: "More Options" })).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render the notification options menu when showNotificationMenu is false", () => {
|
||||
mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showNotificationMenu: false });
|
||||
renderMenu();
|
||||
expect(screen.queryByRole("button", { name: "Notification options" })).toBeNull();
|
||||
});
|
||||
|
||||
it("should display all the buttons and have the actions linked for the more options menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMenu();
|
||||
|
||||
const openMenu = screen.getByRole("button", { name: "More Options" });
|
||||
await user.click(openMenu);
|
||||
|
||||
await user.click(screen.getByRole("menuitem", { name: "Mark as read" }));
|
||||
expect(defaultValue.markAsRead).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Mark as unread" }));
|
||||
expect(defaultValue.markAsUnread).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitemcheckbox", { name: "Favourited" }));
|
||||
expect(defaultValue.toggleFavorite).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitemcheckbox", { name: "Low priority" }));
|
||||
expect(defaultValue.toggleLowPriority).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Invite" }));
|
||||
expect(defaultValue.invite).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Copy room link" }));
|
||||
expect(defaultValue.copyRoomLink).toHaveBeenCalled();
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Leave room" }));
|
||||
expect(defaultValue.leaveRoom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display all the buttons and have the actions linked for the notification options menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMenu();
|
||||
|
||||
const openMenu = screen.getByRole("button", { name: "Notification options" });
|
||||
await user.click(openMenu);
|
||||
|
||||
await user.click(screen.getByRole("menuitem", { name: "Match default settings" }));
|
||||
expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages);
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "All messages" }));
|
||||
expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud);
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Mentions and keywords" }));
|
||||
expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly);
|
||||
|
||||
await user.click(openMenu);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Mute room" }));
|
||||
expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute);
|
||||
});
|
||||
});
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* 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 React from "react";
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import {
|
||||
type RoomListItemViewState,
|
||||
useRoomListItemViewModel,
|
||||
} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel";
|
||||
import { RoomNotificationState } from "../../../../../../src/stores/notifications/RoomNotificationState";
|
||||
|
||||
jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel", () => ({
|
||||
useRoomListItemViewModel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<RoomListItemView />", () => {
|
||||
let defaultValue: RoomListItemViewState;
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const renderRoomListItem = (props: Partial<React.ComponentProps<typeof RoomListItemView>> = {}) => {
|
||||
const defaultProps = {
|
||||
room,
|
||||
isSelected: false,
|
||||
isFocused: false,
|
||||
onFocus: jest.fn(),
|
||||
roomIndex: 0,
|
||||
roomCount: 1,
|
||||
listIsScrolling: false,
|
||||
};
|
||||
|
||||
return render(<RoomListItemView {...defaultProps} {...props} />, withClientContextRenderOptions(matrixClient));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkRoom(matrixClient, "room1");
|
||||
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
|
||||
const notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "isNotification", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(1);
|
||||
|
||||
defaultValue = {
|
||||
openRoom: jest.fn(),
|
||||
showContextMenu: false,
|
||||
showHoverMenu: false,
|
||||
notificationState,
|
||||
a11yLabel: "Open room room1",
|
||||
isBold: false,
|
||||
isVideoRoom: false,
|
||||
callConnectionState: null,
|
||||
callType: CallType.Video,
|
||||
hasParticipantInCall: false,
|
||||
name: room.name,
|
||||
showNotificationDecoration: false,
|
||||
messagePreview: undefined,
|
||||
};
|
||||
|
||||
mocked(useRoomListItemViewModel).mockReturnValue(defaultValue);
|
||||
});
|
||||
|
||||
test("should render a room item", () => {
|
||||
const onClick = jest.fn();
|
||||
const { asFragment } = renderRoomListItem({
|
||||
onClick,
|
||||
roomCount: 0,
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should render a room item with a message preview", () => {
|
||||
defaultValue.messagePreview = "The message looks like this";
|
||||
|
||||
const onClick = jest.fn();
|
||||
const { asFragment } = renderRoomListItem({
|
||||
onClick,
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should call openRoom when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRoomListItem();
|
||||
|
||||
await user.click(screen.getByRole("option", { name: `Open room ${room.name}` }));
|
||||
expect(defaultValue.openRoom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should be selected if isSelected=true", async () => {
|
||||
const { asFragment } = renderRoomListItem({
|
||||
isSelected: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("option", { name: `Open room ${room.name}` })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should display notification decoration", async () => {
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
const { asFragment } = renderRoomListItem();
|
||||
|
||||
expect(screen.getByTestId("notification-decoration")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should not display notification decoration when hovered", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
renderRoomListItem();
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
await user.hover(listItem);
|
||||
|
||||
expect(screen.queryByRole("notification-decoration")).toBeNull();
|
||||
});
|
||||
|
||||
test("should render the context menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showContextMenu: true,
|
||||
});
|
||||
|
||||
renderRoomListItem();
|
||||
|
||||
const button = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]);
|
||||
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
||||
// Menu should close
|
||||
await user.keyboard("{Escape}");
|
||||
expect(screen.queryByRole("menu")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
* 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 React, { act } from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
|
||||
import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
|
||||
describe("<RoomListPrimaryFilters />", () => {
|
||||
let vm: RoomListViewState;
|
||||
const filterToggleMocks = [jest.fn(), jest.fn(), jest.fn()];
|
||||
|
||||
let resizeCallback: ResizeObserverCallback;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks between tests
|
||||
filterToggleMocks.forEach((mock) => mock.mockClear());
|
||||
|
||||
// Mock ResizeObserver
|
||||
global.ResizeObserver = jest.fn().mockImplementation((callback) => {
|
||||
resizeCallback = callback;
|
||||
return {
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vm = {
|
||||
primaryFilters: [
|
||||
{ name: "People", active: true, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
|
||||
{ name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
|
||||
{ name: "Unreads", active: false, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
|
||||
],
|
||||
} as unknown as RoomListViewState;
|
||||
});
|
||||
|
||||
function mockFiltersOffsetLeft() {
|
||||
// Use `getByText` instead of `getByRole` to bypass the aria-hidden
|
||||
jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0);
|
||||
jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30);
|
||||
jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60);
|
||||
|
||||
// @ts-ignore
|
||||
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
|
||||
}
|
||||
|
||||
it("should renders all filters correctly", () => {
|
||||
const { asFragment } = render(<RoomListPrimaryFilters vm={vm} />);
|
||||
mockFiltersOffsetLeft();
|
||||
|
||||
// Check that all filters are rendered
|
||||
expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "Rooms" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "Unreads" })).toBeInTheDocument();
|
||||
|
||||
// Check that the active filter is marked as selected
|
||||
expect(screen.getByRole("option", { name: "People" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "false");
|
||||
expect(screen.getByRole("option", { name: "Unreads" })).toHaveAttribute("aria-selected", "false");
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call toggle function when a filter is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RoomListPrimaryFilters vm={vm} />);
|
||||
mockFiltersOffsetLeft();
|
||||
|
||||
// Click on an inactive filter
|
||||
await user.click(screen.getByRole("option", { name: "People" }));
|
||||
|
||||
// Check that the toggle function was called
|
||||
expect(filterToggleMocks[0]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
function makeUnreadWrapping() {
|
||||
// Use `getByText` instead of `getByRole` to bypass the aria-hidden
|
||||
jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0);
|
||||
jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30);
|
||||
// Unreads is wrapping
|
||||
jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0);
|
||||
|
||||
// @ts-ignore
|
||||
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
|
||||
}
|
||||
|
||||
it("should hide or display filters if they are wrapping", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RoomListPrimaryFilters vm={vm} />);
|
||||
mockFiltersOffsetLeft();
|
||||
|
||||
// No filter is wrapping, so chevron shouldn't be visible
|
||||
expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull();
|
||||
expect(screen.queryByRole("option", { name: "Unreads" })).toBeVisible();
|
||||
|
||||
makeUnreadWrapping();
|
||||
|
||||
// The Unreads filter is wrapping, it should not be visible
|
||||
expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull();
|
||||
// Now filters are wrapping, so chevron should be visible
|
||||
await user.click(screen.getByRole("button", { name: "Expand filter list" }));
|
||||
// The list is expanded, so Unreads should be visible
|
||||
expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible();
|
||||
});
|
||||
|
||||
it("should move the active filter if the list is collapsed and the filter is wrapping", async () => {
|
||||
vm = {
|
||||
primaryFilters: [
|
||||
{ name: "People", active: false, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
|
||||
{ name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
|
||||
{ name: "Unreads", active: true, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
|
||||
],
|
||||
} as unknown as RoomListViewState;
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<RoomListPrimaryFilters vm={vm} />);
|
||||
makeUnreadWrapping();
|
||||
|
||||
// Unread filter should be moved to the first position
|
||||
expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).toBe(
|
||||
screen.getByRole("option", { name: "Unreads" }),
|
||||
);
|
||||
|
||||
// When the list is expanded, the Unreads filter should move to its original position
|
||||
await user.click(screen.getByRole("button", { name: "Expand filter list" }));
|
||||
expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).not.toEqual(
|
||||
screen.getByRole("option", { name: "Unreads" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide the filter is the previous is on the same vertical position", async () => {
|
||||
render(<RoomListPrimaryFilters vm={vm} />);
|
||||
mockFiltersOffsetLeft();
|
||||
|
||||
jest.spyOn(screen.getByRole("option", { name: "People" }), "offsetLeft", "get").mockReturnValue(0);
|
||||
// Rooms is wrapping
|
||||
jest.spyOn(screen.getByRole("option", { name: "Rooms" }), "offsetLeft", "get").mockReturnValue(0);
|
||||
|
||||
// @ts-ignore
|
||||
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
|
||||
|
||||
// The Unreads filter is wrapping, it should not be visible
|
||||
expect(screen.queryByRole("option", { name: "Rooms" })).toBeNull();
|
||||
// Now filters are wrapping, so chevron should be visible
|
||||
expect(screen.getByRole("button", { name: "Expand filter list" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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 { render, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
type RoomListViewState,
|
||||
useRoomListViewModel,
|
||||
} from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomListView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListView";
|
||||
import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
|
||||
jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewModel", () => ({
|
||||
useRoomListViewModel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<RoomListView />", () => {
|
||||
const defaultValue: RoomListViewState = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms: [] },
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
canCreateRoom: true,
|
||||
activeIndex: undefined,
|
||||
};
|
||||
const matrixClient = stubClient();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render the loading room list", () => {
|
||||
mocked(useRoomListViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
isLoadingRooms: true,
|
||||
});
|
||||
|
||||
const roomList = render(<RoomListView />);
|
||||
expect(roomList.container.querySelector(".mx_RoomListSkeleton")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should render an empty room list", () => {
|
||||
mocked(useRoomListViewModel).mockReturnValue(defaultValue);
|
||||
|
||||
render(<RoomListView />);
|
||||
expect(screen.getByText("No chats yet")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a room list", () => {
|
||||
mocked(useRoomListViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
roomsResult: { spaceId: "home", rooms: [mkRoom(matrixClient, "testing room")] },
|
||||
});
|
||||
|
||||
render(<RoomListView />);
|
||||
expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,279 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<EmptyRoomList /> should display empty state for filter favourite 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
You don't have favourite chats yet
|
||||
</span>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_description"
|
||||
>
|
||||
You can add a chat to your favourites in the chat settings
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display empty state for filter people 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
You don’t have direct chats with anyone yet
|
||||
</span>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_description"
|
||||
>
|
||||
You can deselect filters in order to see your other chats
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display empty state for filter rooms 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
You’re not in any room yet
|
||||
</span>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_description"
|
||||
>
|
||||
You can deselect filters in order to see your other chats
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the invite filter 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
You don't have any unread invites
|
||||
</span>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
See all activity
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the low priority filter 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
You don't have any low priority rooms
|
||||
</span>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
See all activity
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the mention filter 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
You don't have any unread mentions
|
||||
</span>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
See all activity
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the unread filter 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
Congrats! You don’t have any unread messages
|
||||
</span>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show all chats
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should not render the new room button if the user doesn't have the rights to create a room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
No chats yet
|
||||
</span>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_description"
|
||||
>
|
||||
Get started by messaging someone
|
||||
</span>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_DefaultPlaceholder"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
|
||||
/>
|
||||
</svg>
|
||||
Start chat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should render the default placeholder when there is no filter 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_GenericPlaceholder"
|
||||
data-testid="empty-room-list"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_title"
|
||||
>
|
||||
No chats yet
|
||||
</span>
|
||||
<span
|
||||
class="mx_EmptyRoomList_GenericPlaceholder_description"
|
||||
>
|
||||
Get started by messaging someone or by creating a room
|
||||
</span>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EmptyRoomList_DefaultPlaceholder"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
|
||||
/>
|
||||
</svg>
|
||||
Start chat
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.566 17-.944 4.094q-.086.406-.372.656t-.687.25q-.543 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.801-3.5H3.158q-.572 0-.916-.484a1.27 1.27 0 0 1-.2-1.078 1.12 1.12 0 0 1 1.116-.938H6.85l1.145-5h-3.12q-.57 0-.915-.484a1.27 1.27 0 0 1-.2-1.078A1.12 1.12 0 0 1 4.875 7h3.691l.945-4.094q.085-.406.372-.656.286-.25.686-.25.544 0 .887.469.345.468.2 1.031l-.8 3.5h4.578l.944-4.094q.085-.406.372-.656.286-.25.687-.25.543 0 .887.469t.2 1.031L17.723 7h3.119q.573 0 .916.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937H17.15l-1.145 5h3.12q.57 0 .915.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937h-3.691l-.944 4.094q-.087.406-.373.656t-.686.25q-.544 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.8-3.5zm.573-2.5h4.578l1.144-5h-4.578z"
|
||||
/>
|
||||
</svg>
|
||||
New room
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,155 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<RoomListItemMenuView /> should render the more options menu 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListItemMenuView"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="More Options"
|
||||
aria-labelledby="_r_2_"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-_r_0_"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="_r_9_"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-_r_7_"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemMenuView /> should render the notification options menu 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListItemMenuView"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="More Options"
|
||||
aria-labelledby="_r_i_"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-_r_g_"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="_r_p_"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-_r_n_"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,234 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="true"
|
||||
aria-setsize="1"
|
||||
class="_flex_4dswl_9 mx_RoomListItemView mx_RoomListItemView_selected"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_zysgz_43"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemView /> should display notification decoration 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="1"
|
||||
class="_flex_4dswl_9 mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_zysgz_43"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="_flex_4dswl_9 mx_RoomListItemView_notificationDecoration"
|
||||
data-testid="notification-decoration"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
fill="var(--cpd-color-icon-accent-primary)"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_unread-counter_1147r_8"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemView /> should render a room item 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="0"
|
||||
class="_flex_4dswl_9 mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_zysgz_43"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemView /> should render a room item with a message preview 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="1"
|
||||
class="_flex_4dswl_9 mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_zysgz_43"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListItemView_messagePreview"
|
||||
title="The message looks like this"
|
||||
>
|
||||
The message looks like this
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,47 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<RoomListPrimaryFilters /> should renders all filters correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_RoomListPrimaryFilters"
|
||||
data-testid="primary-filters"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
aria-label="Room list filters"
|
||||
class="_flex_4dswl_9 mx_RoomListPrimaryFilters_list"
|
||||
id="_r_0_"
|
||||
role="listbox"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: wrap;"
|
||||
>
|
||||
<button
|
||||
aria-hidden="false"
|
||||
aria-selected="true"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
People
|
||||
</button>
|
||||
<button
|
||||
aria-hidden="false"
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
Rooms
|
||||
</button>
|
||||
<button
|
||||
aria-hidden="false"
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
Unreads
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
showSpacePreferences,
|
||||
showSpaceSettings,
|
||||
} from "../../../src/utils/space";
|
||||
import { createRoom, hasCreateRoomRights } from "../../../src/components/viewmodels/roomlist/utils";
|
||||
import { createTestClient, mkSpace } from "../../test-utils";
|
||||
import { createRoom, hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
|
||||
|
||||
jest.mock("../../../src/PosthogTrackers", () => ({
|
||||
trackInteraction: jest.fn(),
|
||||
@@ -38,7 +38,7 @@ jest.mock("../../../src/utils/space", () => ({
|
||||
showSpaceSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
createRoom: jest.fn(),
|
||||
hasCreateRoomRights: jest.fn(),
|
||||
}));
|
||||
|
||||
439
test/viewmodels/room-list/RoomListItemViewModel-test.tsx
Normal file
439
test/viewmodels/room-list/RoomListItemViewModel-test.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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 { type MatrixClient, type MatrixEvent, Room, RoomEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
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/room-list/MessagePreviewStore";
|
||||
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../src/stores/room-list/models";
|
||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import type { Call } from "../../../src/models/Call";
|
||||
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
|
||||
|
||||
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;
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
||||
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||
});
|
||||
|
||||
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(UPDATE_EVENT);
|
||||
|
||||
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) => {
|
||||
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()!, {}]]),
|
||||
} 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()!, {}]]),
|
||||
} 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(),
|
||||
} 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();
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
546
test/viewmodels/room-list/RoomListViewViewModel-test.tsx
Normal file
546
test/viewmodels/room-list/RoomListViewViewModel-test.tsx
Normal file
@@ -0,0 +1,546 @@
|
||||
/*
|
||||
* 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 { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { createTestClient, flushPromises, mkStubRoom, stubClient } from "../../test-utils";
|
||||
import RoomListStoreV3, { 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 dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { RoomListViewViewModel } from "../../../src/viewmodels/room-list/RoomListViewViewModel";
|
||||
import { hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
|
||||
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
hasCreateRoomRights: jest.fn().mockReturnValue(false),
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(true),
|
||||
hasAccessToNotificationMenu: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
describe("RoomListViewViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let room1: Room;
|
||||
let room2: Room;
|
||||
let room3: Room;
|
||||
let viewModel: RoomListViewViewModel;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
room1 = mkStubRoom("!room1:server", "Room 1", matrixClient);
|
||||
room2 = mkStubRoom("!room2:server", "Room 2", matrixClient);
|
||||
room3 = mkStubRoom("!room3:server", "Room 3", matrixClient);
|
||||
|
||||
// Setup DMRoomMap
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn().mockReturnValue(null),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room2, room3],
|
||||
});
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null);
|
||||
|
||||
mocked(hasCreateRoomRights).mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
viewModel?.dispose();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Initialization", () => {
|
||||
it("should initialize with correct snapshot", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
|
||||
expect(snapshot.isRoomListEmpty).toBe(false);
|
||||
expect(snapshot.isLoadingRooms).toBe(false);
|
||||
expect(snapshot.roomListState.spaceId).toBe("home");
|
||||
expect(snapshot.filterIds.length).toBeGreaterThan(0);
|
||||
expect(snapshot.activeFilterId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should initialize with empty room list", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [],
|
||||
});
|
||||
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().roomIds).toEqual([]);
|
||||
expect(viewModel.getSnapshot().isRoomListEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it("should set canCreateRoom based on user rights", () => {
|
||||
mocked(hasCreateRoomRights).mockReturnValue(true);
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().canCreateRoom).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room list updates", () => {
|
||||
it("should update room list when ListsUpdate event fires", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const newRoom = mkStubRoom("!room4:server", "Room 4", matrixClient);
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room2, room3, newRoom],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(viewModel.getSnapshot().roomIds).toEqual([
|
||||
"!room1:server",
|
||||
"!room2:server",
|
||||
"!room3:server",
|
||||
"!room4:server",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should update loading state when ListsLoaded event fires", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true);
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().isLoadingRooms).toBe(true);
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsLoaded);
|
||||
|
||||
expect(viewModel.getSnapshot().isLoadingRooms).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Space switching", () => {
|
||||
it("should update room list when space changes", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const spaceRoomList = [room1, room2];
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
rooms: spaceRoomList,
|
||||
});
|
||||
|
||||
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue("!room1:server");
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(viewModel.getSnapshot().roomListState.spaceId).toBe("!space:server");
|
||||
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server"]);
|
||||
});
|
||||
|
||||
it("should clear view models when space changes", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
// Get view models for visible rooms
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
|
||||
|
||||
const disposeSpy1 = jest.spyOn(vm1, "dispose");
|
||||
const disposeSpy2 = jest.spyOn(vm2, "dispose");
|
||||
|
||||
// Change space
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
rooms: [room3],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(disposeSpy1).toHaveBeenCalled();
|
||||
expect(disposeSpy2).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Active room tracking", () => {
|
||||
it("should update active room index when room is selected", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
oldRoomId: "!room1:server",
|
||||
newRoomId: "!room2:server",
|
||||
});
|
||||
|
||||
// Use setTimeout to allow the dispatcher callback to run
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1);
|
||||
});
|
||||
|
||||
it("should return undefined active room index when no room is selected", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null);
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
oldRoomId: "!room1:server",
|
||||
newRoomId: null,
|
||||
});
|
||||
|
||||
// Use setTimeout to allow the dispatcher callback to run
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sticky room behavior", () => {
|
||||
it("should keep selected room at same index when room list updates", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
// Select room at index 1
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
newRoomId: "!room2:server",
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1);
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
// Active room should still be at index 1 (sticky behavior)
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1);
|
||||
expect(viewModel.getSnapshot().roomIds[1]).toBe("!room2:server");
|
||||
});
|
||||
|
||||
it("should not apply sticky behavior when user changes rooms", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
// Select room at index 1
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
newRoomId: "!room2:server",
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// User switches to room3
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room3:server");
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
oldRoomId: "!room2:server",
|
||||
newRoomId: "!room3:server",
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filters", () => {
|
||||
it("should toggle filter on", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().activeFilterId).toBeUndefined();
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1],
|
||||
filterKeys: [FilterKey.UnreadFilter],
|
||||
});
|
||||
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
expect(viewModel.getSnapshot().activeFilterId).toBe("unread");
|
||||
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server"]);
|
||||
});
|
||||
|
||||
it("should toggle filter off", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
// Turn filter on
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1],
|
||||
filterKeys: [FilterKey.UnreadFilter],
|
||||
});
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
expect(viewModel.getSnapshot().activeFilterId).toBe("unread");
|
||||
|
||||
// Turn filter off
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room2, room3],
|
||||
});
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
expect(viewModel.getSnapshot().activeFilterId).toBeUndefined();
|
||||
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
|
||||
});
|
||||
|
||||
it("should clear view models when filter changes", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
// Get view models
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const disposeSpy = jest.spyOn(vm1, "dispose");
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room2],
|
||||
filterKeys: [FilterKey.UnreadFilter],
|
||||
});
|
||||
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
expect(disposeSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room item view models", () => {
|
||||
it("should create room item view model on demand", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const itemViewModel = viewModel.getRoomItemViewModel("!room1:server");
|
||||
|
||||
expect(itemViewModel).toBeDefined();
|
||||
expect(itemViewModel.getSnapshot().room).toBe(room1);
|
||||
});
|
||||
|
||||
it("should reuse existing room item view model", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const itemViewModel1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const itemViewModel2 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
|
||||
expect(itemViewModel1).toBe(itemViewModel2);
|
||||
});
|
||||
|
||||
it("should throw error when requesting view model for non-existent room", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
expect(() => {
|
||||
viewModel.getRoomItemViewModel("!nonexistent:server");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should dispose view models for rooms no longer visible", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
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");
|
||||
|
||||
// Update to show only middle room (index 1)
|
||||
viewModel.updateVisibleRooms(1, 2);
|
||||
|
||||
expect(disposeSpy1).toHaveBeenCalled();
|
||||
expect(disposeSpy3).toHaveBeenCalled();
|
||||
|
||||
// vm2 should still exist
|
||||
const vm2Again = viewModel.getRoomItemViewModel("!room2:server");
|
||||
expect(vm2Again).toBe(vm2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room creation", () => {
|
||||
it("should dispatch CreateChat action when createChatRoom is called", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "fire");
|
||||
|
||||
viewModel.createChatRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(Action.CreateChat);
|
||||
});
|
||||
|
||||
it("should dispatch CreateRoom action without parent space", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.createRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CreateRoom,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch CreateRoom action with parent space", () => {
|
||||
const spaceRoom = mkStubRoom("!space:server", "Space", matrixClient);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(spaceRoom);
|
||||
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
viewModel.createRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CreateRoom,
|
||||
parent_space: spaceRoom,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Keyboard navigation (ViewRoomDelta)", () => {
|
||||
beforeEach(() => {
|
||||
// stubClient sets up MatrixClientPeg which is needed when ViewRoom action is dispatched
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("should navigate to next room when delta is 1", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server");
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: 1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "!room2:server",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should navigate to previous room when delta is -1", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: -1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "!room1:server",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should wrap around to last room when navigating backwards from first room", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server");
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: -1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "!room3:server",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not navigate when current room is not found", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!unknown:server");
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
dispatchSpy.mockClear();
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: 1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// Should not dispatch ViewRoom since current room wasn't found
|
||||
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not navigate when no room is selected", async () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null);
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
dispatchSpy.mockClear();
|
||||
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: 1,
|
||||
unread: false,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cleanup", () => {
|
||||
it("should dispose all room item view models on dispose", () => {
|
||||
viewModel = new RoomListViewViewModel({ client: matrixClient });
|
||||
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
|
||||
|
||||
const disposeSpy1 = jest.spyOn(vm1, "dispose");
|
||||
const disposeSpy2 = jest.spyOn(vm2, "dispose");
|
||||
|
||||
viewModel.dispose();
|
||||
|
||||
expect(disposeSpy1).toHaveBeenCalled();
|
||||
expect(disposeSpy2).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,22 +8,18 @@
|
||||
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,
|
||||
hasAccessToNotificationMenu,
|
||||
} from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { showCreateNewRoom } from "../../../../../src/utils/space";
|
||||
import { createTestClient, mkStubRoom } from "../../test-utils";
|
||||
import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { showCreateNewRoom } from "../../../src/utils/space";
|
||||
import { hasCreateRoomRights, createRoom, hasAccessToNotificationMenu } from "../../../src/viewmodels/room-list/utils";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/space", () => ({
|
||||
jest.mock("../../../src/utils/space", () => ({
|
||||
showCreateNewRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user