Merge branch 'develop' into gsouquet/threads-forceenablelabsflag

This commit is contained in:
Germain
2023-01-11 11:51:57 +00:00
97 changed files with 3280 additions and 1325 deletions
@@ -141,9 +141,10 @@ describe("EventTile", () => {
mxEvent = rootEvent;
});
it("shows an unread notification bage", () => {
it("shows an unread notification badge", () => {
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
// By default, the thread will assume it is read.
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
act(() => {
@@ -15,15 +15,21 @@ limitations under the License.
*/
import * as React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { MatrixEvent, MsgType, RoomMember } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../test-utils";
import MessageComposer, {
MessageComposer as MessageComposerClass,
} from "../../../../src/components/views/rooms/MessageComposer";
import {
createTestClient,
filterConsole,
flushPromises,
mkEvent,
mkStubRoom,
mockPlatformPeg,
stubClient,
} from "../../../test-utils";
import MessageComposer from "../../../../src/components/views/rooms/MessageComposer";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import RoomContext from "../../../../src/contexts/RoomContext";
@@ -31,42 +37,108 @@ import { IRoomState } from "../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { LocalRoom } from "../../../../src/models/LocalRoom";
import MessageComposerButtons from "../../../../src/components/views/rooms/MessageComposerButtons";
import { Features } from "../../../../src/settings/Settings";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import dis from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { SendMessageComposer } from "../../../../src/components/views/rooms/SendMessageComposer";
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
import { addTextToComposerEnzyme } from "../../../test-utils/composer";
import { addTextToComposerRTL } from "../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
import { SendWysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer";
import { Action } from "../../../../src/dispatcher/actions";
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import Modal from "../../../../src/Modal";
jest.mock("../../../../src/components/views/rooms/wysiwyg_composer", () => ({
SendWysiwygComposer: jest.fn().mockImplementation(() => <div data-testid="wysiwyg-composer" />),
}));
const openStickerPicker = async (): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Sticker"));
});
};
const startVoiceMessage = async (): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Voice Message"));
});
};
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
const recording = new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"),
MatrixClientPeg.get(),
state,
);
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
};
const waitForModal = async (): Promise<void> => {
await flushPromises();
await flushPromises();
};
const shouldClearModal = async (): Promise<void> => {
afterEach(async () => {
Modal.closeCurrentModal("force");
await waitForModal();
});
};
const expectVoiceMessageRecordingTriggered = (): void => {
// Checking for the voice message dialog text, if no mic can be found.
// By this we know at least that starting a voice message was triggered.
expect(screen.getByText("No microphone found")).toBeInTheDocument();
};
describe("MessageComposer", () => {
stubClient();
const cli = createTestClient();
filterConsole("Starting load of AsyncWrapper for modal");
beforeEach(() => {
mockPlatformPeg();
});
afterEach(() => {
jest.useRealTimers();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
// restore settings
act(() => {
[
"MessageComposerInput.showStickersButton",
"MessageComposerInput.showPollsButton",
Features.VoiceBroadcast,
"feature_wysiwyg_composer",
].forEach((setting: string): void => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
});
});
});
describe("for a Room", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
it("Renders a SendMessageComposer and MessageComposerButtons by default", () => {
const wrapper = wrapAndRender({ room });
expect(wrapper.find("SendMessageComposer")).toHaveLength(1);
expect(wrapper.find("MessageComposerButtons")).toHaveLength(1);
wrapAndRender({ room });
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
});
it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", () => {
const wrapper = wrapAndRender({ room }, false);
expect(wrapper.find("SendMessageComposer")).toHaveLength(0);
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
expect(wrapper.find(".mx_MessageComposer_noperm_error")).toHaveLength(1);
wrapAndRender({ room }, false);
expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument();
expect(screen.getByText("You do not have permission to post to this room")).toBeInTheDocument();
});
it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => {
const wrapper = wrapAndRender(
wrapAndRender(
{ room },
true,
false,
@@ -81,13 +153,12 @@ describe("MessageComposer", () => {
}),
);
expect(wrapper.find("SendMessageComposer")).toHaveLength(0);
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
expect(wrapper.find(".mx_MessageComposer_roomReplaced_header")).toHaveLength(1);
expect(screen.queryByLabelText("Send a message")).not.toBeInTheDocument();
expect(screen.getByText("This room has been replaced and is no longer active.")).toBeInTheDocument();
});
describe("when receiving a »reply_to_event«", () => {
let wrapper: ReactWrapper;
let roomContext: IRoomState;
let resizeNotifier: ResizeNotifier;
beforeEach(() => {
@@ -95,18 +166,17 @@ describe("MessageComposer", () => {
resizeNotifier = {
notifyTimelineHeightChanged: jest.fn(),
} as unknown as ResizeNotifier;
wrapper = wrapAndRender({
roomContext = wrapAndRender({
room,
resizeNotifier,
});
}).roomContext;
});
it("should call notifyTimelineHeightChanged() for the same context", () => {
dis.dispatch({
action: "reply_to_event",
context: (wrapper.instance as unknown as MessageComposerClass).context,
context: roomContext.timelineRenderingType,
});
wrapper.update();
jest.advanceTimersByTime(150);
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled();
@@ -117,7 +187,6 @@ describe("MessageComposer", () => {
action: "reply_to_event",
context: "test",
});
wrapper.update();
jest.advanceTimersByTime(150);
expect(resizeNotifier.notifyTimelineHeightChanged).not.toHaveBeenCalled();
@@ -128,28 +197,33 @@ describe("MessageComposer", () => {
[
{
setting: "MessageComposerInput.showStickersButton",
prop: "showStickersButton",
buttonLabel: "Sticker",
},
{
setting: "MessageComposerInput.showPollsButton",
prop: "showPollsButton",
buttonLabel: "Poll",
},
{
setting: Features.VoiceBroadcast,
prop: "showVoiceBroadcastButton",
buttonLabel: "Voice broadcast",
},
].forEach(({ setting, prop }) => {
].forEach(({ setting, buttonLabel }) => {
[true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => {
let wrapper: ReactWrapper;
beforeEach(() => {
beforeEach(async () => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
wrapper = wrapAndRender({ room });
wrapAndRender({ room });
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
});
});
it(`should pass the prop ${prop} = ${value}`, () => {
expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(value);
it(`should${value || "not"} display the button`, () => {
if (value) {
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
} else {
expect(screen.queryByLabelText(buttonLabel)).not.toBeInTheDocument();
}
});
describe(`and setting ${setting} to ${!value}`, () => {
@@ -164,11 +238,14 @@ describe("MessageComposer", () => {
},
true,
);
wrapper.update();
});
it(`should pass the prop ${prop} = ${!value}`, () => {
expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(!value);
it(`should${!value || "not"} display the button`, () => {
if (!value) {
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
} else {
expect(screen.queryByLabelText(buttonLabel)).not.toBeInTheDocument();
}
});
});
});
@@ -176,26 +253,22 @@ describe("MessageComposer", () => {
});
it("should not render the send button", () => {
const wrapper = wrapAndRender({ room });
expect(wrapper.find("SendButton")).toHaveLength(0);
wrapAndRender({ room });
expect(screen.queryByLabelText("Send message")).not.toBeInTheDocument();
});
describe("when a message has been entered", () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = wrapAndRender({ room });
addTextToComposerEnzyme(wrapper, "Hello");
wrapper.update();
beforeEach(async () => {
const renderResult = wrapAndRender({ room }).renderResult;
await addTextToComposerRTL(renderResult, "Hello");
});
it("should render the send button", () => {
expect(wrapper.find("SendButton")).toHaveLength(1);
expect(screen.getByLabelText("Send message")).toBeInTheDocument();
});
});
describe("UIStore interactions", () => {
let wrapper: ReactWrapper;
let resizeCallback: Function;
beforeEach(() => {
@@ -205,74 +278,74 @@ describe("MessageComposer", () => {
});
describe("when a non-resize event occurred in UIStore", () => {
let stateBefore: any;
beforeEach(() => {
wrapper = wrapAndRender({ room }).children();
stateBefore = { ...wrapper.instance().state };
beforeEach(async () => {
wrapAndRender({ room });
await openStickerPicker();
resizeCallback("test", {});
wrapper.update();
});
it("should not change the state", () => {
expect(wrapper.instance().state).toEqual(stateBefore);
it("should still display the sticker picker", () => {
expect(screen.getByText("You don't currently have any stickerpacks enabled")).toBeInTheDocument();
});
});
describe("when a resize to narrow event occurred in UIStore", () => {
beforeEach(() => {
wrapper = wrapAndRender({ room }, true, true).children();
wrapper.setState({
isMenuOpen: true,
isStickerPickerOpen: true,
});
beforeEach(async () => {
wrapAndRender({ room }, true, true);
await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {});
wrapper.update();
});
it("isMenuOpen should be true", () => {
expect(wrapper.state("isMenuOpen")).toBe(true);
it("should close the menu", () => {
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
});
it("isStickerPickerOpen should be false", () => {
expect(wrapper.state("isStickerPickerOpen")).toBe(false);
it("should not show the attachment button", () => {
expect(screen.queryByLabelText("Attachment")).not.toBeInTheDocument();
});
it("should close the sticker picker", () => {
expect(
screen.queryByText("You don't currently have any stickerpacks enabled"),
).not.toBeInTheDocument();
});
});
describe("when a resize to non-narrow event occurred in UIStore", () => {
beforeEach(() => {
wrapper = wrapAndRender({ room }, true, false).children();
wrapper.setState({
isMenuOpen: true,
isStickerPickerOpen: true,
});
beforeEach(async () => {
wrapAndRender({ room }, true, false);
await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {});
wrapper.update();
});
it("isMenuOpen should be false", () => {
expect(wrapper.state("isMenuOpen")).toBe(false);
it("should close the menu", () => {
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
});
it("isStickerPickerOpen should be false", () => {
expect(wrapper.state("isStickerPickerOpen")).toBe(false);
it("should show the attachment button", () => {
expect(screen.getByLabelText("Attachment")).toBeInTheDocument();
});
it("should close the sticker picker", () => {
expect(
screen.queryByText("You don't currently have any stickerpacks enabled"),
).not.toBeInTheDocument();
});
});
});
describe("when not replying to an event", () => {
it("should pass the expected placeholder to SendMessageComposer", () => {
const wrapper = wrapAndRender({ room });
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send a message…");
wrapAndRender({ room });
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
});
it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => {
const wrapper = wrapAndRender({
wrapAndRender({
room,
e2eStatus: E2EStatus.Normal,
});
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send an encrypted message…");
expect(screen.getByLabelText("Send an encrypted message…")).toBeInTheDocument();
});
});
@@ -282,8 +355,8 @@ describe("MessageComposer", () => {
const checkPlaceholder = (expected: string) => {
it("should pass the expected placeholder to SendMessageComposer", () => {
const wrapper = wrapAndRender(props);
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe(expected);
wrapAndRender(props);
expect(screen.getByLabelText(expected)).toBeInTheDocument();
});
};
@@ -296,7 +369,7 @@ describe("MessageComposer", () => {
beforeEach(() => {
replyToEvent = mkEvent({
event: true,
type: MsgType.Text,
type: EventType.RoomMessage,
user: cli.getUserId(),
content: {},
});
@@ -337,25 +410,72 @@ describe("MessageComposer", () => {
});
});
});
describe("when clicking start a voice message", () => {
beforeEach(async () => {
wrapAndRender({ room });
await startVoiceMessage();
await flushPromises();
});
shouldClearModal();
it("should try to start a voice message", () => {
expectVoiceMessageRecordingTriggered();
});
});
describe("when recording a voice broadcast and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
wrapAndRender({ room });
await startVoiceMessage();
await waitForModal();
});
shouldClearModal();
it("should not start a voice message and display the info dialog", async () => {
expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument();
expect(screen.getByText("Can't start voice message")).toBeInTheDocument();
});
});
describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
wrapAndRender({ room });
await startVoiceMessage();
await waitForModal();
});
shouldClearModal();
it("should try to start a voice message and should not display the info dialog", async () => {
expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument();
expectVoiceMessageRecordingTriggered();
});
});
});
describe("for a LocalRoom", () => {
const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!);
it("should pass the sticker picker disabled prop", () => {
const wrapper = wrapAndRender({ room: localRoom });
expect(wrapper.find(MessageComposerButtons).props().showStickersButton).toBe(false);
it("should not show the stickers button", async () => {
wrapAndRender({ room: localRoom });
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
});
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
});
});
it("should render SendWysiwygComposer", () => {
it("should render SendWysiwygComposer when enabled", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const wrapper = wrapAndRender({ room });
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, false);
expect(wrapper.find(SendWysiwygComposer)).toBeTruthy();
wrapAndRender({ room });
expect(screen.getByTestId("wysiwyg-composer")).toBeInTheDocument();
});
});
@@ -364,7 +484,7 @@ function wrapAndRender(
canSendMessages = true,
narrow = false,
tombstone?: MatrixEvent,
): ReactWrapper {
) {
const mockClient = MatrixClientPeg.get();
const roomId = "myroomid";
const room: any = props.room || {
@@ -376,7 +496,7 @@ function wrapAndRender(
},
};
const roomState = {
const roomContext = {
room,
canSendMessages,
tombstone,
@@ -389,11 +509,14 @@ function wrapAndRender(
permalinkCreator: new RoomPermalinkCreator(room),
};
return mount(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={roomState}>
<MessageComposer {...defaultProps} {...props} />
</RoomContext.Provider>
</MatrixClientContext.Provider>,
);
return {
renderResult: render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={roomContext}>
<MessageComposer {...defaultProps} {...props} />
</RoomContext.Provider>
</MatrixClientContext.Provider>,
),
roomContext,
};
}
@@ -17,13 +17,15 @@ limitations under the License.
import React from "react";
import "jest-mock";
import { screen, act, render } from "@testing-library/react";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { mocked } from "jest-mock";
import { EventStatus } from "matrix-js-sdk/src/models/event-status";
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import { mkThread } from "../../../../test-utils/threads";
import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
import { mkMessage, stubClient } from "../../../../test-utils/test-utils";
import { mkEvent, mkMessage, stubClient } from "../../../../test-utils/test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import * as RoomNotifs from "../../../../../src/RoomNotifs";
@@ -34,28 +36,57 @@ jest.mock("../../../../../src/RoomNotifs", () => ({
}));
const ROOM_ID = "!roomId:example.org";
let THREAD_ID;
let THREAD_ID: string;
describe("UnreadNotificationBadge", () => {
let mockClient: MatrixClient;
stubClient();
const client = MatrixClientPeg.get();
let room: Room;
function getComponent(threadId?: string) {
return <UnreadNotificationBadge room={room} threadId={threadId} />;
}
beforeAll(() => {
client.supportsExperimentalThreads = () => true;
});
beforeEach(() => {
jest.clearAllMocks();
stubClient();
mockClient = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
room = new Room(ROOM_ID, client, client.getUserId()!, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const receipt = new MatrixEvent({
type: "m.receipt",
room_id: room.roomId,
content: {
"$event0:localhost": {
[ReceiptType.Read]: {
[client.getUserId()!]: { ts: 1, thread_id: "$otherthread:localhost" },
},
},
"$event1:localhost": {
[ReceiptType.Read]: {
[client.getUserId()!]: { ts: 1 },
},
},
},
});
room.addReceipt(receipt);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
const { rootEvent } = mkThread({
room,
client,
authorId: client.getUserId()!,
participantUserIds: [client.getUserId()!],
});
THREAD_ID = rootEvent.getId()!;
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
@@ -125,4 +156,34 @@ describe("UnreadNotificationBadge", () => {
const { container } = render(getComponent());
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
});
it("activity renders unread notification badge", () => {
act(() => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
// Add another event on the thread which is not sent by us.
const event = mkEvent({
event: true,
type: "m.room.message",
user: "@alice:server.org",
room: room.roomId,
content: {
"msgtype": MsgType.Text,
"body": "Hello from Bob",
"m.relates_to": {
event_id: THREAD_ID,
rel_type: RelationType.Thread,
},
},
ts: 5,
});
room.addLiveEvents([event]);
});
const { container } = render(getComponent(THREAD_ID));
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy();
});
});
@@ -1,273 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import ReactTestUtils from "react-dom/test-utils";
import ReactDOM from "react-dom";
import { PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import * as TestUtils from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import dis from "../../../../src/dispatcher/dispatcher";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
import RoomList from "../../../../src/components/views/rooms/RoomList";
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
import { RoomTile } from "../../../../src/components/views/rooms/RoomTile";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
function generateRoomId() {
return "!" + Math.random().toString().slice(2, 10) + ":domain";
}
describe("RoomList", () => {
function createRoom(opts) {
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
// The room list now uses getPendingEvents(), so we need a detached ordering.
pendingEventOrdering: PendingEventOrdering.Detached,
});
if (opts) {
Object.assign(room, opts);
}
return room;
}
let parentDiv = null;
let root = null;
const myUserId = "@me:domain";
const movingRoomId = "!someroomid";
let movingRoom: Room | undefined;
let otherRoom: Room | undefined;
let myMember: RoomMember | undefined;
let myOtherMember: RoomMember | undefined;
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(myUserId),
getRooms: jest.fn(),
getVisibleRooms: jest.fn(),
getRoom: jest.fn(),
});
const defaultProps = {
onKeyDown: jest.fn(),
onFocus: jest.fn(),
onBlur: jest.fn(),
onResize: jest.fn(),
resizeNotifier: {} as unknown as ResizeNotifier,
isMinimized: false,
activeSpace: "",
};
beforeEach(async function (done) {
RoomListStoreClass.TEST_MODE = true;
jest.clearAllMocks();
client.credentials = { userId: myUserId };
DMRoomMap.makeShared();
parentDiv = document.createElement("div");
document.body.appendChild(parentDiv);
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render(<WrappedRoomList {...defaultProps} />, parentDiv);
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
movingRoom = createRoom({ name: "Moving room" });
expect(movingRoom.roomId).not.toBe(null);
// Mock joined member
myMember = new RoomMember(movingRoomId, myUserId);
myMember.membership = "join";
movingRoom.updateMyMembership("join");
movingRoom.getMember = (userId) =>
({
[client.credentials.userId]: myMember,
}[userId]);
otherRoom = createRoom({ name: "Other room" });
myOtherMember = new RoomMember(otherRoom.roomId, myUserId);
myOtherMember.membership = "join";
otherRoom.updateMyMembership("join");
otherRoom.getMember = (userId) =>
({
[client.credentials.userId]: myOtherMember,
}[userId]);
// Mock the matrix client
const mockRooms = [
movingRoom,
otherRoom,
createRoom({ tags: { "m.favourite": { order: 0.1 } }, name: "Some other room" }),
createRoom({ tags: { "m.favourite": { order: 0.2 } }, name: "Some other room 2" }),
createRoom({ tags: { "m.lowpriority": {} }, name: "Some unimportant room" }),
createRoom({ tags: { "custom.tag": {} }, name: "Some room customly tagged" }),
];
client.getRooms.mockReturnValue(mockRooms);
client.getVisibleRooms.mockReturnValue(mockRooms);
const roomMap = {};
client.getRooms().forEach((r) => {
roomMap[r.roomId] = r;
});
client.getRoom.mockImplementation((roomId) => roomMap[roomId]);
// Now that everything has been set up, prepare and update the store
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
done();
});
afterEach(async (done) => {
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
parentDiv = null;
}
await RoomListLayoutStore.instance.resetLayouts();
await (RoomListStore.instance as RoomListStoreClass).resetStore();
done();
});
function expectRoomInSubList(room, subListTest) {
const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSublist);
const containingSubList = subLists.find(subListTest);
let expectedRoomTile;
try {
const roomTiles = ReactTestUtils.scryRenderedComponentsWithType(containingSubList, RoomTile);
console.info({ roomTiles: roomTiles.length });
expectedRoomTile = roomTiles.find((tile) => tile.props.room === room);
} catch (err) {
// truncate the error message because it's spammy
err.message =
"Error finding RoomTile for " +
room.roomId +
" in " +
subListTest +
": " +
err.message.split("componentType")[0] +
"...";
throw err;
}
expect(expectedRoomTile).toBeTruthy();
expect(expectedRoomTile.props.room).toBe(room);
}
function expectCorrectMove(oldTagId, newTagId) {
const getTagSubListTest = (tagId) => {
return (s) => s.props.tagId === tagId;
};
// Default to finding the destination sublist with newTag
const destSubListTest = getTagSubListTest(newTagId);
const srcSubListTest = getTagSubListTest(oldTagId);
// Set up the room that will be moved such that it has the correct state for a room in
// the section for oldTagId
if (oldTagId === DefaultTagID.Favourite || oldTagId === DefaultTagID.LowPriority) {
movingRoom.tags = { [oldTagId]: {} };
} else if (oldTagId === DefaultTagID.DM) {
// Mock inverse m.direct
// @ts-ignore forcing private property
DMRoomMap.shared().roomToUser = {
[movingRoom.roomId]: "@someotheruser:domain",
};
}
dis.dispatch({ action: "MatrixActions.sync", prevState: null, state: "PREPARED", matrixClient: client });
expectRoomInSubList(movingRoom, srcSubListTest);
dis.dispatch({
action: "RoomListActions.tagRoom.pending",
request: {
oldTagId,
newTagId,
room: movingRoom,
},
});
expectRoomInSubList(movingRoom, destSubListTest);
}
function itDoesCorrectOptimisticUpdatesForDraggedRoomTiles() {
// TODO: Re-enable dragging tests when we support dragging again.
describe.skip("does correct optimistic update when dragging from", () => {
it("rooms to people", () => {
expectCorrectMove(undefined, DefaultTagID.DM);
});
it("rooms to favourites", () => {
expectCorrectMove(undefined, "m.favourite");
});
it("rooms to low priority", () => {
expectCorrectMove(undefined, "m.lowpriority");
});
// XXX: Known to fail - the view does not update immediately to reflect the change.
// Whe running the app live, it updates when some other event occurs (likely the
// m.direct arriving) that these tests do not fire.
xit("people to rooms", () => {
expectCorrectMove(DefaultTagID.DM, undefined);
});
it("people to favourites", () => {
expectCorrectMove(DefaultTagID.DM, "m.favourite");
});
it("people to lowpriority", () => {
expectCorrectMove(DefaultTagID.DM, "m.lowpriority");
});
it("low priority to rooms", () => {
expectCorrectMove("m.lowpriority", undefined);
});
it("low priority to people", () => {
expectCorrectMove("m.lowpriority", DefaultTagID.DM);
});
it("low priority to low priority", () => {
expectCorrectMove("m.lowpriority", "m.lowpriority");
});
it("favourites to rooms", () => {
expectCorrectMove("m.favourite", undefined);
});
it("favourites to people", () => {
expectCorrectMove("m.favourite", DefaultTagID.DM);
});
it("favourites to low priority", () => {
expectCorrectMove("m.favourite", "m.lowpriority");
});
});
}
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});
@@ -3,7 +3,7 @@
exports[`RoomTile should render the room 1`] = `
<div>
<div
aria-label="!1:example.org Unread messages."
aria-label="!1:example.org"
aria-selected="false"
class="mx_AccessibleButton mx_RoomTile"
role="treeitem"
@@ -37,7 +37,7 @@ exports[`RoomTile should render the room 1`] = `
class="mx_RoomTile_titleContainer"
>
<div
class="mx_RoomTile_title mx_RoomTile_titleHasUnreadEvents"
class="mx_RoomTile_title"
tabindex="-1"
title="!1:example.org"
>
@@ -51,15 +51,7 @@ exports[`RoomTile should render the room 1`] = `
<div
aria-hidden="true"
class="mx_RoomTile_badgeContainer"
>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</div>
/>
<div
aria-expanded="false"
aria-haspopup="true"
@@ -27,6 +27,8 @@ import { SubSelection } from "../../../../../../src/components/views/rooms/wysiw
describe("LinkModal", () => {
const formattingFunctions = {
link: jest.fn(),
removeLinks: jest.fn(),
getLink: jest.fn().mockReturnValue("my initial content"),
} as unknown as FormattingFunctions;
const defaultValue: SubSelection = {
focusNode: null,
@@ -35,13 +37,14 @@ describe("LinkModal", () => {
anchorOffset: 4,
};
const customRender = (isTextEnabled: boolean, onClose: () => void) => {
const customRender = (isTextEnabled: boolean, onClose: () => void, isEditing = false) => {
return render(
<LinkModal
composer={formattingFunctions}
isTextEnabled={isTextEnabled}
onClose={onClose}
composerContext={{ selection: defaultValue }}
isEditing={isEditing}
/>,
);
};
@@ -75,13 +78,13 @@ describe("LinkModal", () => {
// When
jest.useFakeTimers();
screen.getByText("Save").click();
jest.runAllTimers();
// Then
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
await waitFor(() => expect(onClose).toBeCalledTimes(1));
// When
jest.runAllTimers();
await waitFor(() => {
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
expect(onClose).toBeCalledTimes(1);
});
// Then
expect(formattingFunctions.link).toHaveBeenCalledWith("l", undefined);
@@ -118,15 +121,41 @@ describe("LinkModal", () => {
// When
jest.useFakeTimers();
screen.getByText("Save").click();
jest.runAllTimers();
// Then
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
await waitFor(() => expect(onClose).toBeCalledTimes(1));
// When
jest.runAllTimers();
await waitFor(() => {
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
expect(onClose).toBeCalledTimes(1);
});
// Then
expect(formattingFunctions.link).toHaveBeenCalledWith("l", "t");
});
it("Should remove the link", async () => {
// When
const onClose = jest.fn();
customRender(true, onClose, true);
await userEvent.click(screen.getByText("Remove"));
// Then
expect(formattingFunctions.removeLinks).toHaveBeenCalledTimes(1);
expect(onClose).toBeCalledTimes(1);
});
it("Should display the link in editing", async () => {
// When
customRender(true, jest.fn(), true);
// Then
expect(screen.getByLabelText("Link")).toContainHTML("my initial content");
expect(screen.getByText("Save")).toBeDisabled();
// When
await userEvent.type(screen.getByLabelText("Link"), "l");
// Then
await waitFor(() => expect(screen.getByText("Save")).toBeEnabled());
});
});