Show chat panel when opening a video room with unread messages (#8812)

* Show chat panel when opening a video room with unread messages

* Remove unnecessary calls to private methods in tests

* Make room ID mandatory when toggling the right panel

* Restore the isViewingRoom check

* Test RightPanelStore

* Make the constructor private again

* Add even more tests

* Fix onReady
This commit is contained in:
Robin
2022-06-17 16:57:40 -04:00
committed by GitHub
parent 162be6ca94
commit ef48443dc9
11 changed files with 432 additions and 124 deletions

View File

@@ -15,37 +15,112 @@ limitations under the License.
*/
import React from "react";
import TestRenderer from "react-test-renderer";
import { mount } from "enzyme";
import { jest } from "@jest/globals";
import { Room } from "matrix-js-sdk/src/models/room";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client";
import RightPanel from "../../../src/components/structures/RightPanel";
import _RightPanel from "../../../src/components/structures/RightPanel";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import { stubClient } from "../../test-utils";
import { stubClient, wrapInMatrixClientContext, mkRoom } from "../../test-utils";
import { Action } from "../../../src/dispatcher/actions";
import dis from "../../../src/dispatcher/dispatcher";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../src/settings/SettingsStore";
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../src/stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import RoomSummaryCard from "../../../src/components/views/right_panel/RoomSummaryCard";
import MemberList from "../../../src/components/views/rooms/MemberList";
const RightPanel = wrapInMatrixClientContext(_RightPanel);
describe("RightPanel", () => {
it("renders info from only one room during room changes", async () => {
const resizeNotifier = new ResizeNotifier();
let cli: MockedObject<MatrixClient>;
beforeEach(() => {
stubClient();
const cli = MatrixClientPeg.get();
cli.hasLazyLoadMembersEnabled = () => false;
// Init misc. startup deps
cli = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
});
const r1 = new Room("r1", cli, "@name:example.com");
const r2 = new Room("r2", cli, "@name:example.com");
afterEach(async () => {
const roomChanged = new Promise<void>(resolve => {
const ref = dis.register(payload => {
if (payload.action === Action.ActiveRoomChanged) {
dis.unregister(ref);
resolve();
}
});
});
dis.fire(Action.ViewHomePage); // Stop viewing any rooms
await roomChanged;
jest.spyOn(cli, "getRoom").mockImplementation(roomId => {
dis.fire(Action.OnLoggedOut, true); // Shut down the stores
jest.restoreAllMocks();
});
const spinUpStores = async () => {
// Selectively spin up the stores we need
WidgetLayoutStore.instance.useUnitTestClient(cli);
// @ts-ignore
// This is private but it's the only way to selectively enable stores
await WidgetLayoutStore.instance.onReady();
// Make sure we start with a clean store
RightPanelStore.instance.reset();
RightPanelStore.instance.useUnitTestClient(cli);
// @ts-ignore
await RightPanelStore.instance.onReady();
};
const waitForRpsUpdate = () =>
new Promise<void>(resolve => RightPanelStore.instance.once(UPDATE_EVENT, resolve));
it("navigates from room summary to member list", async () => {
const r1 = mkRoom(cli, "r1");
cli.getRoom.mockImplementation(roomId => roomId === "r1" ? r1 : null);
// Set up right panel state
const realGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
if (roomId === "r1") {
return {
history: [{ phase: RightPanelPhases.RoomSummary }],
isOpen: true,
};
}
return null;
});
await spinUpStores();
const viewedRoom = waitForRpsUpdate();
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await viewedRoom;
const wrapper = mount(<RightPanel room={r1} resizeNotifier={resizeNotifier} />);
expect(wrapper.find(RoomSummaryCard).exists()).toEqual(true);
const switchedPhases = waitForRpsUpdate();
wrapper.find("AccessibleButton.mx_RoomSummaryCard_icon_people").simulate("click");
await switchedPhases;
wrapper.update();
expect(wrapper.find(MemberList).exists()).toEqual(true);
});
it("renders info from only one room during room changes", async () => {
const r1 = mkRoom(cli, "r1");
const r2 = mkRoom(cli, "r2");
cli.getRoom.mockImplementation(roomId => {
if (roomId === "r1") return r1;
if (roomId === "r2") return r2;
return null;
@@ -70,35 +145,12 @@ describe("RightPanel", () => {
return null;
});
// Wake up various stores we rely on
WidgetLayoutStore.instance.useUnitTestClient(cli);
// @ts-ignore
await WidgetLayoutStore.instance.onReady();
RightPanelStore.instance.useUnitTestClient(cli);
// @ts-ignore
await RightPanelStore.instance.onReady();
const resizeNotifier = new ResizeNotifier();
await spinUpStores();
// Run initial render with room 1, and also running lifecycle methods
const renderer = TestRenderer.create(<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
/>
</MatrixClientContext.Provider>);
const wrapper = mount(<RightPanel room={r1} resizeNotifier={resizeNotifier} />);
// Wait for RPS room 1 updates to fire
const rpsUpdated = new Promise<void>(resolve => {
const update = () => {
if (
RightPanelStore.instance.currentCardForRoom("r1").phase !==
RightPanelPhases.RoomMemberList
) return;
RightPanelStore.instance.off(UPDATE_EVENT, update);
resolve();
};
RightPanelStore.instance.on(UPDATE_EVENT, update);
});
const rpsUpdated = waitForRpsUpdate();
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
@@ -108,7 +160,7 @@ describe("RightPanel", () => {
// After all that setup, now to the interesting part...
// We want to verify that as we change to room 2, we should always have
// the correct right panel state for whichever room we are showing.
const instance = renderer.root.instance;
const instance = wrapper.find(_RightPanel).instance() as _RightPanel;
const rendered = new Promise<void>(resolve => {
jest.spyOn(instance, "render").mockImplementation(() => {
const { props, state } = instance;
@@ -127,21 +179,8 @@ describe("RightPanel", () => {
action: Action.ViewRoom,
room_id: "r2",
});
renderer.update(<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
/>
</MatrixClientContext.Provider>);
wrapper.setProps({ room: r2 });
await rendered;
});
afterAll(async () => {
// @ts-ignore
await WidgetLayoutStore.instance.onNotReady();
// @ts-ignore
await RightPanelStore.instance.onNotReady();
jest.restoreAllMocks();
});
});

View File

@@ -16,12 +16,13 @@ limitations under the License.
import React from "react";
import { mount, ReactWrapper } from "enzyme";
import { act } from "react-dom/test-utils";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { stubClient, wrapInMatrixClientContext } from "../../test-utils";
import { stubClient, mockPlatformPeg, unmockPlatformPeg, wrapInMatrixClientContext } from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { Action } from "../../../src/dispatcher/actions";
import dis from "../../../src/dispatcher/dispatcher";
@@ -29,18 +30,25 @@ import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayloa
import { RoomView as _RoomView } from "../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
import SettingsStore from "../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { NotificationState } from "../../../src/stores/notifications/NotificationState";
import RightPanelStore from "../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
const RoomView = wrapInMatrixClientContext(_RoomView);
describe("RoomView", () => {
let cli: MockedObject<MatrixClient>;
let room: Room;
beforeEach(() => {
let roomCount = 0;
beforeEach(async () => {
mockPlatformPeg({ reload: () => {} });
stubClient();
cli = mocked(MatrixClientPeg.get());
room = new Room("r1", cli, "@alice:example.com");
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
room.getPendingEvents = () => [];
cli.getRoom.mockReturnValue(room);
// Re-emit certain events on the mocked client
@@ -48,11 +56,17 @@ describe("RoomView", () => {
room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args));
DMRoomMap.makeShared();
RightPanelStore.instance.useUnitTestClient(cli);
});
afterEach(async () => {
unmockPlatformPeg();
jest.restoreAllMocks();
});
const mountRoomView = async (): Promise<ReactWrapper> => {
if (RoomViewStore.instance.getRoomId() !== room.roomId) {
const switchRoomPromise = new Promise<void>(resolve => {
const switchedRoom = new Promise<void>(resolve => {
const subscription = RoomViewStore.instance.addListener(() => {
if (RoomViewStore.instance.getRoomId()) {
subscription.remove();
@@ -67,10 +81,10 @@ describe("RoomView", () => {
metricsTrigger: null,
});
await switchRoomPromise;
await switchedRoom;
}
return mount(
const roomView = mount(
<RoomView
mxClient={cli}
threepidInvite={null}
@@ -81,6 +95,8 @@ describe("RoomView", () => {
onRegistered={null}
/>,
);
await act(() => Promise.resolve()); // Allow state to settle
return roomView;
};
const getRoomViewInstance = async (): Promise<_RoomView> =>
(await mountRoomView()).find(_RoomView).instance() as _RoomView;
@@ -126,4 +142,25 @@ describe("RoomView", () => {
room.getUnfilteredTimelineSet().resetLiveTimeline();
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
});
describe("video rooms", () => {
beforeEach(async () => {
// Make it a video room
room.isElementVideoRoom = () => true;
await SettingsStore.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
});
it("normally doesn't open the chat panel", async () => {
jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(false);
await mountRoomView();
expect(RightPanelStore.instance.isOpen).toEqual(false);
});
it("opens the chat panel if there are unread messages", async () => {
jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(true);
await mountRoomView();
expect(RightPanelStore.instance.isOpen).toEqual(true);
expect(RightPanelStore.instance.currentCard.phase).toEqual(RightPanelPhases.Timeline);
});
});
});

View File

@@ -0,0 +1,227 @@
/*
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 { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { stubClient } from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { Action } from "../../../src/dispatcher/actions";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { ActiveRoomChangedPayload } from "../../../src/dispatcher/payloads/ActiveRoomChangedPayload";
import RightPanelStore from "../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
import SettingsStore from "../../../src/settings/SettingsStore";
describe("RightPanelStore", () => {
// Mock out the settings store so the right panel store can't persist values between tests
jest.spyOn(SettingsStore, "setValue").mockImplementation(async () => {});
const store = RightPanelStore.instance;
let cli: MockedObject<MatrixClient>;
beforeEach(() => {
stubClient();
cli = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
// Make sure we start with a clean store
store.reset();
store.useUnitTestClient(cli);
});
const viewRoom = async (roomId: string) => {
const roomChanged = new Promise<void>(resolve => {
const ref = defaultDispatcher.register(payload => {
if (payload.action === Action.ActiveRoomChanged && payload.newRoomId === roomId) {
defaultDispatcher.unregister(ref);
resolve();
}
});
});
defaultDispatcher.dispatch<ActiveRoomChangedPayload>({
action: Action.ActiveRoomChanged,
oldRoomId: null,
newRoomId: roomId,
});
await roomChanged;
};
const setCard = (roomId: string, phase: RightPanelPhases) => store.setCard({ phase }, true, roomId);
describe("isOpen", () => {
it("is false if no rooms are open", () => {
expect(store.isOpen).toEqual(false);
});
it("is false if a room other than the current room is open", async () => {
await viewRoom("!1:example.org");
setCard("!2:example.org", RightPanelPhases.RoomSummary);
expect(store.isOpen).toEqual(false);
});
it("is true if the current room is open", async () => {
await viewRoom("!1:example.org");
setCard("!1:example.org", RightPanelPhases.RoomSummary);
expect(store.isOpen).toEqual(true);
});
});
describe("currentCard", () => {
it("has a phase of null if nothing is open", () => {
expect(store.currentCard.phase).toEqual(null);
});
it("has a phase of null if the panel is open but in another room", async () => {
await viewRoom("!1:example.org");
setCard("!2:example.org", RightPanelPhases.RoomSummary);
expect(store.currentCard.phase).toEqual(null);
});
it("reflects the phase of the current room", async () => {
await viewRoom("!1:example.org");
setCard("!1:example.org", RightPanelPhases.RoomSummary);
expect(store.currentCard.phase).toEqual(RightPanelPhases.RoomSummary);
});
});
describe("setCard", () => {
it("does nothing if given no room ID and not viewing a room", () => {
store.setCard({ phase: RightPanelPhases.RoomSummary }, true);
expect(store.isOpen).toEqual(false);
expect(store.currentCard.phase).toEqual(null);
});
it("does nothing if given an invalid state", async () => {
await viewRoom("!1:example.org");
// Needs a member specified to be valid
store.setCard({ phase: RightPanelPhases.RoomMemberInfo }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([]);
});
it("only creates a single history entry if given the same card twice", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([
{ phase: RightPanelPhases.RoomSummary, state: {} },
]);
});
it("opens the panel in the given room with the correct phase", () => {
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomSummary);
});
it("overwrites history if changing the phase", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([
{ phase: RightPanelPhases.RoomMemberList, state: {} },
]);
});
});
describe("setCards", () => {
it("overwrites history", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org");
store.setCards([
{ phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.PinnedMessages },
], true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([
{ phase: RightPanelPhases.RoomSummary, state: {} },
{ phase: RightPanelPhases.PinnedMessages, state: {} },
]);
});
});
describe("pushCard", () => {
it("does nothing if given no room ID and not viewing a room", () => {
store.pushCard({ phase: RightPanelPhases.RoomSummary }, true);
expect(store.isOpen).toEqual(false);
expect(store.currentCard.phase).toEqual(null);
});
it("opens the panel in the given room with the correct phase", () => {
store.pushCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomSummary);
});
it("appends the phase to any phases that were there before", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
store.pushCard({ phase: RightPanelPhases.PinnedMessages }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([
{ phase: RightPanelPhases.RoomSummary, state: {} },
{ phase: RightPanelPhases.PinnedMessages, state: {} },
]);
});
});
describe("popCard", () => {
it("removes the most recent card", () => {
store.setCards([
{ phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.PinnedMessages },
], true, "!1:example.org");
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.PinnedMessages);
store.popCard("!1:example.org");
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomSummary);
});
});
describe("togglePanel", () => {
it("does nothing if the room has no phase to open to", () => {
expect(store.isOpenForRoom("!1:example.org")).toEqual(false);
store.togglePanel("!1:example.org");
expect(store.isOpenForRoom("!1:example.org")).toEqual(false);
});
it("works if a room is specified", () => {
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
store.togglePanel("!1:example.org");
expect(store.isOpenForRoom("!1:example.org")).toEqual(false);
store.togglePanel("!1:example.org");
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
});
it("operates on the current room if no room is specified", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomSummary }, true);
expect(store.isOpen).toEqual(true);
store.togglePanel(null);
expect(store.isOpen).toEqual(false);
store.togglePanel(null);
expect(store.isOpen).toEqual(true);
});
});
it("doesn't restore member info cards when switching back to a room", async () => {
await viewRoom("!1:example.org");
store.setCards([
{
phase: RightPanelPhases.RoomMemberList,
},
{
phase: RightPanelPhases.RoomMemberInfo,
state: { member: new RoomMember("!1:example.org", "@alice:example.org") },
},
], true, "!1:example.org");
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberInfo);
// Switch away and back
await viewRoom("!2:example.org");
await viewRoom("!1:example.org");
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
});
});