2022-05-06 12:09:28 +03:00
/ *
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 .
* /
2022-12-12 14:24:14 +03:00
import React from "react" ;
2023-02-28 11:58:23 +03:00
import { render , screen , act , fireEvent , waitFor , getByRole , RenderResult } from "@testing-library/react" ;
2022-09-25 17:57:25 +03:00
import { mocked , Mocked } from "jest-mock" ;
import { EventType , RoomType } from "matrix-js-sdk/src/@types/event" ;
import { Room } from "matrix-js-sdk/src/models/room" ;
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state" ;
import { PendingEventOrdering } from "matrix-js-sdk/src/client" ;
import { CallType } from "matrix-js-sdk/src/webrtc/call" ;
2022-10-07 05:27:28 +03:00
import { ClientWidgetApi , Widget } from "matrix-widget-api" ;
import EventEmitter from "events" ;
2022-12-12 14:24:14 +03:00
import { ISearchResults } from "matrix-js-sdk/src/@types/search" ;
2022-09-25 17:57:25 +03:00
import type { MatrixClient } from "matrix-js-sdk/src/client" ;
import type { MatrixEvent } from "matrix-js-sdk/src/models/event" ;
import type { RoomMember } from "matrix-js-sdk/src/models/room-member" ;
import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call" ;
import {
stubClient ,
mkRoomMember ,
setupAsyncStoreWithClient ,
resetAsyncStoreWithClient ,
mockPlatformPeg ,
} from "../../../test-utils" ;
2022-12-12 14:24:14 +03:00
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg" ;
import DMRoomMap from "../../../../src/utils/DMRoomMap" ;
2022-09-25 17:57:25 +03:00
import RoomHeader , { IProps as RoomHeaderProps } from "../../../../src/components/views/rooms/RoomHeader" ;
2022-12-12 14:24:14 +03:00
import { SearchScope } from "../../../../src/components/views/rooms/SearchBar" ;
import { E2EStatus } from "../../../../src/utils/ShieldUtils" ;
import { mkEvent } from "../../../test-utils" ;
2022-03-04 18:53:22 +03:00
import { IRoomState } from "../../../../src/components/structures/RoomView" ;
2022-12-12 14:24:14 +03:00
import RoomContext from "../../../../src/contexts/RoomContext" ;
2022-09-25 17:57:25 +03:00
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" ;
2022-10-07 05:27:28 +03:00
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore" ;
import WidgetUtils from "../../../../src/utils/WidgetUtils" ;
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions" ;
import MediaDeviceHandler , { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler" ;
2023-06-09 15:33:54 +03:00
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents" ;
import { UIComponent } from "../../../../src/settings/UIFeature" ;
jest . mock ( "../../../../src/customisations/helpers/UIComponents" , ( ) = > ( {
shouldShowComponent : jest.fn ( ) ,
} ) ) ;
2022-09-25 17:57:25 +03:00
2023-02-28 11:58:23 +03:00
describe ( "RoomHeader" , ( ) = > {
2022-09-25 17:57:25 +03:00
let client : Mocked < MatrixClient > ;
let room : Room ;
let alice : RoomMember ;
let bob : RoomMember ;
let carol : RoomMember ;
beforeEach ( async ( ) = > {
mockPlatformPeg ( { supportsJitsiScreensharing : ( ) = > true } ) ;
stubClient ( ) ;
2023-06-05 20:12:23 +03:00
client = mocked ( MatrixClientPeg . safeGet ( ) ) ;
2022-09-25 17:57:25 +03:00
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" ) ] ) ;
2022-12-12 14:24:14 +03:00
client . getRoom . mockImplementation ( ( roomId ) = > ( roomId === room . roomId ? room : null ) ) ;
2022-09-25 17:57:25 +03:00
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 ,
content ,
} ) ;
room . addLiveEvents ( [ event ] ) ;
2023-02-16 12:38:44 +03:00
return { event_id : event.getId ( ) ! } ;
2022-09-25 17:57:25 +03:00
} ) ;
alice = mkRoomMember ( room . roomId , "@alice:example.org" ) ;
bob = mkRoomMember ( room . roomId , "@bob:example.org" ) ;
carol = mkRoomMember ( room . roomId , "@carol:example.org" ) ;
2022-12-12 14:24:14 +03:00
client . getRoom . mockImplementation ( ( roomId ) = > ( roomId === room . roomId ? room : null ) ) ;
2022-09-25 17:57:25 +03:00
client . getRooms . mockReturnValue ( [ room ] ) ;
client . reEmitter . reEmit ( room , [ RoomStateEvent . Events ] ) ;
2022-12-12 14:24:14 +03:00
await Promise . all (
[ CallStore . instance , WidgetStore . instance ] . map ( ( store ) = > setupAsyncStoreWithClient ( store , client ) ) ,
) ;
2022-10-07 05:27:28 +03:00
jest . spyOn ( MediaDeviceHandler , "getDevices" ) . mockResolvedValue ( {
[ MediaDeviceKindEnum . AudioInput ] : [ ] ,
[ MediaDeviceKindEnum . VideoInput ] : [ ] ,
[ MediaDeviceKindEnum . AudioOutput ] : [ ] ,
} ) ;
2023-02-28 11:58:23 +03:00
2023-05-23 18:24:12 +03:00
DMRoomMap . makeShared ( client ) ;
2023-02-28 11:58:23 +03:00
jest . spyOn ( DMRoomMap . shared ( ) , "getUserIdForRoomId" ) . mockReturnValue ( carol . userId ) ;
2022-09-25 17:57:25 +03:00
} ) ;
afterEach ( async ( ) = > {
await Promise . all ( [ CallStore . instance , WidgetStore . instance ] . map ( resetAsyncStoreWithClient ) ) ;
client . reEmitter . stopReEmitting ( room , [ RoomStateEvent . Events ] ) ;
jest . restoreAllMocks ( ) ;
2023-04-26 12:36:00 +03:00
SdkConfig . reset ( ) ;
2022-09-25 17:57:25 +03:00
} ) ;
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 (
2022-12-12 14:24:14 +03:00
( userId ) = > members . find ( ( member ) = > member . userId === userId ) ? ? null ,
2022-09-25 17:57:25 +03:00
) ;
} ;
const mockEnabledSettings = ( settings : string [ ] ) = > {
2022-12-12 14:24:14 +03:00
jest . spyOn ( SettingsStore , "getValue" ) . mockImplementation ( ( settingName ) = > settings . includes ( settingName ) ) ;
2022-09-25 17:57:25 +03:00
} ;
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 ) ;
} ;
2022-12-12 14:24:14 +03:00
const withCall = async ( fn : ( call : ElementCall ) = > void | Promise < void > ) : Promise < void > = > {
2022-10-07 05:27:28 +03:00
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 ) ;
} ;
2022-09-25 17:57:25 +03:00
const renderHeader = ( props : Partial < RoomHeaderProps > = { } , roomContext : Partial < IRoomState > = { } ) = > {
render (
< RoomContext.Provider value = { { . . . roomContext , room } as IRoomState } >
< RoomHeader
room = { room }
inRoom = { true }
2022-12-12 14:24:14 +03:00
onSearchClick = { ( ) = > { } }
2022-09-25 17:57:25 +03:00
onInviteClick = { null }
2022-12-12 14:24:14 +03:00
onForgetClick = { ( ) = > { } }
onAppsClick = { ( ) = > { } }
2022-09-25 17:57:25 +03:00
e2eStatus = { E2EStatus . Normal }
appsShown = { true }
searchInfo = { {
2022-11-18 19:40:22 +03:00
searchId : Math.random ( ) ,
promise : new Promise < ISearchResults > ( ( ) = > { } ) ,
term : "" ,
scope : SearchScope.Room ,
count : 0 ,
2022-09-25 17:57:25 +03:00
} }
2022-10-07 05:27:28 +03:00
viewingCall = { false }
activeCall = { null }
2022-09-25 17:57:25 +03:00
{ . . . 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 (
2022-12-12 14:24:14 +03:00
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
"and there's an ongoing call" ,
2022-09-25 17:57:25 +03:00
async ( ) = > {
mockEnabledSettings ( [ "showCallButtonsInComposer" , "feature_group_calls" ] ) ;
2022-12-12 14:24:14 +03:00
SdkConfig . put ( {
element_call : { url : "https://call.element.io" , use_exclusively : true , brand : "Element Call" } ,
} ) ;
2022-09-25 17:57:25 +03:00
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 (
2022-12-12 14:24:14 +03:00
"hides the voice call button and starts an Element call when the video call button is pressed if configured to " +
"use Element Call exclusively" ,
2022-09-25 17:57:25 +03:00
async ( ) = > {
mockEnabledSettings ( [ "showCallButtonsInComposer" , "feature_group_calls" ] ) ;
2022-12-12 14:24:14 +03:00
SdkConfig . put ( {
element_call : { url : "https://call.element.io" , use_exclusively : true , brand : "Element Call" } ,
} ) ;
2022-09-25 17:57:25 +03:00
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" } ) ) ;
2022-12-12 14:24:14 +03:00
await waitFor ( ( ) = >
expect ( dispatcherSpy ) . toHaveBeenCalledWith ( {
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : true ,
} ) ,
) ;
2022-09-25 17:57:25 +03:00
defaultDispatcher . unregister ( dispatcherRef ) ;
} ,
) ;
it (
2022-12-12 14:24:14 +03:00
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
"and the user lacks permission" ,
2022-09-25 17:57:25 +03:00
( ) = > {
mockEnabledSettings ( [ "showCallButtonsInComposer" , "feature_group_calls" ] ) ;
2022-12-12 14:24:14 +03:00
SdkConfig . put ( {
element_call : { url : "https://call.element.io" , use_exclusively : true , brand : "Element Call" } ,
} ) ;
2022-09-25 17:57:25 +03:00
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 (
2022-12-12 14:24:14 +03:00
"starts a legacy 1:1 call when call buttons are pressed in the new group call experience if there's 1 other " +
"member" ,
2022-09-25 17:57:25 +03:00
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
expect ( placeCallSpy ) . toHaveBeenCalledWith ( room . roomId , CallType . Video ) ;
} ,
) ;
it (
2022-12-12 14:24:14 +03:00
"creates a Jitsi widget when call buttons are pressed in the new group call experience if the user lacks " +
"permission to start Element calls" ,
2022-09-25 17:57:25 +03:00
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 (
2022-12-12 14:24:14 +03:00
"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" ,
2022-09-25 17:57:25 +03:00
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 } ) ) ;
2022-12-12 14:24:14 +03:00
await waitFor ( ( ) = >
expect ( dispatcherSpy ) . toHaveBeenCalledWith ( {
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : true ,
} ) ,
) ;
2022-09-25 17:57:25 +03:00
defaultDispatcher . unregister ( dispatcherRef ) ;
} ,
) ;
it (
2022-12-12 14:24:14 +03:00
"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" ,
2022-09-25 17:57:25 +03:00
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" } ) ) ;
2022-12-12 14:24:14 +03:00
await waitFor ( ( ) = >
expect ( dispatcherSpy ) . toHaveBeenCalledWith ( {
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : true ,
} ) ,
) ;
2022-09-25 17:57:25 +03:00
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" ) ;
} ) ;
2022-10-07 05:27:28 +03:00
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 } ) ) ;
2022-12-12 14:24:14 +03:00
await waitFor ( ( ) = >
expect ( dispatcherSpy ) . toHaveBeenCalledWith ( {
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : false ,
} ) ,
) ;
2022-10-07 05:27:28 +03:00
defaultDispatcher . unregister ( dispatcherRef ) ;
} ) ;
it ( "shows a reduce button when viewing a call that returns to the timeline when pressed" , async ( ) = > {
mockEnabledSettings ( [ "feature_group_calls" ] ) ;
2022-12-12 14:24:14 +03:00
await withCall ( async ( call ) = > {
2022-10-07 05:27:28 +03:00
renderHeader ( { viewingCall : true , activeCall : call } ) ;
const dispatcherSpy = jest . fn ( ) ;
const dispatcherRef = defaultDispatcher . register ( dispatcherSpy ) ;
fireEvent . click ( screen . getByRole ( "button" , { name : /timeline/i } ) ) ;
2022-12-12 14:24:14 +03:00
await waitFor ( ( ) = >
expect ( dispatcherSpy ) . toHaveBeenCalledWith ( {
action : Action.ViewRoom ,
room_id : room.roomId ,
view_call : false ,
} ) ,
) ;
2022-10-07 05:27:28 +03:00
defaultDispatcher . unregister ( dispatcherRef ) ;
} ) ;
} ) ;
it ( "shows a layout button when viewing a call that shows a menu when pressed" , async ( ) = > {
mockEnabledSettings ( [ "feature_group_calls" ] ) ;
2022-12-12 14:24:14 +03:00
await withCall ( async ( call ) = > {
2022-10-07 05:27:28 +03:00
await call . connect ( ) ;
2023-03-08 14:48:58 +03:00
const messaging = WidgetMessagingStore . instance . getMessagingForUid ( WidgetUtils . getWidgetUid ( call . widget ) ) ! ;
2022-10-07 05:27:28 +03:00
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 ( ) ;
} ) ;
2023-02-28 11:58:23 +03:00
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
const initial = rendered . container . querySelector ( ".mx_BaseAvatar_initial" ) ;
expect ( initial ) . toHaveTextContent ( "X" ) ;
// And there is no image avatar (because it's not set on this room)
const image = rendered . container . querySelector ( ".mx_BaseAvatar_image" ) ;
expect ( image ) . toHaveAttribute ( "src" , "data:image/png;base64,00" ) ;
} ) ;
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
const initial = rendered . container . querySelector ( ".mx_BaseAvatar_initial" ) ;
expect ( initial ) . toHaveTextContent ( "Y" ) ;
// And there is no image avatar (because it's not set on this room)
const image = rendered . container . querySelector ( ".mx_BaseAvatar_image" ) ;
expect ( image ) . toHaveAttribute ( "src" , "data:image/png;base64,00" ) ;
} ) ;
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
const initial = rendered . container . querySelector ( ".mx_BaseAvatar_initial" ) ;
expect ( initial ) . toHaveTextContent ( "Z" ) ;
// And there is no image avatar (because it's not set on this room)
const image = rendered . container . querySelector ( ".mx_BaseAvatar_image" ) ;
expect ( image ) . toHaveAttribute ( "src" , "data:image/png;base64,00" ) ;
} ) ;
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
const initial = rendered . container . querySelector ( ".mx_BaseAvatar_initial" ) ;
expect ( initial ) . toHaveTextContent ( "Z" ) ;
// And there is no image avatar (because it's not set on this room)
const image = rendered . container . querySelector ( ".mx_BaseAvatar_image" ) ;
expect ( image ) . toHaveAttribute ( "src" , "data:image/png;base64,00" ) ;
} ) ;
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
const image = rendered . container . querySelector ( ".mx_BaseAvatar_image" ) ;
expect ( image ) . toHaveAttribute ( "src" , "http://this.is.a.url/example.org/other" ) ;
// And there is no initial avatar
expect ( rendered . container . querySelector ( ".mx_BaseAvatar_initial" ) ) . toBeFalsy ( ) ;
} ) ;
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
const initial = rendered . container . querySelector ( ".mx_BaseAvatar_initial" ) ;
expect ( initial ) . toHaveTextContent ( "Z" ) ;
// And there is no image avatar (because it's not set on this room)
const image = rendered . container . querySelector ( ".mx_BaseAvatar_image" ) ;
expect ( image ) . toHaveAttribute ( "src" , "data:image/png;base64,00" ) ;
} ) ;
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_RoomHeader_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_RoomHeader_button" ) ) . toBeFalsy ( ) ;
} ) ;
2023-06-09 15:33:54 +03:00
it ( "should render the room options context menu if not passing enableRoomOptionsMenu (default true) and UIComponent customisations room options enabled" , ( ) = > {
mocked ( shouldShowComponent ) . mockReturnValue ( true ) ;
2023-02-28 11:58:23 +03:00
const room = createRoom ( { name : "Room" , isDm : false , userIds : [ ] } ) ;
const wrapper = mountHeader ( room ) ;
2023-06-09 15:33:54 +03:00
expect ( shouldShowComponent ) . toHaveBeenCalledWith ( UIComponent . RoomOptionsMenu ) ;
2023-02-28 11:58:23 +03:00
expect ( wrapper . container . querySelector ( ".mx_RoomHeader_name.mx_AccessibleButton" ) ) . toBeDefined ( ) ;
} ) ;
2023-06-09 15:33:54 +03:00
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_RoomHeader_name.mx_AccessibleButton" ) ) . toBeFalsy ( ) ;
} ,
) ;
2022-09-25 17:57:25 +03:00
} ) ;
2023-02-28 11:58:23 +03:00
interface IRoomCreationInfo {
name : string ;
isDm : boolean ;
userIds : string [ ] ;
}
function createRoom ( info : IRoomCreationInfo ) {
stubClient ( ) ;
2023-06-05 20:12:23 +03:00
const client : MatrixClient = MatrixClientPeg . safeGet ( ) ;
2023-02-28 11:58:23 +03:00
const roomId = "!1234567890:domain" ;
const userId = client . getUserId ( ) ! ;
if ( info . isDm ) {
client . getAccountData = ( eventType ) = > {
expect ( eventType ) . toEqual ( "m.direct" ) ;
return mkDirectEvent ( roomId , userId , info . userIds ) ;
} ;
}
2023-05-23 18:24:12 +03:00
DMRoomMap . makeShared ( client ) . start ( ) ;
2023-02-28 11:58:23 +03:00
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 : {
membership : "join" ,
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 ,
} ) ;
}