2023-08-01 08:32:53 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2023-08-01 08:32:53 +01:00
Copyright 2022 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
2023-08-01 08:32:53 +01:00
*/
import React from "react" ;
import { render , screen , act , fireEvent , waitFor , getByRole , RenderResult } from "@testing-library/react" ;
import { mocked , Mocked } from "jest-mock" ;
2023-08-10 09:01:14 +01:00
import {
EventType ,
RoomType ,
Room ,
RoomStateEvent ,
PendingEventOrdering ,
ISearchResults ,
2024-03-20 14:27:29 +00:00
IContent ,
2023-08-10 09:01:14 +01:00
} from "matrix-js-sdk/src/matrix" ;
2024-03-18 14:40:52 +00:00
import { KnownMembership } from "matrix-js-sdk/src/types" ;
2023-08-01 08:32:53 +01:00
import { CallType } from "matrix-js-sdk/src/webrtc/call" ;
import { ClientWidgetApi , Widget } from "matrix-widget-api" ;
import EventEmitter from "events" ;
2023-08-16 11:20:48 +01:00
import { setupJestCanvasMock } from "jest-canvas-mock" ;
2023-11-01 13:03:10 +01:00
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle" ;
2024-07-25 14:49:41 +02:00
import { MatrixRTCSession , MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc" ;
2023-08-01 08:32:53 +01:00
2023-08-09 08:18:41 +01:00
import type { MatrixClient , MatrixEvent , RoomMember } from "matrix-js-sdk/src/matrix" ;
2023-08-01 08:32:53 +01:00
import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call" ;
import {
stubClient ,
mkRoomMember ,
setupAsyncStoreWithClient ,
resetAsyncStoreWithClient ,
mockPlatformPeg ,
2023-08-03 13:56:30 +01:00
mkEvent ,
2023-11-16 03:25:34 +00:00
filterConsole ,
2023-08-01 08:32:53 +01:00
} from "../../../test-utils" ;
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg" ;
import DMRoomMap from "../../../../src/utils/DMRoomMap" ;
import RoomHeader , { IProps as RoomHeaderProps } from "../../../../src/components/views/rooms/LegacyRoomHeader" ;
import { E2EStatus } from "../../../../src/utils/ShieldUtils" ;
import { IRoomState } from "../../../../src/components/structures/RoomView" ;
import RoomContext from "../../../../src/contexts/RoomContext" ;
import SdkConfig from "../../../../src/SdkConfig" ;
import SettingsStore from "../../../../src/settings/SettingsStore" ;
import { ElementCall , JitsiCall } from "../../../../src/models/Call" ;
import { CallStore } from "../../../../src/stores/CallStore" ;
import LegacyCallHandler from "../../../../src/LegacyCallHandler" ;
import defaultDispatcher from "../../../../src/dispatcher/dispatcher" ;
import { Action } from "../../../../src/dispatcher/actions" ;
import WidgetStore from "../../../../src/stores/WidgetStore" ;
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore" ;
import MediaDeviceHandler , { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler" ;
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents" ;
import { UIComponent } from "../../../../src/settings/UIFeature" ;
2024-01-29 17:06:12 +01:00
import WidgetUtils from "../../../../src/utils/WidgetUtils" ;
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions" ;
2024-06-26 11:59:04 +01:00
import { SearchScope } from "../../../../src/Searching" ;
2023-08-01 08:32:53 +01:00
jest . mock ( "../../../../src/customisations/helpers/UIComponents" , () => ({
shouldShowComponent : jest.fn (),
}));
describe ( "LegacyRoomHeader" , () => {
let client : Mocked < MatrixClient >;
let room : Room ;
let alice : RoomMember ;
let bob : RoomMember ;
let carol : RoomMember ;
2023-11-16 03:25:34 +00:00
filterConsole (
"Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur." ,
);
2023-08-01 08:32:53 +01:00
beforeEach ( async () => {
2023-08-16 11:20:48 +01:00
// some of our tests rely on the jest canvas mock, and `afterEach` will have reset the mock, so we need to
// restore it.
setupJestCanvasMock ();
2023-08-01 08:32:53 +01:00
mockPlatformPeg ({ supportsJitsiScreensharing : () => true });
stubClient ();
client = mocked ( MatrixClientPeg . safeGet ());
client . getUserId . mockReturnValue ( "@alice:example.org" );
room = new Room ( "!1:example.org" , client , "@alice:example.org" , {
pendingEventOrdering : PendingEventOrdering.Detached ,
});
room . currentState . setStateEvents ([ mkCreationEvent ( room . roomId , "@alice:example.org" )]);
client . getRoom . mockImplementation (( roomId ) => ( roomId === room . roomId ? room : null ));
client . getRooms . mockReturnValue ([ room ]);
client . reEmitter . reEmit ( room , [ RoomStateEvent . Events ]);
client . sendStateEvent . mockImplementation ( async ( roomId , eventType , content , stateKey = "" ) => {
if ( roomId !== room . roomId ) throw new Error ( "Unknown room" );
const event = mkEvent ({
event : true ,
type : eventType ,
room : roomId ,
user : alice.userId ,
skey : stateKey ,
2024-03-20 14:27:29 +00:00
content : content as IContent ,
2023-08-01 08:32:53 +01:00
});
room . addLiveEvents ([ event ]);
return { event_id : event.getId () ! };
});
alice = mkRoomMember ( room . roomId , "@alice:example.org" );
bob = mkRoomMember ( room . roomId , "@bob:example.org" );
carol = mkRoomMember ( room . roomId , "@carol:example.org" );
client . getRoom . mockImplementation (( roomId ) => ( roomId === room . roomId ? room : null ));
client . getRooms . mockReturnValue ([ room ]);
client . reEmitter . reEmit ( room , [ RoomStateEvent . Events ]);
await Promise . all (
[ CallStore . instance , WidgetStore . instance ]. map (( store ) => setupAsyncStoreWithClient ( store , client )),
);
jest . spyOn ( MediaDeviceHandler , "getDevices" ). mockResolvedValue ({
[ MediaDeviceKindEnum . AudioInput ] : [],
[ MediaDeviceKindEnum . VideoInput ] : [],
[ MediaDeviceKindEnum . AudioOutput ] : [],
});
DMRoomMap . makeShared ( client );
jest . spyOn ( DMRoomMap . shared (), "getUserIdForRoomId" ). mockReturnValue ( carol . userId );
});
afterEach ( async () => {
await Promise . all ([ CallStore . instance , WidgetStore . instance ]. map ( resetAsyncStoreWithClient ));
client . reEmitter . stopReEmitting ( room , [ RoomStateEvent . Events ]);
jest . restoreAllMocks ();
SdkConfig . reset ();
});
const mockRoomType = ( type : string ) => {
jest . spyOn ( room , "getType" ). mockReturnValue ( type );
};
const mockRoomMembers = ( members : RoomMember []) => {
jest . spyOn ( room , "getJoinedMembers" ). mockReturnValue ( members );
jest . spyOn ( room , "getMember" ). mockImplementation (
( userId ) => members . find (( member ) => member . userId === userId ) ?? null ,
);
};
const mockEnabledSettings = ( settings : string []) => {
jest . spyOn ( SettingsStore , "getValue" ). mockImplementation (( settingName ) => settings . includes ( settingName ));
};
const mockEventPowerLevels = ( events : { [ eventType : string ] : number }) => {
room . currentState . setStateEvents ([
mkEvent ({
event : true ,
type : EventType . RoomPowerLevels ,
room : room.roomId ,
user : alice.userId ,
skey : "" ,
content : { events , state_default : 0 },
}),
]);
};
const mockLegacyCall = () => {
jest . spyOn ( LegacyCallHandler . instance , "getCallForRoom" ). mockReturnValue ({} as unknown as MatrixCall );
};
const withCall = async ( fn : ( call : ElementCall ) => void | Promise < void >) : Promise < void > => {
await ElementCall . create ( room );
const call = CallStore . instance . getCall ( room . roomId );
if ( ! ( call instanceof ElementCall )) throw new Error ( "Failed to create call" );
const widget = new Widget ( call . widget );
const eventEmitter = new EventEmitter ();
const messaging = {
on : eventEmitter.on.bind ( eventEmitter ),
off : eventEmitter.off.bind ( eventEmitter ),
once : eventEmitter.once.bind ( eventEmitter ),
emit : eventEmitter.emit.bind ( eventEmitter ),
stop : jest.fn (),
transport : {
send : jest.fn (),
reply : jest.fn (),
},
} as unknown as Mocked < ClientWidgetApi >;
WidgetMessagingStore . instance . storeMessaging ( widget , call . roomId , messaging );
await fn ( call );
call . destroy ();
WidgetMessagingStore . instance . stopMessaging ( widget , call . roomId );
};
const renderHeader = ( props : Partial < RoomHeaderProps > = {}, roomContext : Partial < IRoomState > = {}) => {
render (
< RoomContext.Provider value = {{ ... roomContext , room } as IRoomState }>
< RoomHeader
room = { room }
inRoom = { true }
onSearchClick = {() => {}}
onInviteClick = { null }
onForgetClick = {() => {}}
onAppsClick = {() => {}}
e2eStatus = { E2EStatus . Normal }
appsShown = { true }
searchInfo = {{
searchId : Math.random (),
promise : new Promise < ISearchResults >(() => {}),
term : "" ,
scope : SearchScope.Room ,
count : 0 ,
}}
viewingCall = { false }
activeCall = { null }
{ ...props }
/>
</ RoomContext.Provider >,
);
};
it ( "hides call buttons in video rooms" , () => {
mockRoomType ( RoomType . UnstableCall );
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_video_rooms" , "feature_element_call_video_rooms" ]);
renderHeader ();
expect ( screen . queryByRole ( "button" , { name : /call/i })). toBeNull ();
});
it ( "hides call buttons if showCallButtonsInComposer is disabled" , () => {
mockEnabledSettings ([]);
renderHeader ();
expect ( screen . queryByRole ( "button" , { name : /call/i })). toBeNull ();
});
it (
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
"and there's an ongoing call" ,
async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
SdkConfig . put ({
element_call : { url : "https://call.element.io" , use_exclusively : true , brand : "Element Call" },
});
await ElementCall . create ( room );
renderHeader ();
expect ( screen . queryByRole ( "button" , { name : "Voice call" })). toBeNull ();
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
},
);
it (
"hides the voice call button and starts an Element call when the video call button is pressed if configured to " +
"use Element Call exclusively" ,
async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
SdkConfig . put ({
element_call : { url : "https://call.element.io" , use_exclusively : true , brand : "Element Call" },
});
renderHeader ();
expect ( screen . queryByRole ( "button" , { name : "Voice call" })). toBeNull ();
const dispatcherSpy = jest . fn ();
const dispatcherRef = defaultDispatcher . register ( dispatcherSpy );
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
await waitFor (() =>
expect ( dispatcherSpy ). toHaveBeenCalledWith ({
action : Action.ViewRoom ,
room_id : room.roomId ,
2024-01-29 17:06:12 +01:00
skipLobby : false ,
2023-08-01 08:32:53 +01:00
view_call : true ,
}),
);
defaultDispatcher . unregister ( dispatcherRef );
},
);
it (
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
"and the user lacks permission" ,
() => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
SdkConfig . put ({
element_call : { url : "https://call.element.io" , use_exclusively : true , brand : "Element Call" },
});
mockEventPowerLevels ({ [ ElementCall . CALL_EVENT_TYPE . name ] : 100 });
renderHeader ();
expect ( screen . queryByRole ( "button" , { name : "Voice call" })). toBeNull ();
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
},
);
it ( "disables call buttons in the new group call experience if there's an ongoing Element call" , async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
await ElementCall . create ( room );
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "disables call buttons in the new group call experience if there's an ongoing legacy 1:1 call" , () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
mockLegacyCall ();
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "disables call buttons in the new group call experience if there's an existing Jitsi widget" , async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
await JitsiCall . create ( room );
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "disables call buttons in the new group call experience if there's no other members" , () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it (
"starts a legacy 1:1 call when call buttons are pressed in the new group call experience if there's 1 other " +
"member" ,
async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
mockRoomMembers ([ alice , bob ]);
renderHeader ();
const placeCallSpy = jest . spyOn ( LegacyCallHandler . instance , "placeCall" ). mockResolvedValue ( undefined );
fireEvent . click ( screen . getByRole ( "button" , { name : "Voice call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Voice );
placeCallSpy . mockClear ();
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
2023-11-22 11:35:51 +01:00
fireEvent . click ( screen . getByRole ( "menuitem" , { name : "Legacy video call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
2023-08-01 08:32:53 +01:00
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Video );
},
);
it (
"creates a Jitsi widget when call buttons are pressed in the new group call experience if the user lacks " +
"permission to start Element calls" ,
async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
mockRoomMembers ([ alice , bob , carol ]);
mockEventPowerLevels ({ [ ElementCall . CALL_EVENT_TYPE . name ] : 100 });
renderHeader ();
const placeCallSpy = jest . spyOn ( LegacyCallHandler . instance , "placeCall" ). mockResolvedValue ( undefined );
fireEvent . click ( screen . getByRole ( "button" , { name : "Voice call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Voice );
placeCallSpy . mockClear ();
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Video );
},
);
it (
"creates a Jitsi widget when the voice call button is pressed and shows a menu when the video call button is " +
"pressed in the new group call experience" ,
async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
mockRoomMembers ([ alice , bob , carol ]);
renderHeader ();
const placeCallSpy = jest . spyOn ( LegacyCallHandler . instance , "placeCall" ). mockResolvedValue ( undefined );
fireEvent . click ( screen . getByRole ( "button" , { name : "Voice call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Voice );
// First try creating a Jitsi widget from the menu
placeCallSpy . mockClear ();
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
fireEvent . click ( getByRole ( screen . getByRole ( "menu" ), "menuitem" , { name : /jitsi/i }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Video );
// Then try starting an Element call from the menu
const dispatcherSpy = jest . fn ();
const dispatcherRef = defaultDispatcher . register ( dispatcherSpy );
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
fireEvent . click ( getByRole ( screen . getByRole ( "menu" ), "menuitem" , { name : /element/i }));
await waitFor (() =>
expect ( dispatcherSpy ). toHaveBeenCalledWith ({
action : Action.ViewRoom ,
room_id : room.roomId ,
2024-01-29 17:06:12 +01:00
skipLobby : false ,
2023-08-01 08:32:53 +01:00
view_call : true ,
}),
);
defaultDispatcher . unregister ( dispatcherRef );
},
);
it (
"disables the voice call button and starts an Element call when the video call button is pressed in the new " +
"group call experience if the user lacks permission to edit widgets" ,
async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
mockRoomMembers ([ alice , bob , carol ]);
mockEventPowerLevels ({ "im.vector.modular.widgets" : 100 });
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
const dispatcherSpy = jest . fn ();
const dispatcherRef = defaultDispatcher . register ( dispatcherSpy );
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
await waitFor (() =>
expect ( dispatcherSpy ). toHaveBeenCalledWith ({
action : Action.ViewRoom ,
room_id : room.roomId ,
2024-01-29 17:06:12 +01:00
skipLobby : false ,
2023-08-01 08:32:53 +01:00
view_call : true ,
}),
);
defaultDispatcher . unregister ( dispatcherRef );
},
);
it ( "disables call buttons in the new group call experience if the user lacks permission" , () => {
mockEnabledSettings ([ "showCallButtonsInComposer" , "feature_group_calls" ]);
mockRoomMembers ([ alice , bob , carol ]);
mockEventPowerLevels ({ [ ElementCall . CALL_EVENT_TYPE . name ] : 100 , "im.vector.modular.widgets" : 100 });
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "disables call buttons if there's an ongoing legacy 1:1 call" , () => {
mockEnabledSettings ([ "showCallButtonsInComposer" ]);
mockLegacyCall ();
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "disables call buttons if there's an existing Jitsi widget" , async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" ]);
await JitsiCall . create ( room );
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "disables call buttons if there's no other members" , () => {
mockEnabledSettings ([ "showCallButtonsInComposer" ]);
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "starts a legacy 1:1 call when call buttons are pressed if there's 1 other member" , async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" ]);
mockRoomMembers ([ alice , bob ]);
mockEventPowerLevels ({ "im.vector.modular.widgets" : 100 }); // Just to verify that it doesn't try to use Jitsi
renderHeader ();
const placeCallSpy = jest . spyOn ( LegacyCallHandler . instance , "placeCall" ). mockResolvedValue ( undefined );
fireEvent . click ( screen . getByRole ( "button" , { name : "Voice call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Voice );
placeCallSpy . mockClear ();
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Video );
});
it ( "creates a Jitsi widget when call buttons are pressed" , async () => {
mockEnabledSettings ([ "showCallButtonsInComposer" ]);
mockRoomMembers ([ alice , bob , carol ]);
renderHeader ();
const placeCallSpy = jest . spyOn ( LegacyCallHandler . instance , "placeCall" ). mockResolvedValue ( undefined );
fireEvent . click ( screen . getByRole ( "button" , { name : "Voice call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Voice );
placeCallSpy . mockClear ();
fireEvent . click ( screen . getByRole ( "button" , { name : "Video call" }));
await act (() => Promise . resolve ()); // Allow effects to settle
expect ( placeCallSpy ). toHaveBeenCalledWith ( room . roomId , CallType . Video );
});
it ( "disables call buttons if the user lacks permission" , () => {
mockEnabledSettings ([ "showCallButtonsInComposer" ]);
mockRoomMembers ([ alice , bob , carol ]);
mockEventPowerLevels ({ "im.vector.modular.widgets" : 100 });
renderHeader ();
expect ( screen . getByRole ( "button" , { name : "Voice call" })). toHaveAttribute ( "aria-disabled" , "true" );
expect ( screen . getByRole ( "button" , { name : "Video call" })). toHaveAttribute ( "aria-disabled" , "true" );
});
it ( "shows a close button when viewing a call lobby that returns to the timeline when pressed" , async () => {
mockEnabledSettings ([ "feature_group_calls" ]);
renderHeader ({ viewingCall : true });
const dispatcherSpy = jest . fn ();
const dispatcherRef = defaultDispatcher . register ( dispatcherSpy );
fireEvent . click ( screen . getByRole ( "button" , { name : /close/i }));
await waitFor (() =>
expect ( dispatcherSpy ). toHaveBeenCalledWith ({
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : false ,
}),
);
defaultDispatcher . unregister ( dispatcherRef );
});
it ( "shows a reduce button when viewing a call that returns to the timeline when pressed" , async () => {
mockEnabledSettings ([ "feature_group_calls" ]);
await withCall ( async ( call ) => {
renderHeader ({ viewingCall : true , activeCall : call });
const dispatcherSpy = jest . fn ();
const dispatcherRef = defaultDispatcher . register ( dispatcherSpy );
fireEvent . click ( screen . getByRole ( "button" , { name : /timeline/i }));
await waitFor (() =>
expect ( dispatcherSpy ). toHaveBeenCalledWith ({
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : false ,
}),
);
defaultDispatcher . unregister ( dispatcherRef );
});
});
it ( "shows a layout button when viewing a call that shows a menu when pressed" , async () => {
mockEnabledSettings ([ "feature_group_calls" ]);
await withCall ( async ( call ) => {
2024-01-29 17:06:12 +01:00
// We set the call to skip lobby because otherwise the connection will wait until
// the user clicks the "join" button, inside the widget lobby which is hard to mock.
call . widget . data = { ... call . widget . data , skipLobby : true };
// The connect method will wait until the session actually connected. Otherwise it will timeout.
// Emitting SessionStarted will trigger the connect method to resolve.
setTimeout (
() =>
client . matrixRTC . emit ( MatrixRTCSessionManagerEvents . SessionStarted , room . roomId , {
room ,
} as MatrixRTCSession ),
100 ,
);
await call . start ();
2023-08-01 08:32:53 +01:00
const messaging = WidgetMessagingStore . instance . getMessagingForUid ( WidgetUtils . getWidgetUid ( call . widget )) ! ;
renderHeader ({ viewingCall : true , activeCall : call });
// Should start with Freedom selected
fireEvent . click ( screen . getByRole ( "button" , { name : /layout/i }));
screen . getByRole ( "menuitemradio" , { name : "Freedom" , checked : true });
// Clicking Spotlight should tell the widget to switch and close the menu
fireEvent . click ( screen . getByRole ( "menuitemradio" , { name : "Spotlight" }));
expect ( mocked ( messaging . transport ). send ). toHaveBeenCalledWith ( ElementWidgetActions . SpotlightLayout , {});
expect ( screen . queryByRole ( "menu" )). toBeNull ();
// When the widget responds and the user reopens the menu, they should see Spotlight selected
act (() => {
messaging . emit (
`action: ${ ElementWidgetActions . SpotlightLayout } ` ,
new CustomEvent ( "widgetapirequest" , { detail : { data : {} } }),
);
});
fireEvent . click ( screen . getByRole ( "button" , { name : /layout/i }));
screen . getByRole ( "menuitemradio" , { name : "Spotlight" , checked : true });
// Now try switching back to Freedom
fireEvent . click ( screen . getByRole ( "menuitemradio" , { name : "Freedom" }));
expect ( mocked ( messaging . transport ). send ). toHaveBeenCalledWith ( ElementWidgetActions . TileLayout , {});
expect ( screen . queryByRole ( "menu" )). toBeNull ();
// When the widget responds and the user reopens the menu, they should see Freedom selected
act (() => {
messaging . emit (
`action: ${ ElementWidgetActions . TileLayout } ` ,
new CustomEvent ( "widgetapirequest" , { detail : { data : {} } }),
);
});
fireEvent . click ( screen . getByRole ( "button" , { name : /layout/i }));
screen . getByRole ( "menuitemradio" , { name : "Freedom" , checked : true });
});
});
it ( "shows an invite button in video rooms" , () => {
mockEnabledSettings ([ "feature_video_rooms" , "feature_element_call_video_rooms" ]);
mockRoomType ( RoomType . UnstableCall );
const onInviteClick = jest . fn ();
renderHeader ({ onInviteClick , viewingCall : true });
fireEvent . click ( screen . getByRole ( "button" , { name : /invite/i }));
expect ( onInviteClick ). toHaveBeenCalled ();
});
it ( "hides the invite button in non-video rooms when viewing a call" , () => {
renderHeader ({ onInviteClick : () => {}, viewingCall : true });
expect ( screen . queryByRole ( "button" , { name : /invite/i })). toBeNull ();
});
it ( "shows the room avatar in a room with only ourselves" , () => {
// When we render a non-DM room with 1 person in it
const room = createRoom ({ name : "X Room" , isDm : false , userIds : [] });
const rendered = mountHeader ( room );
// Then the room's avatar is the initial of its name
2023-08-24 04:48:35 +01:00
const initial = rendered . container . querySelector ( ".mx_BaseAvatar" );
2023-08-01 08:32:53 +01:00
expect ( initial ). toHaveTextContent ( "X" );
});
it ( "shows the room avatar in a room with 2 people" , () => {
// When we render a non-DM room with 2 people in it
const room = createRoom ({ name : "Y Room" , isDm : false , userIds : [ "other" ] });
const rendered = mountHeader ( room );
// Then the room's avatar is the initial of its name
2023-08-24 04:48:35 +01:00
const initial = rendered . container . querySelector ( ".mx_BaseAvatar" );
2023-08-01 08:32:53 +01:00
expect ( initial ). toHaveTextContent ( "Y" );
});
it ( "shows the room avatar in a room with >2 people" , () => {
// When we render a non-DM room with 3 people in it
const room = createRoom ({ name : "Z Room" , isDm : false , userIds : [ "other1" , "other2" ] });
const rendered = mountHeader ( room );
// Then the room's avatar is the initial of its name
2023-08-24 04:48:35 +01:00
const initial = rendered . container . querySelector ( ".mx_BaseAvatar" );
2023-08-01 08:32:53 +01:00
expect ( initial ). toHaveTextContent ( "Z" );
});
it ( "shows the room avatar in a DM with only ourselves" , () => {
// When we render a non-DM room with 1 person in it
const room = createRoom ({ name : "Z Room" , isDm : true , userIds : [] });
const rendered = mountHeader ( room );
// Then the room's avatar is the initial of its name
2023-08-24 04:48:35 +01:00
const initial = rendered . container . querySelector ( ".mx_BaseAvatar" );
2023-08-01 08:32:53 +01:00
expect ( initial ). toHaveTextContent ( "Z" );
});
it ( "shows the user avatar in a DM with 2 people" , () => {
// Note: this is the interesting case - this is the ONLY
// time we should use the user's avatar.
// When we render a DM room with only 2 people in it
const room = createRoom ({ name : "Y Room" , isDm : true , userIds : [ "other" ] });
const rendered = mountHeader ( room );
// Then we use the other user's avatar as our room's image avatar
2023-08-24 04:48:35 +01:00
const image = rendered . container . querySelector ( ".mx_BaseAvatar img" );
2023-08-01 08:32:53 +01:00
expect ( image ). toHaveAttribute ( "src" , "http://this.is.a.url/example.org/other" );
});
it ( "shows the room avatar in a DM with >2 people" , () => {
// When we render a DM room with 3 people in it
const room = createRoom ({
name : "Z Room" ,
isDm : true ,
userIds : [ "other1" , "other2" ],
});
const rendered = mountHeader ( room );
// Then the room's avatar is the initial of its name
2023-08-24 04:48:35 +01:00
const initial = rendered . container . querySelector ( ".mx_BaseAvatar" );
2023-08-01 08:32:53 +01:00
expect ( initial ). toHaveTextContent ( "Z" );
});
it ( "renders call buttons normally" , () => {
const room = createRoom ({ name : "Room" , isDm : false , userIds : [ "other" ] });
const wrapper = mountHeader ( room );
expect ( wrapper . container . querySelector ( '[aria-label="Voice call"]' )). toBeDefined ();
expect ( wrapper . container . querySelector ( '[aria-label="Video call"]' )). toBeDefined ();
});
it ( "hides call buttons when the room is tombstoned" , () => {
const room = createRoom ({ name : "Room" , isDm : false , userIds : [] });
const wrapper = mountHeader (
room ,
{},
{
tombstone : mkEvent ({
event : true ,
type : "m.room.tombstone" ,
room : room.roomId ,
user : "@user1:server" ,
skey : "" ,
content : {},
ts : Date.now (),
}),
},
);
expect ( wrapper . container . querySelector ( '[aria-label="Voice call"]' )). toBeFalsy ();
expect ( wrapper . container . querySelector ( '[aria-label="Video call"]' )). toBeFalsy ();
});
it ( "should render buttons if not passing showButtons (default true)" , () => {
const room = createRoom ({ name : "Room" , isDm : false , userIds : [] });
const wrapper = mountHeader ( room );
expect ( wrapper . container . querySelector ( ".mx_LegacyRoomHeader_button" )). toBeDefined ();
});
it ( "should not render buttons if passing showButtons = false" , () => {
const room = createRoom ({ name : "Room" , isDm : false , userIds : [] });
const wrapper = mountHeader ( room , { showButtons : false });
expect ( wrapper . container . querySelector ( ".mx_LegacyRoomHeader_button" )). toBeFalsy ();
});
it ( "should render the room options context menu if not passing enableRoomOptionsMenu (default true) and UIComponent customisations room options enabled" , () => {
mocked ( shouldShowComponent ). mockReturnValue ( true );
const room = createRoom ({ name : "Room" , isDm : false , userIds : [] });
const wrapper = mountHeader ( room );
expect ( shouldShowComponent ). toHaveBeenCalledWith ( UIComponent . RoomOptionsMenu );
expect ( wrapper . container . querySelector ( ".mx_LegacyRoomHeader_name.mx_AccessibleButton" )). toBeDefined ();
});
it . each ([
[ false , true ],
[ true , false ],
])(
"should not render the room options context menu if passing enableRoomOptionsMenu = %s and UIComponent customisations room options enable = %s" ,
( enableRoomOptionsMenu , showRoomOptionsMenu ) => {
mocked ( shouldShowComponent ). mockReturnValue ( showRoomOptionsMenu );
const room = createRoom ({ name : "Room" , isDm : false , userIds : [] });
const wrapper = mountHeader ( room , { enableRoomOptionsMenu });
expect ( wrapper . container . querySelector ( ".mx_LegacyRoomHeader_name.mx_AccessibleButton" )). toBeFalsy ();
},
);
2023-11-01 13:03:10 +01:00
it ( "renders additionalButtons" , async () => {
const additionalButtons : ViewRoomOpts [ "buttons" ] = [
{
2023-11-09 12:23:30 +01:00
icon : () => <> test - icon </>,
2023-11-01 13:03:10 +01:00
id : "test-id" ,
label : () => "test-label" ,
onClick : () => {},
},
];
renderHeader ({ additionalButtons });
expect ( screen . getByRole ( "button" , { name : "test-icon" })). toBeInTheDocument ();
});
it ( "calls onClick-callback on additionalButtons" , () => {
const callback = jest . fn ();
const additionalButtons : ViewRoomOpts [ "buttons" ] = [
{
2023-11-09 12:23:30 +01:00
icon : () => <> test - icon </>,
2023-11-01 13:03:10 +01:00
id : "test-id" ,
label : () => "test-label" ,
onClick : callback ,
},
];
renderHeader ({ additionalButtons });
fireEvent . click ( screen . getByRole ( "button" , { name : "test-icon" }));
expect ( callback ). toHaveBeenCalled ();
});
2023-08-01 08:32:53 +01:00
});
interface IRoomCreationInfo {
name : string ;
isDm : boolean ;
userIds : string [];
}
function createRoom ( info : IRoomCreationInfo ) {
stubClient ();
const client : MatrixClient = MatrixClientPeg . safeGet ();
const roomId = "!1234567890:domain" ;
const userId = client . getUserId () ! ;
if ( info . isDm ) {
client . getAccountData = ( eventType ) => {
2024-01-29 18:52:48 +01:00
if ( eventType === "m.direct" ) {
return mkDirectEvent ( roomId , userId , info . userIds );
} else {
return undefined ;
}
2023-08-01 08:32:53 +01:00
};
}
DMRoomMap . makeShared ( client ). start ();
const room = new Room ( roomId , client , userId , {
pendingEventOrdering : PendingEventOrdering.Detached ,
});
const otherJoinEvents : MatrixEvent [] = [];
for ( const otherUserId of info . userIds ) {
otherJoinEvents . push ( mkJoinEvent ( roomId , otherUserId ));
}
room . currentState . setStateEvents ([
mkCreationEvent ( roomId , userId ),
mkNameEvent ( roomId , userId , info . name ),
mkJoinEvent ( roomId , userId ),
... otherJoinEvents ,
]);
room . recalculate ();
return room ;
}
function mountHeader ( room : Room , propsOverride = {}, roomContext? : Partial < IRoomState >) : RenderResult {
const props : RoomHeaderProps = {
room ,
inRoom : true ,
onSearchClick : () => {},
onInviteClick : null ,
onForgetClick : () => {},
onAppsClick : () => {},
e2eStatus : E2EStatus.Normal ,
appsShown : true ,
searchInfo : {
searchId : Math.random (),
promise : new Promise < ISearchResults >(() => {}),
term : "" ,
scope : SearchScope.Room ,
count : 0 ,
},
viewingCall : false ,
activeCall : null ,
... propsOverride ,
};
return render (
< RoomContext.Provider value = {{ ... roomContext , room } as IRoomState }>
< RoomHeader { ...props } />
</ RoomContext.Provider >,
);
}
function mkCreationEvent ( roomId : string , userId : string ) : MatrixEvent {
return mkEvent ({
event : true ,
type : "m.room.create" ,
room : roomId ,
user : userId ,
content : {
creator : userId ,
room_version : "5" ,
predecessor : {
room_id : "!prevroom" ,
event_id : "$someevent" ,
},
},
});
}
function mkNameEvent ( roomId : string , userId : string , name : string ) : MatrixEvent {
return mkEvent ({
event : true ,
type : "m.room.name" ,
room : roomId ,
user : userId ,
content : { name },
});
}
function mkJoinEvent ( roomId : string , userId : string ) {
const ret = mkEvent ({
event : true ,
type : "m.room.member" ,
room : roomId ,
user : userId ,
content : {
2024-03-12 14:52:54 +00:00
membership : KnownMembership.Join ,
2023-08-01 08:32:53 +01:00
avatar_url : "mxc://example.org/" + userId ,
},
});
ret . event . state_key = userId ;
return ret ;
}
function mkDirectEvent ( roomId : string , userId : string , otherUsers : string []) : MatrixEvent {
const content : Record < string , string [] > = {};
for ( const otherUserId of otherUsers ) {
content [ otherUserId ] = [ roomId ];
}
return mkEvent ({
event : true ,
type : "m.direct" ,
room : roomId ,
user : userId ,
content ,
});
}