Compare commits

...

29 Commits
8317 ... 8342

Author SHA1 Message Date
Дамян Минков
ada6150971 feat(tests): Adds dial-in test. (#15470)
* feat(tests): Adds dial-in test.

* feat(tests): Adds fake dial-in test.

* squash: switch to performance.now().
2025-01-10 06:28:53 -06:00
damencho
62d1ee606a feat(tests): Adds desktop sharing test. 2025-01-09 18:29:47 -06:00
simplify123
1e1de6acf0 lang: Update main-zhCN.json (#15426)
main-zhCN.json 
line 413 missing:
"sessTerminatedReason": "会议已经结束",
2025-01-09 15:04:23 -06:00
damencho
e1e0fba4ff fix(readme): Fixes badge icon.
Fixes #15464
2025-01-09 15:04:02 -06:00
Jaya Allamsetty
97be3fa399 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1893.0.0+496b64a7...v1895.0.0+7de7d9aa
2025-01-09 15:22:18 -05:00
damencho
fbdee85c4b fix(readme): Fixes badge icon and f-droid link.
Fixes #15464
Fixes #15465
2025-01-09 12:38:11 -06:00
Bhavik Arora
b71b4c8c45 fix(video-menu): Hide "Mute all" option when all participants are moderators (#15461)
* Fixed exception for visibility state of mute all button

* Updated changes as per review: shifted conditional to the toggle

* chore: fix eslint warnings and errors

---------

Co-authored-by: Bhavik Arora <arorabhavik1001>
2025-01-09 10:21:28 -06:00
Florian
a8d123ede4 fix(lang) fix typo in unsecure room name warning (#15468) 2025-01-09 17:01:23 +01:00
Hristo Terezov
fb8e451e2e feat(customParticipantButton): metrics 2025-01-08 11:11:21 -06:00
damencho
5dd7944bdb fix(connection): Adds default tenant value on error.
Avoids undefined error.
2025-01-08 08:36:35 -06:00
Jaya Allamsetty
48732c6784 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1890.0.0+144b0cab...v1893.0.0+496b64a7
2025-01-07 21:45:03 -06:00
Hristo Terezov
97146ed8a7 fix(configWhitelist): Remove some props. 2025-01-07 15:32:08 -06:00
Дамян Минков
78b17c8d17 feat(tests): Adds self view, display name and end conference tests. (#15432)
* feat(tests): Use shorter display names for screenshots.

* feat(tests): Adds self view tests.

* feat(tests): Adds display name test.

* feat(tests): Adds end conference test.
2025-01-07 14:55:43 -06:00
Saúl Ibarra Corretgé
9732d1be86 fix(polls) improve resilience to bogus data 2025-01-07 16:06:10 +01:00
Hristo Terezov
a836187620 feat(URL-overrides): Add metrics. 2025-01-06 09:35:36 -06:00
Wilson Furtado
a0cad4c595 fix(notification) add tabIndex to close Icon in the notification Item 2025-01-06 10:13:06 +01:00
Wilson Furtado
19b9eac84a fix(input) fix content overlapping when clearable icon is present 2025-01-06 10:12:14 +01:00
Wilson Furtado
068e33fc0f fix(toolbar): Removed unnecessary re render of the reactions component (#15433)
Added usememo to remove unnecessary re render. Fixes #15434.
2024-12-31 10:04:47 -06:00
Wilson Furtado
00c6bee2fd feat(welcome-page): added hover fill to trash icon (#15431)
Fixes #15430.
2024-12-30 16:11:43 -06:00
Wilson Furtado
748ead7e13 feat: Added hover styles and added tabIndex to the meeting list container instead of the meeting item header (#15429)
* added hover styles and added tabIndex to the container instead of the meeting heading

* lint error fix
2024-12-30 14:05:56 -06:00
damencho
174c4418fb fix(prosody): Adds another condition to the filter. 2024-12-26 12:12:53 -06:00
Damien Fetis
64494cab81 lang: Update missing french labels (#15422) 2024-12-23 18:28:35 -06:00
damencho
50d0092e30 fix(shared-video): Remove disable button action from web. 2024-12-23 07:46:25 -06:00
damencho
fdbd7239ff fix(shared-video): Gets from info from the incoming presence.
Ignore using from field send in attributes of the command.
2024-12-23 07:46:25 -06:00
Дамян Минков
a30958ab23 feat(tests): Adds chat panel and codec selection tests. (#15416)
* fix(tests): Another attempt to fix Firefox excludes.

Drawback is that it will be a little bit slow.

* feat(tests): Adds chatPanel tests.

* feat(tests): Adds codec selection tests.
2024-12-20 17:04:07 -06:00
damencho
aca55172e4 fix(config): Fixes wrong file location. 2024-12-20 11:37:48 -06:00
Дамян Минков
c6cce9253c feat(tests): Adds breakout tests. (#15414)
* feat(tests): Introduces BasePageObject.

* fix(tests): Use wdio aria selector where possible.

* fix(tests): Correct test exclusion for Firefox.

* fix(tests): Rearrange code.

* feat(tests): Adds breakout tests.
2024-12-20 06:17:49 -06:00
Saúl Ibarra Corretgé
c23684e11c fix(devices) filter out Zoom audio device
I haven't seen any particular problem with it, but it's of no use
anyway...
2024-12-20 07:58:48 +01:00
luzpaz
30595584a3 chore(typos) fix various typos
Found via `codespell -q 3 -S "*.svg,./lang" -L anser,bu,dialin,goup,miliseconds,nd,vew`
2024-12-20 07:58:31 +01:00
105 changed files with 2434 additions and 412 deletions

View File

@@ -34,7 +34,7 @@ mobile apps:
| Android | Android (F-Droid) | iOS |
|:-:|:-:|:-:|
| [<img src="resources/img/google-play-badge.png" height="50">](https://play.google.com/store/apps/details?id=org.jitsi.meet) | [<img src="resources/img/f-droid-badge.png" height="50">](https://f-droid.org/en/packages/org.jitsi.meet/) | [<img src="resources/img/appstore-badge.png" height="50">](https://itunes.apple.com/us/app/jitsi-meet/id1165103905) |
| [<img src="resources/img/google-play-badge.png" height="50">](https://play.google.com/store/apps/details?id=org.jitsi.meet) | [<img src="resources/img/f-droid-badge.png" height="50">](https://f-droid.org/packages/org.jitsi.meet/) | [<img src="resources/img/appstore-badge.png" height="50">](https://itunes.apple.com/us/app/jitsi-meet/id1165103905) |
If you are feeling adventurous and want to get an early scoop of the features as they are being
developed you can also sign up for our open beta testing here:

View File

@@ -42,7 +42,7 @@ public class JitsiMeetActivityDelegate {
/**
* Tells whether or not the permissions request is currently in progress.
*
* @return {@code true} if the permssions are being requested or {@code false} otherwise.
* @return {@code true} if the permissions are being requested or {@code false} otherwise.
*/
static boolean arePermissionsBeingRequested() {
return permissionListener != null;

View File

@@ -459,7 +459,7 @@ var config = {
// // Translation languages.
// // Available languages can be found in
// // ./src/react/features/transcribing/translation-languages.json.
// // ./lang/translation-languages.json.
// translationLanguages: ['en', 'es', 'fr', 'ro'],
// // Important languages to show on the top of the language list.
@@ -907,7 +907,7 @@ var config = {
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
// buttons varies from 2 buttons to 8 buttons. Every array in the mainToolbarButtons array will replace the
// corresponding default buttons configuration matched by the number of buttons specified in the array. Arrays with
// more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a cerain
// more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a certain
// configuration (for example when 3 buttons are displayed) the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.
// mainToolbarButtons: [
@@ -1735,7 +1735,7 @@ var config = {
// 'notify.participantsWantToJoin', // shown when lobby is enabled and participants request to join meeting
// 'notify.passwordRemovedRemotely', // shown when a password has been removed remotely
// 'notify.passwordSetRemotely', // shown when a password has been set remotely
// 'notify.raisedHand', // shown when a partcipant used raise hand,
// 'notify.raisedHand', // shown when a participant used raise hand,
// 'notify.screenShareNoAudio', // shown when the audio could not be shared for the selected screen
// 'notify.screenSharingAudioOnlyTitle', // shown when the best performance has been affected by screen sharing
// 'notify.selfViewTitle', // show "You can always un-hide the self-view from settings"
@@ -1766,7 +1766,7 @@ var config = {
// disableFilmstripAutohiding: false,
// filmstrip: {
// // Disable the vertical/horizonal filmstrip.
// // Disable the vertical/horizontal filmstrip.
// disabled: false,
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
// // (width, tiles aspect ratios) through the interfaceConfig options.

View File

@@ -19,11 +19,11 @@
flex-direction: column;
.description {
color: #2f3237;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
max-width: 436px;
color: #2f3237;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
max-width: 436px;
}
}
@@ -127,7 +127,8 @@
cursor: pointer;
}
&.with-click-handler:hover {
&.with-click-handler:hover,
&.with-click-handler:focus {
background-color: #c7ddff;
}
@@ -155,14 +156,22 @@
margin-right: 16px;
position: absolute;
&> svg {
&>svg {
fill: #0074e0;
}
}
.item:hover, .item:focus, .item:focus-within {
.item:hover,
.item:focus,
.item:focus-within {
.delete-meeting {
display: block;
}
.delete-meeting:hover {
&>svg {
fill: #4687ED;
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -227,7 +227,7 @@ var interfaceConfig = {
/**
* Specify custom URL for downloading f droid app.
*/
// MOBILE_DOWNLOAD_LINK_F_DROID: 'https://f-droid.org/en/packages/org.jitsi.meet/',
// MOBILE_DOWNLOAD_LINK_F_DROID: 'https://f-droid.org/packages/org.jitsi.meet/',
// Connection indicators (
// CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,

View File

@@ -69,7 +69,7 @@ platform :ios do
end
)
# Inrement the build number by 1
# Increment the build number by 1
increment_build_number(
build_number: Time.now.to_i,
xcodeproj: "app/app.xcodeproj"

View File

@@ -334,6 +334,7 @@
"kickParticipantButton": "Expulser",
"kickParticipantDialog": "Êtes-vous sûr(e) de vouloir expulser ce participant ?",
"kickParticipantTitle": "Expulser ce participant ?",
"kickSystemTitle": "Oups ! Vous avez été expulsé de la réunion",
"kickTitle": "Oups ! vous avez été expulsé(e) par {{participantDisplayName}}",
"linkMeeting": "Relier la conférence",
"linkMeetingTitle": "Relier la conférence à Salesforce",
@@ -439,7 +440,10 @@
"shareScreenWarningD2": "vous devez arrêter le partage d'audio, démarrer le partage d'écran et cocher l'option \"Partager l'audio\".",
"shareScreenWarningH1": "Si vous voulez partager uniquement votre écran:",
"shareScreenWarningTitle": "Vous devez cesser de partager votre audio avant de partager votre écran",
"shareVideoConfirmPlay": "Vous êtes sur le point d'ouvrir un site web externe. Voulez-vous continuer ?",
"shareVideoConfirmPlayTitle": "{{name}} a partagé une vidéo avec vous.",
"shareVideoLinkError": "Veuillez renseigner un lien de diffusion vidéo fonctionnel.",
"shareVideoLinkStopped": "La vidéo de {{name}} a été arrêtée.",
"shareVideoTitle": "Partager une vidéo",
"shareYourScreen": "Partager votre écran",
"shareYourScreenDisabled": "Le partage d'écran est désactivé.",
@@ -638,6 +642,7 @@
"on": "En direct",
"onBy": "{{name}} a démarré la diffusion en direct",
"pending": "Lancement du direct…",
"policyError": "Vous avez essayé de démarrer une diffusion en direct trop rapidement. Veuillez réessayer plus tard !",
"serviceName": "Service de diffusion en direct",
"sessionAlreadyActive": "Cette session est déjà en cours d'enregistrement ou de diffusion.",
"signIn": "Se connecter avec Google",
@@ -736,6 +741,7 @@
"connectedOneMember": "{{name}} a rejoint la réunion",
"connectedThreePlusMembers": "{{name}} et {{count}} autres personnes ont rejoint la réunion",
"connectedTwoMembers": "{{first}} et {{second}} ont rejoint la réunion",
"connectionFailed": "Connexion échouée. Veuillez réessayer plus tard !",
"dataChannelClosed": "Qualité vidéo dégradée",
"dataChannelClosedDescription": "Le canal de communication avec le Bridge a été interrompu, la qualité vidéo se trouve limitée à sa valeur la plus faible.",
"dataChannelClosedDescriptionWithAudio": "Le canal de pont est fermé, ce qui peut entraîner des perturbations de l'audio et de la vidéo.",
@@ -750,6 +756,9 @@
"gifsMenu": "GIPHY",
"groupTitle": "Notifications",
"hostAskedUnmute": "Le modérateur souhaite vous donner la parole",
"invalidTenant": "Tenant invalide",
"invalidTenantHyphenDescription": "Le tenant que vous utilisez est invalide (commence ou se termine par '-').",
"invalidTenantLengthDescription": "Le tenant que vous utilisez est trop long.",
"invitedOneMember": "{{name}} a été invité(e)",
"invitedThreePlusMembers": "{{name}} et {{count}} autres ont été invités",
"invitedTwoMembers": "{{first}} et {{second}} ont été invités",
@@ -786,6 +795,7 @@
"newDeviceAction": "Utiliser",
"newDeviceAudioTitle": "Nouveau périphérique audio détecté",
"newDeviceCameraTitle": "Nouvelle caméra détectée",
"nextToSpeak": "Vous êtes le prochain à prendre la parole",
"noiseSuppressionDesktopAudioDescription": "La suppression de bruit ne peut pas être activée en même temps que la partage audio du système, veuillez le désactiver et réessayer.",
"noiseSuppressionFailedTitle": "Échec du démarrage de la suppression de bruit",
"noiseSuppressionStereoDescription": "La suppression de bruit dune source stéréo nest pas encore supportée.",
@@ -820,6 +830,7 @@
"videoUnmuteBlockedDescription": "Le rétablissement de la vidéo a été bloqué temporairement en raison de limites système.",
"videoUnmuteBlockedTitle": "Rétablissement de la caméra bloqué !",
"viewLobby": "Voir la salle d'attente",
"viewParticipants": "Voir les participants",
"viewVisitors": "Voir les visiteurs",
"waitingParticipants": "{{waitingParticipants}} personnes",
"waitingVisitors": "Visiteurs en attente dans la file : {{waitingVisitors}}",
@@ -839,6 +850,8 @@
"breakoutRooms": "Salles annexes",
"goLive": "Passer en direct",
"invite": "Inviter quelqu'un",
"lowerAllHands": "Abaisser toutes les mains",
"lowerHand": "Abaisser la main",
"moreModerationActions": "Options de modération supplémentaires",
"moreModerationControls": "Options de modération supplémentaires",
"moreParticipantOptions": "Options supplémentaires pour les participants",
@@ -875,6 +888,7 @@
"submit": "Envoyer"
},
"by": "Par {{ name }}",
"closeButton": "Fermer le sondage",
"create": {
"addOption": "Ajouter une option",
"answerPlaceholder": "Option {{index}}",
@@ -914,9 +928,11 @@
"configuringDevices": "Configuration des appareils…",
"connectedWithAudioQ": "Êtes-vous connecté avec le microphone ?",
"connection": {
"failed": "Le test de connexion a échoué !",
"good": "Votre connexion Internet est bonne !",
"nonOptimal": "Votre connexion n'est pas optimale",
"poor": "Vous avez une mauvaise connexion"
"poor": "Vous avez une mauvaise connexion",
"running": "Exécution du test de connexion…"
},
"connectionDetails": {
"audioClipping": "Attendez vous à ce que votre son soit coupé.",
@@ -925,6 +941,7 @@
"goodQuality": "Impressionnant ! La qualité de vos médias sera excellente",
"noMediaConnectivity": "Nous n'avons pas pu trouver un moyen d'établir une connectivité multimédia pour ce test. Cela est généralement causé par un pare-feu ou un NAT.",
"noVideo": "Attendez vous à ce que votre qualité vidéo soit très mauvaise.",
"testFailed": "Le test de connexion a rencontré des problèmes inattendus, mais cela pourrait ne pas affecter votre expérience.",
"undetectable": "Si vous ne parvenez toujours pas à passer des appels dans le navigateur, nous vous recommandons de vous assurer que vos haut-parleurs, microphone et caméra sont correctement configurés, que vous avez accordé à votre navigateur les droits d'utiliser votre microphone et votre caméra et que la version de votre navigateur est à jour. Si vous rencontrez toujours des difficultés pour appeler, vous devez contacter le développeur de l'application Web.",
"veryPoorConnection": "Attendez vous à ce que la qualité de votre appel soit très mauvaise",
"videoFreezing": "Attendez vous à ce que votre vidéo saute, soit noire, et pixelisée.",
@@ -1040,6 +1057,7 @@
"onBy": "{{name}} a démarré l'enregistrement",
"onlyRecordSelf": "Enregistrer seulement mon audio et ma vidéo.",
"pending": "Préparation de l'enregistrement de la réunion…",
"policyError": "Vous avez essayé de démarrer un enregistrement trop rapidement. Veuillez réessayer plus tard !",
"recordAudioAndVideo": "Enregistrer l'audio et la vidéo",
"recordTranscription": "Enregistrer la transcription",
"saveLocalRecording": "Sauvegarder lenregistrement local (Beta)",
@@ -1088,6 +1106,7 @@
"desktopShareWarning": "Vous devez repartager l'écran pour que ces paramètres soient utilisés.",
"devices": "Périphériques",
"followMe": "Tout le monde me suit",
"followMeRecorder": "L'enregistreur me suit",
"framesPerSecond": "images par seconde",
"incomingMessage": "un message arrive",
"language": "Langue",
@@ -1234,6 +1253,7 @@
"lobbyButton": "Activer / Désactiver le mode salle d'attente",
"localRecording": "Activer / Désactiver les contrôles d'enregistrement local",
"lockRoom": "Activer / Désactiver le mot de passe de la réunion",
"love": "Cœur",
"lowerHand": "Baisser la main",
"moreActions": "Activer / Désactiver le menu d'actions supplémentaires",
"moreActionsMenu": "Menu d'actions supplémentaires",
@@ -1251,6 +1271,7 @@
"privateMessage": "Envoyer un message privé",
"profile": "Éditer votre profil",
"raiseHand": "Lever la main",
"react": "Réactions aux messages",
"reactions": "Réactions",
"reactionsMenu": "Ouvrir / fermer le menu réactions",
"recording": "Activer / Désactiver l'enregistrement",
@@ -1322,6 +1343,7 @@
"lobbyButtonEnable": "Activer le mode salle d'attente / contrôle des participant(e)s",
"login": "Connexion",
"logout": "Déconnexion",
"love": "Cœur",
"lowerYourHand": "Baisser la main",
"moreActions": "Plus d'actions",
"moreOptions": "Plus d'options",
@@ -1347,6 +1369,7 @@
"raiseYourHand": "Lever la main",
"reactionBoo": "Envoyer réaction huer",
"reactionClap": "Envoyer réaction applaudir",
"reactionHeart": "Envoyer une réaction en forme de cœur",
"reactionLaugh": "Envoyer réaction rire",
"reactionLike": "Envoyer réaction approuver",
"reactionSilence": "Envoyer réaction silence",
@@ -1380,7 +1403,7 @@
"transcribing": {
"ccButtonTooltip": "Activer / Désactiver les sous-titres",
"expandedLabel": "La transcription est actuellement activée",
"failedToStart": "Échec de démarrage de la transcription",
"failed": "La transcription a échoué",
"labelToolTip": "La transcription de la réunion est en cours",
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
"sourceLanguageHere": "ici",
@@ -1481,10 +1504,15 @@
},
"visitors": {
"chatIndicator": "(visiteur)",
"labelTooltip": "Nombre de Visiteurs",
"joinMeeting": {
"description": "Vous êtes actuellement un observateur dans cette conférence.",
"raiseHand": "Levez la main",
"title": "Rejoindre la réunion",
"wishToSpeak": "Si vous souhaitez prendre la parole, veuillez lever la main ci-dessous et attendre l'approbation du modérateur."
},
"labelTooltip": "Nombre de Visiteurs: {{count}}",
"notification": {
"demoteDescription": "Envoyé ici par {{actor}}, levez la main pour participer",
"description": "Pour participer lever la main.",
"noMainParticipantsDescription": "Un participant doit démarrer la réunion. Veuillez réessayer dans un moment.",
"noMainParticipantsTitle": "Cette réunion n'a pas encore commencé.",
"noVisitorLobby": "Vous ne pouvez pas rejoindre tant qu'une salle d'attente est activée pour la réunion.",

View File

@@ -410,6 +410,7 @@
"sendPrivateMessageTitle": "私信回复?",
"serviceUnavailable": "服务不可用",
"sessTerminated": "通话已结束",
"sessTerminatedReason": "会议已经结束",
"sessionRestarted": "由于连接问题,呼叫重新启动。",
"shareAudio": "继续",
"shareAudioTitle": "如何分享音频",

View File

@@ -1083,7 +1083,7 @@
"about": "You can add a $t(lockRoomPassword) to your meeting. Participants will need to provide the $t(lockRoomPassword) before they are allowed to join the meeting.",
"aboutReadOnly": "Moderator participants can add a $t(lockRoomPassword) to the meeting. Participants will need to provide the $t(lockRoomPassword) before they are allowed to join the meeting.",
"insecureRoomNameWarningNative": "The room name is unsafe. Unwanted participants may join your meeting. {{recommendAction}} Learn more about securing your meeting ",
"insecureRoomNameWarningWeb": "The room name is unsafe. Unwanted participants may join your meeting. {{recommendAction}} Learn more about securing you meeting <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">here</a>.",
"insecureRoomNameWarningWeb": "The room name is unsafe. Unwanted participants may join your meeting. {{recommendAction}} Learn more about securing your meeting <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">here</a>.",
"title": "Security Options",
"unsafeRoomActions": {
"meeting": "Consider securing your meeting using the security button.",

View File

@@ -115,7 +115,7 @@ import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions';
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/functions';
@@ -1571,8 +1571,8 @@ class API {
formattedArgument += `${arg.toString()}: ${arg.stack}`;
} else if (typeof arg === 'object') {
// NOTE: The non-enumerable properties of the objects wouldn't be included in the string after
// JSON.strigify. For example Map instance will be translated to '{}'. So I think we have to eventually
// do something better for parsing the arguments. But since this option for strigify is part of the
// JSON.stringify. For example Map instance will be translated to '{}'. So I think we have to eventually
// do something better for parsing the arguments. But since this option for stringify is part of the
// public interface and I think it could be useful in some cases I will it for now.
try {
formattedArgument += JSON.stringify(arg);

24
package-lock.json generated
View File

@@ -62,7 +62,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1895.0.0+7de7d9aa/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -136,6 +136,7 @@
"@types/js-md5": "0.4.3",
"@types/jsonwebtoken": "9.0.7",
"@types/lodash-es": "4.17.12",
"@types/minimatch": "5.1.2",
"@types/mocha": "10.0.10",
"@types/moment-duration-format": "2.2.6",
"@types/offscreencanvas": "2019.7.2",
@@ -7176,6 +7177,13 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mocha": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
@@ -16417,8 +16425,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"integrity": "sha512-/NoS/uUJvMXdbi5gQoihcQ9ovXlQtFKgdmtiLqLJxz91T9JwdVZNsjo3G6RUVIJ07myb/vjQp91kpm45U7CgIw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1895.0.0+7de7d9aa/lib-jitsi-meet.tgz",
"integrity": "sha512-0drO9rkh7W4bOUaQAEO5AWj0VYgYekIt2U9rj9kn5XX9aEUimClVuUtefcSP4dlw7+9Cucg+2lRoGKW9FX8ysw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -29785,6 +29793,12 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"@types/mocha": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
@@ -36497,8 +36511,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"integrity": "sha512-/NoS/uUJvMXdbi5gQoihcQ9ovXlQtFKgdmtiLqLJxz91T9JwdVZNsjo3G6RUVIJ07myb/vjQp91kpm45U7CgIw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1895.0.0+7de7d9aa/lib-jitsi-meet.tgz",
"integrity": "sha512-0drO9rkh7W4bOUaQAEO5AWj0VYgYekIt2U9rj9kn5XX9aEUimClVuUtefcSP4dlw7+9Cucg+2lRoGKW9FX8ysw==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",

View File

@@ -68,7 +68,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1890.0.0+144b0cab/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1895.0.0+7de7d9aa/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -142,6 +142,7 @@
"@types/js-md5": "0.4.3",
"@types/jsonwebtoken": "9.0.7",
"@types/lodash-es": "4.17.12",
"@types/minimatch": "5.1.2",
"@types/mocha": "10.0.10",
"@types/moment-duration-format": "2.2.6",
"@types/offscreencanvas": "2019.7.2",
@@ -220,9 +221,11 @@
"tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
"start": "make dev",
"test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
"test-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts --spec",
"test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
"test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts"
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts",
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec"
},
"resolutions": {
"@types/react": "17.0.14",

View File

@@ -154,6 +154,30 @@ export async function createHandlers({ getState }: IStore) {
return handlers;
}
/**
* Checks whether a url is a data URL or not.
*
* @param {string} url - The URL to be checked.
* @returns {boolean}
*/
function isDataURL(url?: string): boolean {
if (typeof url !== 'string') { // The icon will be ignored
return false;
}
try {
const urlObject = new URL(url);
if (urlObject.protocol === 'data:') {
return false;
}
} catch {
return false;
}
return true;
}
/**
* Inits JitsiMeetJS.analytics by setting permanent properties and setting the handlers from the loaded scripts.
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be null.
@@ -185,13 +209,21 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
inIframe?: boolean;
isPromotedFromVisitor?: boolean;
isVisitor?: boolean;
overwritesCustomButtonsWithURL?: boolean;
overwritesCustomParticipantButtonsWithURL?: boolean;
overwritesDefaultLogoUrl?: boolean;
overwritesDeploymentUrls?: boolean;
overwritesEtherpadBase?: boolean;
overwritesHosts?: boolean;
overwritesIceServers?: boolean;
overwritesLiveStreamingUrls?: boolean;
overwritesPeopleSearchUrl?: boolean;
overwritesPrejoinConfigICEUrl?: boolean;
overwritesSalesforceUrl?: boolean;
overwritesSupportUrl?: boolean;
overwritesWatchRTCConfigParams?: boolean;
overwritesWatchRTCProxyUrl?: boolean;
overwritesWatchRTCWSUrl?: boolean;
server?: string;
tenant?: string;
wasLobbyVisible?: boolean;
@@ -235,6 +267,21 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
permanentProperties.overwritesPeopleSearchUrl = 'config.peopleSearchUrl' in params;
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
permanentProperties.overwritesEtherpadBase = 'config.etherpad_base' in params;
const hosts = params['config.hosts'] ?? {};
const hostsProps = [ 'anonymousdomain', 'authdomain', 'domain', 'focus', 'muc', 'visitorFocus' ];
permanentProperties.overwritesHosts = 'config.hosts' in params
|| Boolean(hostsProps.find(p => `config.hosts.${p}` in params || (typeof hosts === 'object' && p in hosts)));
permanentProperties.overwritesWatchRTCConfigParams = 'config.watchRTCConfigParams' in params;
const watchRTCConfigParams = params['config.watchRTCConfigParams'] ?? {};
permanentProperties.overwritesWatchRTCProxyUrl = ('config.watchRTCConfigParams.proxyUrl' in params)
|| (typeof watchRTCConfigParams === 'object' && 'proxyUrl' in watchRTCConfigParams);
permanentProperties.overwritesWatchRTCWSUrl = ('config.watchRTCConfigParams.wsUrl' in params)
|| (typeof watchRTCConfigParams === 'object' && 'wsUrl' in watchRTCConfigParams);
const prejoinConfig = params['config.prejoinConfig'] ?? {};
permanentProperties.overwritesPrejoinConfigICEUrl = ('config.prejoinConfig.preCallTestICEUrl' in params)
@@ -260,6 +307,18 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
)
);
permanentProperties.overwritesIceServers = Boolean(Object.keys(params).find(k => k.startsWith('iceServers')));
const customToolbarButtons = params['config.customToolbarButtons'] ?? [];
permanentProperties.overwritesCustomButtonsWithURL = Boolean(
customToolbarButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
const customParticipantMenuButtons = params['config.customParticipantMenuButtons'] ?? [];
permanentProperties.overwritesCustomParticipantButtonsWithURL = Boolean(
customParticipantMenuButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
// Optionally, include local deployment information based on the
// contents of window.config.deploymentInfo.
if (deploymentInfo) {

View File

@@ -121,7 +121,7 @@ MiddlewareRegistry.register(store => next => action => {
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// auto redirect is turned on and we have succesfully logged in
// auto redirect is turned on and we have successfully logged in
// let's mark that
dispatch(setTokenAuthUrlSuccess(true));
}

View File

@@ -430,7 +430,7 @@ function _connectionFailed({ dispatch, getState }: IStore, next: Function, actio
} as INotificationProps;
const { locationURL = { href: '' } as URL } = getState()['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { tenant = '' } = parseURIString(locationURL.href) || {};
if (tenant.startsWith('-') || tenant.endsWith('-')) {
notificationProps.descriptionKey = 'notify.invalidTenantHyphenDescription';

View File

@@ -76,7 +76,6 @@ export default [
'channelLastN',
'connectionIndicators',
'constraints',
'customToolbarButtons',
'brandingRoomAlias',
'debug',
'debugAudioLevels',
@@ -149,7 +148,6 @@ export default [
'enableNoisyMicDetection',
'enableTcc',
'enableAutomaticUrlCopy',
'etherpad_base',
'faceLandmarks',
'failICE',
'feedbackPercentage',
@@ -172,7 +170,6 @@ export default [
'hideAddRoomButton',
'hideEmailInSettings',
'hideLobbyButton',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'ignoreStartMuted',
@@ -236,7 +233,17 @@ export default [
'useTurnUdp',
'videoQuality',
'visitors',
'watchRTCConfigParams',
'watchRTCConfigParams.console',
'watchRTCConfigParams.allowBrowserLogCollection',
'watchRTCConfigParams.collectionInterval',
'watchRTCConfigParams.debug',
'watchRTCConfigParams.keys',
'watchRTCConfigParams.logGetStats',
'watchRTCConfigParams.rtcApiKey',
'watchRTCConfigParams.rtcPeerId',
'watchRTCConfigParams.rtcRoomId',
'watchRTCConfigParams.rtcTags',
'watchRTCConfigParams.rtcToken',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable',
'whiteboard.enabled'

View File

@@ -89,7 +89,7 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
android.downloadLink = android.downloadLink
|| 'https://play.google.com/store/apps/details?id=org.jitsi.meet';
android.appPackage = android.appPackage || 'org.jitsi.meet';
android.fDroidUrl = android.fDroidUrl || 'https://f-droid.org/en/packages/org.jitsi.meet/';
android.fDroidUrl = android.fDroidUrl || 'https://f-droid.org/packages/org.jitsi.meet/';
if (android.dynamicLink) {
android.dynamicLink.apn = android.dynamicLink.apn || 'org.jitsi.meet';
android.dynamicLink.appCode = android.dynamicLink.appCode || 'w2atb';

View File

@@ -1,8 +1,11 @@
/**
* Prefixes of devices that will be filtered from the device list.
*
* NOTE: Currently we filter only 'Microsoft Teams Audio Device' virtual device. It seems that it can't be set
* NOTE: It seems that the filtered devices can't be set
* as default device on the OS level and this use case is not handled in the code. If we add more device prefixes that
* can be default devices we should make sure to handle the default device use case.
*/
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [ 'Microsoft Teams Audio Device' ];
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [
'Microsoft Teams Audio Device',
'ZoomAudioDevice'
];

View File

@@ -96,7 +96,7 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi
const DEFAULT_INITIAL_PROMISE_STATE = createDeferred<IInitialGUMPromiseResult>();
/**
* Reducer fot the common properties in media state.
* Reducer for the common properties in media state.
*
* @param {ICommonState} state - Common media state.
* @param {Object} action - Action object.

View File

@@ -240,7 +240,7 @@ export function participantJoined(participant: IParticipant) {
// conference. The following check is really necessary because a
// JitsiConference may have moved into leaving but may still manage to
// sneak a PARTICIPANT_JOINED in if its leave is delayed for any purpose
// (which is not outragous given that leaving involves network
// (which is not outrageous given that leaving involves network
// requests.)
const stateFeaturesBaseConference
= getState()['features/base/conference'];

View File

@@ -52,7 +52,7 @@ StateListenerRegistry.register(
/**
* Compares the old and new screenshare lists provided and creates/removes the virtual screenshare participant
* tiles accodingly.
* tiles accordingly.
*
* @param {Array<string>} oldScreenshareSourceNames - List of old screenshare source names.
* @param {Array<string>} newScreenshareSourceNames - Current list of screenshare source names.

View File

@@ -227,14 +227,14 @@ class MeetingsList extends Component<IProps> {
<Container
className = { rootClassName }
key = { index }
onClick = { onPress }>
onClick = { onPress }
tabIndex = { 0 }>
<Container className = 'right-column'>
<Text
className = 'title'
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
role = 'button'>
{ title }
</Text>
{

View File

@@ -41,8 +41,8 @@ export function formatURLText(text = '') {
}
if (!result) {
// This will be the case for invalid URLs or URLs without a host (emails for example). In this case beacuse
// of the issue with PunycodeJS that truncates parts of the text when there is '@' we split the text by '@'
// This will be the case for invalid URLs or URLs without a host (emails for example). In this case due to
// the issue with PunycodeJS that truncates parts of the text when there is '@' we split the text by '@'
// and use punycode for every separate part to prevent homograph attacks.
result = text.split('@').map(punycode.toASCII)
.join('@');
@@ -59,7 +59,7 @@ export function formatURLText(text = '') {
*/
export function getSupportUrl(stateful: IStateful) {
// TODO: Once overwriting trough interface config is completelly gone we should think of a way to be able to set
// TODO: Once overwriting through interface config is completely gone we should think of a way to be able to set
// the value in the branding and not return the default value from interface config.
return toState(stateful)['features/dynamic-branding'].supportUrl || interfaceConfig?.SUPPORT_URL;
}

View File

@@ -12,7 +12,7 @@ declare let __DEV__: any;
/**
* Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
* {@code true}), we persist the entire subtree. If it's an {@code Object}, we
* perist a filtered subtree based on the properties of the config object.
* persist a filtered subtree based on the properties of the config object.
*/
declare type ElementConfig = boolean | Object;

View File

@@ -5,7 +5,7 @@ import { setAspectRatio, setReducedUI } from './actions';
/**
* Middleware that handles widnow dimension changes and updates the aspect ratio and
* Middleware that handles window dimension changes and updates the aspect ratio and
* reduced UI modes accordingly.
*
* @param {Store} store - The redux store.

View File

@@ -17,7 +17,7 @@ export * from './functions.any';
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {number|undefined} [options.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}

View File

@@ -31,7 +31,7 @@ export * from './functions.any';
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {number|undefined} [options.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.

View File

@@ -98,6 +98,9 @@ const useStyles = makeStyles()(theme => {
'&.error': {
boxShadow: `0px 0px 0px 2px ${theme.palette.textError}`
},
'&.clearable-input': {
paddingRight: '46px'
}
},
@@ -121,10 +124,6 @@ const useStyles = makeStyles()(theme => {
cursor: 'pointer'
},
clearableInput: {
paddingRight: '46px'
},
clearButton: {
position: 'absolute',
right: '16px',
@@ -185,6 +184,7 @@ const Input = React.forwardRef<any, IProps>(({
}: IProps, ref) => {
const { classes: styles, cx } = useStyles();
const isMobile = isMobileBrowser();
const showClearIcon = clearable && value !== '' && !disabled;
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
onChange?.(e.target.value), []);
@@ -211,7 +211,7 @@ const Input = React.forwardRef<any, IProps>(({
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
error && 'error', showClearIcon && 'clearable-input', icon && 'icon-input') }
disabled = { disabled }
id = { id }
maxLength = { maxLength }
@@ -232,7 +232,7 @@ const Input = React.forwardRef<any, IProps>(({
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
error && 'error', showClearIcon && 'clearable-input', icon && 'icon-input') }
data-testid = { testId }
disabled = { disabled }
id = { id }
@@ -252,7 +252,7 @@ const Input = React.forwardRef<any, IProps>(({
type = { type }
value = { value } />
)}
{clearable && !disabled && value !== '' && <button className = { styles.clearButton }>
{showClearIcon && <button className = { styles.clearButton }>
<Icon
onClick = { clearInput }
size = { 20 }

View File

@@ -19,12 +19,12 @@ interface ISelectProps {
className?: string;
/**
* Wether or not the select is disabled.
* Whether or not the select is disabled.
*/
disabled?: boolean;
/**
* Wether or not the select is in the error state.
* Whether or not the select is in the error state.
*/
error?: boolean;

View File

@@ -13,7 +13,7 @@ import { SECURITY_URL } from './contants';
*
* @param {IReduxState} state - The redux state.
* @param {Function} t - The translation function.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warining.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warning.
* @returns {Text}
*/
export default function getUnsafeRoomText(state: IReduxState, t: Function, context: 'meeting' | 'prejoin' | 'welcome') {

View File

@@ -6,7 +6,7 @@ import { SECURITY_URL } from './contants';
* Gets the unsafe room text for the given context.
*
* @param {Function} t - The translation function.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warining.
* @param {'meeting'|'prejoin'|'welcome'} context - The given context of the warning.
* @returns {string}
*/
export default function getUnsafeRoomText(t: Function, context: 'meeting' | 'prejoin' | 'welcome') {

View File

@@ -18,7 +18,7 @@ import CalendarListContent from './CalendarListContent.native';
import styles from './styles';
/**
* The tyoe of the React {@code Component} props of {@link CalendarList}.
* The type of the React {@code Component} props of {@link CalendarList}.
*/
interface IProps extends WithTranslation {

View File

@@ -152,7 +152,7 @@ export default createStyleSheet({
},
/**
* The container for all the lines if the norification.
* The container for all the lines if the notification.
*/
notificationTextContainer: {
flexDirection: 'column',

View File

@@ -1,5 +1,5 @@
import { Theme } from '@mui/material';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -264,7 +264,7 @@ const ChatMessage = ({
*
* @returns {React$Element<*>}
*/
const renderReactions = () => {
const renderReactions = useMemo(() => {
if (!message.reactions || message.reactions.size === 0) {
return null;
}
@@ -321,7 +321,7 @@ const ChatMessage = ({
</div>
</Popover>
);
};
}, [ message?.reactions, isHovered, isReactionsOpen ]);
return (
<div
@@ -366,7 +366,7 @@ const ChatMessage = ({
<div className = { classes.chatMessageFooterLeft }>
{message.reactions && message.reactions.size > 0 && (
<>
{renderReactions()}
{renderReactions}
</>
)}
</div>

View File

@@ -11,7 +11,7 @@ import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
*
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
* instance which initiated the kick event.
* @param {?Function} submit - The function to execute after submiting the dialog.
* @param {?Function} submit - The function to execute after submitting the dialog.
* @returns {Function}
*/
export function notifyKickedOut(participant: any, submit?: Function) {
@@ -38,7 +38,7 @@ export function notifyKickedOut(participant: any, submit?: Function) {
* Notify that we've been kicked out of the conference.
*
* @param {string} reasonKey - The translation key for the reason why the conference failed.
* @param {?Function} submit - The function to execute after submiting the dialog.
* @param {?Function} submit - The function to execute after submitting the dialog.
* @returns {Function}
*/
export function notifyConferenceFailed(reasonKey: string, submit?: Function) {

View File

@@ -99,7 +99,7 @@ interface IProps extends AbstractDialogTabProps, WithTranslation {
iAmVisitor: boolean;
/**
* Wether noise suppression is on or not.
* Whether noise suppression is on or not.
*/
noiseSuppressionEnabled: boolean;

View File

@@ -50,7 +50,7 @@ export function setMediaEncryptionKey(keyInfo: Object) {
}
/**
* Dispatches an action to start participant e2ee verficiation process.
* Dispatches an action to start participant e2ee verification process.
*
* @param {string} pId - The participant id.
* @returns {{
@@ -69,7 +69,7 @@ export function startVerification(pId: string) {
* Dispatches an action to set participant e2ee verification status.
*
* @param {string} pId - The participant id.
* @param {boolean} isVerified - The verifcation status.
* @param {boolean} isVerified - The verification status.
* @returns {{
* type: PARTICIPANT_VERIFIED,
* pId: string,

View File

@@ -57,7 +57,7 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
remoteParticipants.delete(speaker);
}
// Always update the order of the thumnails.
// Always update the order of the thubmnails.
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);

View File

@@ -254,7 +254,7 @@ export function getFilmstripDimensions({
/**
* Returns true if the local thumbnail should be displayed separately and false otherwise.
*
* @returns {boolean} - True if the local thumbnail should be displayed separately and flase otherwise.
* @returns {boolean} - True if the local thumbnail should be displayed separately and false otherwise.
*/
export function shouldDisplayLocalThumbnailSeparately() {
// XXX Our current design is to have the local participant separate from

View File

@@ -322,7 +322,7 @@ export function calculateResponsiveTileViewDimensions({
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants, desiredNumberOfVisibleTiles); c++) {
const r = Math.ceil(numberOfParticipants / c);
// we want to display as much as possible tumbnails up to desiredNumberOfVisibleTiles
// we want to display as much as possible thumbnails up to desiredNumberOfVisibleTiles
const visibleRows
= numberOfParticipants <= desiredNumberOfVisibleTiles ? r : Math.floor(desiredNumberOfVisibleTiles / c);

View File

@@ -84,7 +84,7 @@ export function updateKnownLargeVideoResolution(resolution: number) {
}
/**
* Sets the dimenstions of the large video in redux.
* Sets the dimensions of the large video in redux.
*
* @param {number} height - The height of the large video.
* @param {number} width - The width of the large video.

View File

@@ -9,7 +9,7 @@ import { isLocalVideoTrackDesktop } from '../../base/tracks/functions.any';
* Checks whether full-screen state should be used or not.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Whether full-screen state shuld be used or not.
* @returns {boolean} - Whether full-screen state should be used or not.
*/
export function shouldUseFullScreen(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];

View File

@@ -350,6 +350,7 @@ const Notification = ({
onClick = { onDismiss }
size = { 20 }
src = { IconCloseLarge }
tabIndex = { 0 }
testId = { `${titleKey || descriptionKey}-dismiss` } />
)}
</div>

View File

@@ -286,7 +286,7 @@ export const isMuteAllVisible = (state: IReduxState) => {
* Returns true if renaming the currently joined breakout room is allowed and false otherwise.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - True if reanming the currently joined breakout room is allowed and false otherwise.
* @returns {boolean} - True if renaming the currently joined breakout room is allowed and false otherwise.
*/
export function isCurrentRoomRenamable(state: IReduxState) {
return isInBreakoutRoom(state) && isBreakoutRoomRenameAllowed(state);

View File

@@ -31,7 +31,7 @@ StateListenerRegistry.register(
}
});
const parsePollData = (pollData: IPollData): IPoll | null => {
const parsePollData = (pollData: Partial<IPollData>): IPoll | null => {
if (typeof pollData !== 'object' || pollData === null) {
return null;
}
@@ -122,6 +122,18 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
case COMMAND_NEW_POLL: {
const { pollId, answers, senderId, question } = data;
const tmp = {
id: pollId,
answers,
question
};
// Check integrity of the poll data.
// TODO(saghul): we should move this to the server side, likely by storing the
// poll data in the room metadata.
if (parsePollData(tmp) === null) {
return;
}
const poll = {
changingVote: false,

View File

@@ -117,7 +117,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
return () => BackHandler.removeEventListener('hardwareBackPress', goBack);
}, []); // dispatch is not in the dependancy list because we want the action to be dispatched only once when
}, []); // dispatch is not in the dependency list because we want the action to be dispatched only once when
// the component is mounted.
const headerLeft = () => {

View File

@@ -216,11 +216,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
if (updatedSessionData?.status === ON) {
// We receive 2 updates of the session status ON. The first one is from jibri when it joins.
// The second one is from jicofo which will deliever the initiator value. Since the start
// The second one is from jicofo which will deliver the initiator value. Since the start
// recording notification uses the initiator value we skip the jibri update and show the
// notification on the update from jicofo.
// FIXE: simplify checks when the backend start sending only one status ON update containing the
// initiator.
// FIXME: simplify checks when the backend start sending only one status ON update containing
// the initiator.
if (initiator && !oldSessionData?.initiator) {
if (typeof recordingLimit === 'object') {
dispatch(showRecordingLimitNotification(mode));

View File

@@ -113,7 +113,7 @@ ReducerRegistry.register<IRecordingState>(STORE_NAME,
* @param {Array} sessionDatas - The current sessions in the redux store.
* @param {Object} newSessionData - The updated session data.
* @private
* @returns {Array} The session datas with the updated session data added.
* @returns {Array} The session data with the updated session data added.
*/
function _updateSessionDatas(sessionDatas: ISessionData[], newSessionData: ISessionData) {
const hasExistingSessionData = sessionDatas.find(
@@ -133,7 +133,7 @@ function _updateSessionDatas(sessionDatas: ISessionData[], newSessionData: ISess
});
} else {
// If the session data is not present, then there is nothing to update
// and instead it needs to be added to the known session datas.
// and instead it needs to be added to the known session data.
newSessionDatas = [
...sessionDatas,
{ ...newSessionData }

View File

@@ -107,7 +107,7 @@ export function requestRemoteControl(userId: string) {
dispatch(setRemoteControlActive(true));
logger.log(`Requsting remote control permissions from: ${userId}`);
logger.log(`Requesting remote control permissions from: ${userId}`);
const { conference } = state['features/base/conference'];

View File

@@ -79,7 +79,7 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
showPrejoinSettings: boolean;
/**
* Wether or not the stage filmstrip is enabled.
* Whether or not the stage filmstrip is enabled.
*/
stageFilmstripEnabled: boolean;
}

View File

@@ -25,7 +25,7 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
displayShortcuts: boolean;
/**
* Wether the keyboard shortcuts are enabled or not.
* Whether the keyboard shortcuts are enabled or not.
*/
keyboardShortcutsEnabled: boolean;

View File

@@ -28,17 +28,6 @@ export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
*/
export const SET_CONFIRM_SHOW_VIDEO = 'SET_CONFIRM_SHOW_VIDEO';
/**
* The type of the action which signals to disable or enable the shared video
* button.
*
* {
* type: SET_DISABLE_BUTTON
* }
*/
export const SET_DISABLE_BUTTON = 'SET_DISABLE_BUTTON';
/**
* The type of the action which sets an array of whitelisted urls.
*

View File

@@ -1 +0,0 @@
export * from './actions.any';

View File

@@ -1,19 +0,0 @@
import { SET_DISABLE_BUTTON } from './actionTypes';
export * from './actions.any';
/**
* Disabled share video button.
*
* @param {boolean} disabled - The current state of the share video button.
* @returns {{
* type: SET_DISABLE_BUTTON,
* disabled: boolean
* }}
*/
export function setDisableButton(disabled: boolean) {
return {
type: SET_DISABLE_BUTTON,
disabled
};
}

View File

@@ -5,7 +5,7 @@ import { IReduxState, IStore } from '../../../app/types';
import { getCurrentConference } from '../../../base/conference/functions';
import { IJitsiConference } from '../../../base/conference/reducer';
import { getLocalParticipant } from '../../../base/participants/functions';
import { setSharedVideoStatus } from '../../actions.any';
import { setSharedVideoStatus } from '../../actions';
import { PLAYBACK_STATUSES } from '../../constants';
/**

View File

@@ -7,7 +7,7 @@ import { translate } from '../../../base/i18n/functions';
import { IconPlay } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleSharedVideo } from '../../actions.native';
import { toggleSharedVideo } from '../../actions';
import { isSharingStatus } from '../../functions';
/**

View File

@@ -15,7 +15,7 @@ import { showWarningNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
import { dockToolbox } from '../../../toolbox/actions';
import { muteLocal } from '../../../video-menu/actions.any';
import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
import { setSharedVideoStatus, stopSharedVideo } from '../../actions';
import { PLAYBACK_STATUSES } from '../../constants';
const logger = Logger.getLogger(__filename);

View File

@@ -3,8 +3,9 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconPlay } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleSharedVideo } from '../../actions.any';
import { toggleSharedVideo } from '../../actions';
import { isSharingStatus } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -83,16 +84,14 @@ class SharedVideoButton extends AbstractButton<IProps> {
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const {
disabled: sharedVideoBtnDisabled,
status: sharedVideoStatus
} = state['features/shared-video'];
const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const isSharing = isSharingStatus(sharedVideoStatus ?? '');
return {
_isDisabled: Boolean(sharedVideoBtnDisabled),
_sharingVideo: isSharingStatus(sharedVideoStatus ?? '')
_isDisabled: isSharing && ownerId !== localParticipantId,
_sharingVideo: isSharing
};
}
export default translate(connect(_mapStateToProps)(SharedVideoButton));

View File

@@ -1 +0,0 @@
import './middleware.any';

View File

@@ -22,7 +22,7 @@ import {
setAllowedUrlDomians,
setSharedVideoStatus,
showConfirmPlayingDialog
} from './actions.any';
} from './actions';
import {
DEFAULT_ALLOWED_URL_DOMAINS,
PLAYBACK_START,
@@ -55,27 +55,41 @@ MiddlewareRegistry.register(store => next => action => {
conference.addCommandListener(SHARED_VIDEO,
({ value, attributes }: { attributes: {
from: string; muted: string; state: string; time: string; }; value: string; }) => {
muted: string; state: string; time: string; }; value: string; },
from: string) => {
const state = getState();
const { from } = attributes;
const sharedVideoStatus = attributes.state;
const { ownerId } = state['features/shared-video'];
if (ownerId && ownerId !== from) {
logger.warn(
`User with id: ${from} sent shared video command: ${sharedVideoStatus} while we are playing.`);
return;
}
if (isSharingStatus(sharedVideoStatus)) {
// confirmShowVideo is undefined the first time we receive
// when confirmShowVideo is false we ignore everything except stop that resets it
if (getState()['features/shared-video'].confirmShowVideo === false) {
if (state['features/shared-video'].confirmShowVideo === false) {
return;
}
if (isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true)
if (isURLAllowedForSharedVideo(value, state['features/shared-video'].allowedUrlDomains, true)
|| localParticipantId === from
|| getState()['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
handleSharingVideoStatus(store, value, attributes, conference);
|| state['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
handleSharingVideoStatus(store, value, {
...attributes,
from
}, conference);
} else {
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(getState(), from), () => {
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(state, from), () => {
handleSharingVideoStatus(store, value, attributes, conference);
handleSharingVideoStatus(store, value, {
...attributes,
from
}, conference);
return true; // on mobile this is used to close the dialog
}));
@@ -87,11 +101,11 @@ MiddlewareRegistry.register(store => next => action => {
if (sharedVideoStatus === 'stop') {
const videoParticipant = getParticipantById(state, value);
if (getState()['features/shared-video'].confirmShowVideo === false) {
if (state['features/shared-video'].confirmShowVideo === false) {
dispatch(showWarningNotification({
titleKey: 'dialog.shareVideoLinkStopped',
titleArguments: {
name: getParticipantDisplayName(getState(), from)
name: getParticipantDisplayName(state, from)
}
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}

View File

@@ -1,41 +0,0 @@
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import { getLocalParticipant } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { setDisableButton } from './actions.web';
import { PLAYBACK_STATUSES, SHARED_VIDEO } from './constants';
import { isSharedVideoEnabled } from './functions';
import './middleware.any';
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const state = getState();
const localParticipantId = getLocalParticipant(state)?.id;
switch (action.type) {
case CONFERENCE_JOIN_IN_PROGRESS: {
if (!isSharedVideoEnabled(state)) {
break;
}
const { conference } = action;
conference.addCommandListener(SHARED_VIDEO, ({ attributes }: { attributes:
{ from: string; state: string; }; }) => {
const { from } = attributes;
const status = attributes.state;
if (status === PLAYBACK_STATUSES.PLAYING) {
if (localParticipantId !== from) {
dispatch(setDisableButton(true));
}
} else if (status === 'stop') {
dispatch(setDisableButton(false));
}
});
break;
}
}
return next(action);
});

View File

@@ -4,7 +4,6 @@ import {
RESET_SHARED_VIDEO_STATUS,
SET_ALLOWED_URL_DOMAINS,
SET_CONFIRM_SHOW_VIDEO,
SET_DISABLE_BUTTON,
SET_SHARED_VIDEO_STATUS
} from './actionTypes';
import { DEFAULT_ALLOWED_URL_DOMAINS } from './constants';
@@ -16,7 +15,6 @@ const initialState = {
export interface ISharedVideoState {
allowedUrlDomains: Array<string>;
confirmShowVideo?: boolean;
disabled?: boolean;
muted?: boolean;
ownerId?: string;
status?: string;
@@ -30,7 +28,7 @@ export interface ISharedVideoState {
*/
ReducerRegistry.register<ISharedVideoState>('features/shared-video',
(state = initialState, action): ISharedVideoState => {
const { videoUrl, status, time, ownerId, disabled, muted, volume } = action;
const { videoUrl, status, time, ownerId, muted, volume } = action;
switch (action.type) {
case RESET_SHARED_VIDEO_STATUS:
@@ -55,12 +53,6 @@ ReducerRegistry.register<ISharedVideoState>('features/shared-video',
volume
};
case SET_DISABLE_BUTTON:
return {
...state,
disabled
};
case SET_ALLOWED_URL_DOMAINS: {
return {
...state,

View File

@@ -202,7 +202,7 @@ export function getVisibleButtons({
setButtonsNotifyClickMode(allButtons, buttonsWithNotifyClick);
const filteredButtons = Object.keys(allButtons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be comming from config.mainToolbarButtons
typeof key !== 'undefined' // filter invalid buttons that may be coming from config.mainToolbarButtons
// override
&& !jwtDisabledButtons.includes(key)
&& isButtonEnabled(key, toolbarButtons));
@@ -264,7 +264,7 @@ interface ICSSTransitionObject {
* @returns {ICSSTransitionObject}
*/
export function getTransitionParamsForElementsAboveToolbox(isToolbarVisible: boolean): ICSSTransitionObject {
// The transistion time and delay is different to account for the time when the toolbar is about to hide/show but
// The transition time and delay is different to account for the time when the toolbar is about to hide/show but
// the elements don't have to move.
return isToolbarVisible ? {
duration: 0.15,

View File

@@ -142,7 +142,7 @@ function _setFullScreen(next: Function, action: AnyAction) {
/**
* Common logic to gather buttons that have to notify the api when clicked.
*
* @param {Array} buttonsWithNotifyClick - The array of systme buttons that need to notify the api.
* @param {Array} buttonsWithNotifyClick - The array of system buttons that need to notify the api.
* @param {Array} customButtons - The custom buttons.
* @returns {Array}
*/

View File

@@ -2,7 +2,7 @@ import { IReduxState } from '../../app/types';
import { requestDisableAudioModeration, requestEnableAudioModeration } from '../../av-moderation/actions';
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants/functions';
import { getLocalParticipant, getParticipantDisplayName, isEveryoneModerator } from '../../base/participants/functions';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantDialog, {
@@ -17,6 +17,7 @@ export interface IProps extends AbstractProps {
content?: string;
exclude: Array<string>;
isAudioModerationEnabled?: boolean;
isEveryoneModerator: boolean;
isModerationSupported?: boolean;
showAdvancedModerationToggle: boolean;
title: string;
@@ -119,10 +120,12 @@ export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
title: t('dialog.muteEveryoneElseTitle', { whom }),
isEveryoneModerator: isEveryoneModerator(state)
} : {
title: t('dialog.muteEveryoneTitle'),
isAudioModerationEnabled: isEnabledFromState(MEDIA_TYPE.AUDIO, state),
isModerationSupported: isSupported()(state)
isModerationSupported: isSupported()(state),
isEveryoneModerator: isEveryoneModerator(state)
};
}

View File

@@ -9,7 +9,7 @@ import { isWhiteboardParticipant } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { stopSharedVideo } from '../../../shared-video/actions.any';
import { stopSharedVideo } from '../../../shared-video/actions';
import { getParticipantMenuButtonsWithNotifyClick, showOverflowDrawer } from '../../../toolbox/functions.web';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
import { setWhiteboardOpen } from '../../../whiteboard/actions';

View File

@@ -28,21 +28,24 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<IProps> {
onSubmit = { this._onSubmit }
title = { this.props.title }>
<div className = 'mute-dialog'>
{ this.state.content }
{ this.props.isModerationSupported && this.props.exclude.length === 0 && (
<>
<div className = 'separator-line' />
<div className = 'control-row'>
<label htmlFor = 'moderation-switch'>
{this.props.t('dialog.moderationAudioLabel')}
</label>
<Switch
checked = { !this.state.audioModerationEnabled }
id = 'moderation-switch'
onChange = { this._onToggleModeration } />
</div>
</>
)}
{this.state.content}
{
this.props.isModerationSupported
&& this.props.exclude.length === 0
&& !this.props.isEveryoneModerator && (
<>
<div className = 'separator-line' />
<div className = 'control-row'>
<label htmlFor = 'moderation-switch'>
{this.props.t('dialog.moderationAudioLabel')}
</label>
<Switch
checked = { !this.state.audioModerationEnabled }
id = 'moderation-switch'
onChange = { this._onToggleModeration } />
</div>
</>
)}
</div>
</Dialog>
);

View File

@@ -29,7 +29,7 @@ export function setMaxReceiverVideoQualityForLargeVideo(maxReceiverVideoQuality:
}
/**
* Sets the max frame height that should be received for the screen sharing filmstrip particpant.
* Sets the max frame height that should be received for the screen sharing filmstrip participant.
*
* @param {number} maxReceiverVideoQuality - The max video frame height to
* receive.

View File

@@ -309,7 +309,7 @@ StateListenerRegistry.register(
});
/**
* Returns the source names asociated with the given participants list.
* Returns the source names associated with the given participants list.
*
* @param {Array<string>} participantList - The list of participants.
* @param {Object} state - The redux state.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -41,7 +41,7 @@ module:hook("pre-iq/full", function(event)
session.granted_jitsi_meet_context_features,
occupant.role == 'moderator');
if jibri.attr.action == 'start' then
if jibri.attr.action == 'start' or jibri.attr.action == 'stop' then
if not is_allowed then
module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza));
session.send(st.error_reply(stanza, 'auth', 'forbidden'));

View File

@@ -80,7 +80,7 @@ function Util.new(module)
These setups relay on configuration 'muc_domain_base' which holds
the main domain and we use it to subtract subdomains from the
virtual addresses.
The following confgurations are for multidomain setups and domain name
The following configurations are for multidomain setups and domain name
verification:
--]]
@@ -225,7 +225,7 @@ function Util:get_public_key(keyId)
self.cache:set(keyId, content);
else
if code == nil then
-- this is timout after nr_retries retries
-- this is timeout after nr_retries retries
module:log('warn', 'Timeout retrieving %s from %s', keyId, keyurl);
end
end

View File

@@ -1,11 +1,15 @@
/* global APP $ */
import { multiremotebrowser } from '@wdio/globals';
import { Key } from 'webdriverio';
import { IConfig } from '../../react/features/base/config/configType';
import { urlObjectToString } from '../../react/features/base/util/uri';
import BreakoutRooms from '../pageobjects/BreakoutRooms';
import ChatPanel from '../pageobjects/ChatPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import InviteDialog from '../pageobjects/InviteDialog';
import Notifications from '../pageobjects/Notifications';
import ParticipantsPane from '../pageobjects/ParticipantsPane';
import SettingsDialog from '../pageobjects/SettingsDialog';
@@ -120,7 +124,10 @@ export class Participant {
async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
const config = {
room: ctx.roomName,
configOverwrite: this.config,
configOverwrite: {
...this.config,
...options.configOverwrite || {}
},
interfaceConfigOverwrite: {
SHOW_CHROME_EXTENSION_BANNER: false
}
@@ -129,7 +136,7 @@ export class Participant {
if (!options.skipDisplayName) {
// @ts-ignore
config.userInfo = {
displayName: this._name
displayName: options.displayName || this._name
};
}
@@ -251,6 +258,22 @@ export class Participant {
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
}
/**
* Checks if the meeting supports breakout rooms.
*/
async isBreakoutRoomsSupported() {
return await this.driver.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
}
/**
* Checks if the participant is in breakout room.
*/
async isInBreakoutRoom() {
return await this.driver.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
}
/**
* Waits to join the muc.
*
@@ -286,7 +309,7 @@ export class Participant {
*
* @returns {Promise<void>}
*/
async waitForSendReceiveData(): Promise<void> {
async waitForSendReceiveData(timeout = 15_000, msg = 'expected to receive/send data in 15s'): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
@@ -300,15 +323,15 @@ export class Participant {
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
}), {
timeout: 15_000,
timeoutMsg: 'expected to receive/send data in 15s'
timeout,
timeoutMsg: msg
});
}
/**
* Waits for remote streams.
*
* @param {number} number - The number of remote streams o wait for.
* @param {number} number - The number of remote streams to wait for.
* @returns {Promise<void>}
*/
waitForRemoteStreams(number: number): Promise<void> {
@@ -321,6 +344,39 @@ export class Participant {
});
}
/**
* Waits for number of participants.
*
* @param {number} number - The number of participant to wait for.
* @param {string} msg - A custom message to use.
* @returns {Promise<void>}
*/
waitForParticipants(number: number, msg?: string): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
await driver.execute(count => APP.conference.listMembers().length === count, number), {
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s`
});
}
/**
* Returns the chat panel for this participant.
*/
getChatPanel(): ChatPanel {
return new ChatPanel(this);
}
/**
* Returns the BreakoutRooms for this participant.
*
* @returns {BreakoutRooms}
*/
getBreakoutRooms(): BreakoutRooms {
return new BreakoutRooms(this);
}
/**
* Returns the toolbar for this participant.
*
@@ -339,6 +395,15 @@ export class Participant {
return new Filmstrip(this);
}
/**
* Returns the invite dialog for this participant.
*
* @returns {InviteDialog}
*/
getInviteDialog(): InviteDialog {
return new InviteDialog(this);
}
/**
* Returns the notifications.
*/
@@ -404,16 +469,40 @@ export class Participant {
}
/**
* Returns the local display name.
* Returns the local display name element.
* @private
*/
async getLocalDisplayName() {
private async getLocalDisplayNameElement() {
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
await localVideoContainer.moveTo();
const localDisplayName = localVideoContainer.$('span[id="localDisplayName"]');
return localVideoContainer.$('span[id="localDisplayName"]');
}
return await localDisplayName.getText();
/**
* Returns the local display name.
*/
async getLocalDisplayName() {
return await (await this.getLocalDisplayNameElement()).getText();
}
/**
* Sets the display name of the local participant.
*/
async setLocalDisplayName(displayName: string) {
const localDisplayName = await this.getLocalDisplayNameElement();
await localDisplayName.click();
await this.driver.keys(displayName);
await this.driver.keys(Key.Return);
// just click somewhere to lose focus, to make sure editing has ended
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
await localVideoContainer.moveTo();
await localVideoContainer.click();
}
/**

View File

@@ -6,6 +6,8 @@ import { v4 as uuidv4 } from 'uuid';
import { Participant } from './Participant';
import { IContext, IJoinOptions } from './types';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
/**
* Ensure that there is on participant.
*
@@ -14,38 +16,81 @@ import { IContext, IJoinOptions } from './types';
* @returns {Promise<void>}
*/
export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
ctx.p1 = new Participant('participant1');
await ctx.p1.joinConference(ctx, {
...options,
skipInMeetingChecks: true
});
await joinTheModeratorAsP1(ctx, options);
}
/**
* Ensure that there are three participants.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
await joinTheModeratorAsP1(ctx);
const p2 = new Participant('participant2');
const p3 = new Participant('participant3');
ctx.p2 = p2;
ctx.p3 = p3;
export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
await joinTheModeratorAsP1(ctx, options);
// these need to be all, so we get the error when one fails
await Promise.all([
p2.joinConference(ctx),
p3.joinConference(ctx)
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: 'p3',
...options
})
]);
const { skipInMeetingChecks } = options;
await Promise.all([
p2.waitForRemoteStreams(2),
p3.waitForRemoteStreams(2)
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(2),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(2)
]);
}
/**
* Ensure that there are four participants.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export async function ensureFourParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
await joinTheModeratorAsP1(ctx, options);
// these need to be all, so we get the error when one fails
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: 'p3',
...options
}),
_joinParticipant('participant4', ctx.p4, p => {
ctx.p4 = p;
}, {
displayName: 'p4',
...options
})
]);
const { skipInMeetingChecks } = options;
await Promise.all([
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(3),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3)
]);
}
@@ -57,7 +102,7 @@ export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
* @returns {Promise<void>}
*/
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
const p1DisplayName = 'participant1';
const p1DisplayName = 'p1';
let token;
// if it is jaas create the first one to be moderator and second not moderator
@@ -66,9 +111,10 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
}
// make sure the first participant is moderator, if supported by deployment
await _joinParticipant(p1DisplayName, ctx.p1, p => {
await _joinParticipant('participant1', ctx.p1, p => {
ctx.p1 = p;
}, {
displayName: p1DisplayName,
...options,
skipInMeetingChecks: true
}, token);
@@ -88,7 +134,10 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, options),
}, {
displayName: 'p2',
...options
}),
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
]);
@@ -237,3 +286,19 @@ export function parseJid(str: string): {
resource: domainParts.length > 0 ? domainParts[1] : undefined
};
}
/**
* Check the subject of the participant.
* @param participant
* @param subject
*/
export async function checkSubject(participant: Participant, subject: string) {
const localTile = participant.driver.$(SUBJECT_XPATH);
await localTile.waitForExist();
await localTile.moveTo();
const txt = await localTile.getText();
expect(txt.startsWith(subject)).toBe(true);
}

View File

@@ -1,8 +1,11 @@
import { IConfig } from '../../react/features/base/config/configType';
import type { Participant } from './Participant';
import WebhookProxy from './WebhookProxy';
export type IContext = {
conferenceJid: string;
dialInPin: string;
iframeAPI: boolean;
jwtKid: string;
jwtPrivateKeyPath: string;
@@ -12,11 +15,22 @@ export type IContext = {
p4: Participant;
roomName: string;
skipSuiteTests: boolean;
times: any;
webhooksProxy: WebhookProxy;
};
export type IJoinOptions = {
/**
* Config overwrites to use.
*/
configOverwrite?: IConfig;
/**
* The display name to use.
*/
displayName?: string;
/**
* Whether to skip setting display name.
*/

View File

@@ -1,4 +1,4 @@
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const START_AUDIO_MODERATION = 'participants-pane-context-menu-start-audio-moderation';
const STOP_AUDIO_MODERATION = 'participants-pane-context-menu-stop-audio-moderation';
@@ -8,17 +8,7 @@ const STOP_VIDEO_MODERATION = 'participants-pane-context-menu-stop-video-moderat
/**
* Represents the Audio Video Moderation menu in the participants pane.
*/
export default class AVModerationMenu {
private participant: Participant;
/**
* Represents the Audio Video Moderation menu in the participants pane.
* @param participant
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class AVModerationMenu extends BasePageObject {
/**
* Clicks the start audio moderation menu item.
*/

View File

@@ -1,4 +1,4 @@
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const CLOSE_BUTTON = 'modal-header-close-button';
const OK_BUTTON = 'modal-dialog-ok-button';
@@ -6,18 +6,7 @@ const OK_BUTTON = 'modal-dialog-ok-button';
/**
* Base class for all dialogs.
*/
export default class BaseDialog {
participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class BaseDialog extends BasePageObject {
/**
* Clicks on the X (close) button.
*/

View File

@@ -0,0 +1,16 @@
import { Participant } from '../helpers/Participant';
/**
* Represents the base page object.
* All page object has the current participant (holding the driver/browser session).
*/
export default class BasePageObject {
participant: Participant;
/**
* Represents the base page object.
*/
constructor(participant: Participant) {
this.participant = participant;
}
}

View File

@@ -0,0 +1,230 @@
import { Participant } from '../helpers/Participant';
import BaseDialog from './BaseDialog';
import BasePageObject from './BasePageObject';
const BREAKOUT_ROOMS_CLASS = 'breakout-room-container';
const ADD_BREAKOUT_ROOM = 'Add breakout room';
const MORE_LABEL = 'More';
const LEAVE_ROOM_LABEL = 'Leave breakout room';
const AUTO_ASSIGN_LABEL = 'Auto assign to breakout rooms';
/**
* Represents a single breakout room and the operations for it.
*/
class BreakoutRoom extends BasePageObject {
title: string;
id: string;
count: number;
/**
* Constructs a breakout room.
*/
constructor(participant: Participant, title: string, id: string) {
super(participant);
this.title = title;
this.id = id;
const tMatch = title.match(/.*\((.*)\)/);
if (tMatch) {
this.count = parseInt(tMatch[1], 10);
}
}
/**
* Returns room name.
*/
get name() {
return this.title.split('(')[0].trim();
}
/**
* Returns the number of participants in the room.
*/
get participantCount() {
return this.count;
}
/**
* Collapses the breakout room.
*/
async collapse() {
const collapseElem = this.participant.driver.$(
`div[data-testid="${this.id}"]`);
await collapseElem.click();
}
/**
* Joins the breakout room.
*/
async joinRoom() {
const joinButton = this.participant.driver
.$(`button[data-testid="join-room-${this.id}"]`);
await joinButton.waitForClickable();
await joinButton.click();
}
/**
* Removes the breakout room.
*/
async removeRoom() {
await this.openContextMenu();
const removeButton = this.participant.driver.$(`#remove-room-${this.id}`);
await removeButton.waitForClickable();
await removeButton.click();
}
/**
* Renames the breakout room.
*/
async renameRoom(newName: string) {
await this.openContextMenu();
const renameButton = this.participant.driver.$(`#rename-room-${this.id}`);
await renameButton.click();
const newNameInput = this.participant.driver.$('input[name="breakoutRoomName"]');
await newNameInput.waitForStable();
await newNameInput.setValue(newName);
await new BaseDialog(this.participant).clickOkButton();
}
/**
* Closes the breakout room.
*/
async closeRoom() {
await this.openContextMenu();
const closeButton = this.participant.driver.$(`#close-room-${this.id}`);
await closeButton.waitForClickable();
await closeButton.click();
}
/**
* Opens the context menu.
* @private
*/
private async openContextMenu() {
const listItem = this.participant.driver.$(`div[data-testid="${this.id}"]`);
await listItem.click();
const button = listItem.$(`aria/${MORE_LABEL}`);
await button.waitForClickable();
await button.click();
}
}
/**
* All breakout rooms objects and operations.
*/
export default class BreakoutRooms extends BasePageObject {
/**
* Returns the number of breakout rooms.
*/
async getRoomsCount() {
const participantsPane = this.participant.getParticipantsPane();
if (!await participantsPane.isOpen()) {
await participantsPane.open();
}
return await this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`).length;
}
/**
* Adds a breakout room.
*/
async addBreakoutRoom() {
const participantsPane = this.participant.getParticipantsPane();
if (!await participantsPane.isOpen()) {
await participantsPane.open();
}
const addBreakoutButton = this.participant.driver.$(`aria/${ADD_BREAKOUT_ROOM}`);
await addBreakoutButton.waitForDisplayed();
await addBreakoutButton.click();
}
/**
* Returns all breakout rooms.
*/
async getRooms(): Promise<BreakoutRoom[]> {
const rooms = this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`);
return rooms.map(async room => new BreakoutRoom(
this.participant, await room.$('span').getText(), await room.getAttribute('data-testid')));
}
/**
* Leave by clicking the leave button in participant pane.
*/
async leaveBreakoutRoom() {
const participantsPane = this.participant.getParticipantsPane();
if (!await participantsPane.isOpen()) {
await participantsPane.open();
}
const leaveButton = this.participant.driver.$(`aria/${LEAVE_ROOM_LABEL}`);
await leaveButton.isClickable();
await leaveButton.click();
}
/**
* Auto assign participants to breakout rooms.
*/
async autoAssignToBreakoutRooms() {
const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
await button.waitForClickable();
await button.click();
}
/**
* Tries to send a participant to a breakout room.
*/
async sendParticipantToBreakoutRoom(participant: Participant, roomName: string) {
const participantsPane = this.participant.getParticipantsPane();
await participantsPane.selectParticipant(participant);
await participantsPane.openParticipantContextMenu(participant);
const sendButton = this.participant.driver.$(`aria/${roomName}`);
await sendButton.waitForClickable();
await sendButton.click();
}
// /**
// * Open context menu for given participant.
// */
// async openParticipantContextMenu(participant: Participant) {
// const listItem = this.participant.driver.$(
// `div[@id="participant-item-${await participant.getEndpointId()}"]`);
//
// await listItem.waitForDisplayed();
// await listItem.moveTo();
//
// const button = listItem.$(`aria/${PARTICIPANT_MORE_LABEL}`);
//
// await button.waitForClickable();
// await button.click();
// }
}

View File

@@ -0,0 +1,22 @@
import BasePageObject from './BasePageObject';
/**
* Chat panel elements.
*/
export default class ChatPanel extends BasePageObject {
/**
* Is chat panel open.
*/
async isOpen() {
return await this.participant.driver.$('#sideToolbarContainer').isExisting();
}
/**
* Presses the "chat" keyboard shortcut which opens or closes the chat
* panel.
*/
async pressShortcut() {
await this.participant.driver.$('body').click();
await this.participant.driver.keys([ 'c' ]);
}
}

View File

@@ -1,22 +1,17 @@
import { Participant } from '../helpers/Participant';
import BaseDialog from './BaseDialog';
import BasePageObject from './BasePageObject';
const LOCAL_VIDEO_XPATH = '//span[@id="localVideoContainer"]';
const LOCAL_VIDEO_MENU_TRIGGER = '#local-video-menu-trigger';
const LOCAL_USER_CONTROLS = 'aria/Local user controls';
const HIDE_SELF_VIEW_BUTTON_XPATH = '//div[contains(@class, "popover")]//div[@id="hideselfviewButton"]';
/**
* Filmstrip elements.
*/
export default class Filmstrip {
private participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class Filmstrip extends BasePageObject {
/**
* Asserts that {@code participant} shows or doesn't show the audio
* mute icon for the conference participant identified by
@@ -104,6 +99,7 @@ export default class Filmstrip {
const popoverElement = this.participant.driver.$(
`//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
await popoverElement.waitForExist();
await popoverElement.waitForDisplayed();
await popoverElement.click();
@@ -131,4 +127,39 @@ export default class Filmstrip {
async muteVideo(participant: Participant) {
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
}
/**
* Kicks a participant.
* @param participantId
*/
async kickParticipant(participantId: string) {
await this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
}
/**
* Clicks on the hide self view button from local video.
*/
async hideSelfView() {
// open local video menu
await this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
await this.participant.driver.$(LOCAL_USER_CONTROLS).moveTo();
// click Hide self view button
const hideSelfViewButton = this.participant.driver.$(HIDE_SELF_VIEW_BUTTON_XPATH);
await hideSelfViewButton.waitForExist();
await hideSelfViewButton.waitForClickable();
await hideSelfViewButton.click();
}
/**
* Checks whether the local self view is displayed or not.
*/
async assertSelfViewIsHidden(hidden: boolean) {
await this.participant.driver.$(LOCAL_VIDEO_XPATH).waitForDisplayed({
reverse: hidden,
timeout: 5000,
timeoutMsg: `Local video thumbnail is${hidden ? '' : ' not'} displayed for ${this.participant.name}`
});
}
}

View File

@@ -1,20 +1,11 @@
import { Participant } from '../helpers/Participant';
import { LOG_PREFIX } from '../helpers/browserLogger';
import BasePageObject from './BasePageObject';
/**
* The Iframe API and helpers from iframeAPITest.html
*/
export default class IframeAPI {
private participant: Participant;
/**
* Initializes for a participant.
* @param participant
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class IframeAPI extends BasePageObject {
/**
* Returns the json object from the iframeAPI helper.
* @param event

View File

@@ -0,0 +1,44 @@
import BaseDialog from './BaseDialog';
const CONFERENCE_ID = 'conference-id';
const DIALOG_CONTAINER = 'invite-more-dialog';
/**
* Represents the invite dialog in a particular participant.
*/
export default class InviteDialog extends BaseDialog {
/**
* Checks if the dialog is open.
*/
async isOpen() {
return this.participant.driver.$(`.${DIALOG_CONTAINER}`).isExisting();
}
/**
* Open the invite dialog, if the info dialog is closed.
*/
async open() {
if (await this.isOpen()) {
return;
}
await this.participant.getParticipantsPane().clickInvite();
}
/**
* Returns the PIN for the conference.
*/
async getPinNumber() {
await this.open();
const elem = this.participant.driver.$(`.${CONFERENCE_ID}`);
await elem.waitForExist({ timeout: 5000 });
const fullText = await elem.getText();
this.participant.log(`Extracted text in invite dialog: ${fullText}`);
return fullText.split(':')[1].trim().replace(/[# ]/g, '');
}
}

View File

@@ -1,25 +1,17 @@
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
const REENABLE_SELF_VIEW_NOTIFICATION_ID = 'notify.selfViewTitle';
const REENABLE_SELF_VIEW_CLOSE_NOTIFICATION = 'notify.selfViewTitle-dismiss';
/**
* Gathers all notifications logic in the UI and obtaining those.
*/
export default class Notifications {
private participant: Participant;
/**
* Represents the Audio Video Moderation menu in the participants pane.
* @param participant
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class Notifications extends BasePageObject {
/**
* Waits for the raised hand notification to be displayed.
* The notification on moderators page when the participant tries to unmute.
@@ -51,4 +43,23 @@ export default class Notifications {
[ `${JOIN_ONE_TEST_ID}-dismiss`, `${JOIN_TWO_TEST_ID}-dismiss`, `${JOIN_MULTIPLE_TEST_ID}-dismiss` ]
.map(async id => this.participant.driver.$(`#${id}"]`).click()));
}
/**
* Waits for the self view notification to be displayed.
*/
async waitForReEnableSelfViewNotification() {
const el
= this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_NOTIFICATION_ID}"]`);
await el.waitForExist({ timeout: 2000 });
await el.waitForDisplayed();
}
/**
* Closes the self view notification.
*/
async closeReEnableSelfViewNotification() {
await this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_CLOSE_NOTIFICATION}"]`).click();
}
}

View File

@@ -1,27 +1,19 @@
import { Participant } from '../helpers/Participant';
import AVModerationMenu from './AVModerationMenu';
import BasePageObject from './BasePageObject';
/**
* Classname of the closed/hidden participants pane
*/
const PARTICIPANTS_PANE = 'participants_pane';
const INVITE = 'Invite someone';
/**
* Represents the participants pane from the UI.
*/
export default class ParticipantsPane {
private participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class ParticipantsPane extends BasePageObject {
/**
* Gets the audio video moderation menu.
*/
@@ -138,22 +130,10 @@ export default class ParticipantsPane {
await this.participant.getNotifications().dismissAnyJoinNotification();
const participantId = await participantToUnmute.getEndpointId();
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
await participantItem.waitForExist();
await participantItem.waitForStable();
await participantItem.waitForDisplayed();
await participantItem.moveTo();
await this.selectParticipant(participantToUnmute);
if (fromContextMenu) {
const meetingParticipantMoreOptions = this.participant.driver
.$(`[data-testid="participant-more-options-${participantId}"]`);
await meetingParticipantMoreOptions.waitForExist();
await meetingParticipantMoreOptions.waitForDisplayed();
await meetingParticipantMoreOptions.waitForStable();
await meetingParticipantMoreOptions.moveTo();
await meetingParticipantMoreOptions.click();
await this.openParticipantContextMenu(participantToUnmute);
}
const unmuteButton = this.participant.driver
@@ -162,4 +142,46 @@ export default class ParticipantsPane {
await unmuteButton.waitForExist();
await unmuteButton.click();
}
/**
* Open context menu for given participant.
*/
async selectParticipant(participant: Participant) {
const participantId = await participant.getEndpointId();
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
await participantItem.waitForExist();
await participantItem.waitForStable();
await participantItem.waitForDisplayed();
await participantItem.moveTo();
}
/**
* Open context menu for given participant.
*/
async openParticipantContextMenu(participant: Participant) {
const participantId = await participant.getEndpointId();
const meetingParticipantMoreOptions = this.participant.driver
.$(`[data-testid="participant-more-options-${participantId}"]`);
await meetingParticipantMoreOptions.waitForExist();
await meetingParticipantMoreOptions.waitForDisplayed();
await meetingParticipantMoreOptions.waitForStable();
await meetingParticipantMoreOptions.moveTo();
await meetingParticipantMoreOptions.click();
}
/**
* Clicks the invite button.
*/
async clickInvite() {
if (!await this.isOpen()) {
await this.open();
}
const inviteButton = this.participant.driver.$(`aria/${INVITE}`);
await inviteButton.waitForDisplayed();
await inviteButton.click();
}
}

View File

@@ -1,7 +1,9 @@
import BaseDialog from './BaseDialog';
const EMAIL_FIELD = '#setEmail';
const HIDE_SELF_VIEW_CHECKBOX = '//input[@name="hide-self-view"]';
const SETTINGS_DIALOG_CONTENT = '.settings-pane';
const X_PATH_MORE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="General"]';
const X_PATH_PROFILE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Profile"]';
/**
@@ -34,6 +36,13 @@ export default class SettingsDialog extends BaseDialog {
await this.openTab(X_PATH_PROFILE_TAB);
}
/**
* Selects the Profile tab to be displayed.
*/
async openMoreTab() {
await this.openTab(X_PATH_MORE_TAB);
}
/**
* Enters the passed in email into the email field.
* @param email
@@ -59,4 +68,21 @@ export default class SettingsDialog extends BaseDialog {
async submit() {
await this.clickOkButton();
}
/**
* Sets the state checked/selected of a checkbox in the settings dialog.
*/
async setHideSelfView(hideSelfView: boolean) {
await this.openMoreTab();
const checkbox = this.participant.driver.$(HIDE_SELF_VIEW_CHECKBOX);
await checkbox.waitForExist();
if (hideSelfView !== await checkbox.isSelected()) {
// we show a div with svg and text after the input and those elements grab the click
// so we need to click on the parent element
await this.participant.driver.$(`${HIDE_SELF_VIEW_CHECKBOX}//ancestor::div[1]`).click();
}
}
}

View File

@@ -1,14 +1,21 @@
// eslint-disable-next-line no-unused-vars
import { Participant } from '../helpers/Participant';
import BasePageObject from './BasePageObject';
const AUDIO_MUTE = 'Mute microphone';
const AUDIO_UNMUTE = 'Unmute microphone';
const CHAT = 'Open chat';
const CLOSE_CHAT = 'Close chat';
const CLOSE_PARTICIPANTS_PANE = 'Close participants pane';
const DESKTOP = 'Start sharing your screen';
const HANGUP = 'Leave the meeting';
const OVERFLOW_MENU = 'More actions menu';
const OVERFLOW = 'More actions';
const PARTICIPANTS = 'Open participants pane';
const PROFILE = 'Edit your profile';
const RAISE_HAND = 'Raise your hand';
const SETTINGS = 'Open settings';
const STOP_DESKTOP = 'Stop sharing your screen';
const ENTER_TILE_VIEW_BUTTON = 'Enter tile view';
const EXIT_TILE_VIEW_BUTTON = 'Exit tile view';
const VIDEO_QUALITY = 'Manage video quality';
const VIDEO_MUTE = 'Stop camera';
const VIDEO_UNMUTE = 'Start camera';
@@ -16,18 +23,7 @@ const VIDEO_UNMUTE = 'Start camera';
/**
* The toolbar elements.
*/
export default class Toolbar {
private participant: Participant;
/**
* Creates toolbar for a participant.
*
* @param {Participant} participant - The participants.
*/
constructor(participant: Participant) {
this.participant = participant;
}
export default class Toolbar extends BasePageObject {
/**
* Returns the button.
*
@@ -36,7 +32,7 @@ export default class Toolbar {
* @private
*/
private getButton(accessibilityCSSSelector: string) {
return this.participant.driver.$(`[aria-label^="${accessibilityCSSSelector}"]`);
return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
}
/**
@@ -125,7 +121,10 @@ export default class Toolbar {
*/
async clickParticipantsPaneButton(): Promise<void> {
this.participant.log('Clicking on: Participants pane Button');
await this.getButton(PARTICIPANTS).click();
// Special case for participants pane button, as it contains the number of participants and its label
// is changing
await this.participant.driver.$(`[aria-label^="${PARTICIPANTS}"]`).click();
}
/**
@@ -151,6 +150,65 @@ export default class Toolbar {
await this.getButton(RAISE_HAND).click();
}
/**
* Clicks on the chat button that opens chat panel.
*/
async clickChatButton(): Promise<void> {
this.participant.log('Clicking on: Chat Button');
await this.getButton(CHAT).click();
}
/**
* Clicks on the chat button that closes chat panel.
*/
async clickCloseChatButton(): Promise<void> {
this.participant.log('Clicking on: Close Chat Button');
await this.getButton(CLOSE_CHAT).click();
}
/**
* Clicks on the desktop sharing button that starts desktop sharing.
*/
async clickDesktopSharingButton() {
await this.getButton(DESKTOP).click();
}
/**
* Clicks on the desktop sharing button to stop it.
*/
async clickStopDesktopSharingButton() {
await this.getButton(STOP_DESKTOP).click();
}
/**
* Clicks on the tile view button which enables tile layout.
*/
async clickEnterTileViewButton() {
await this.getButton(ENTER_TILE_VIEW_BUTTON).click();
}
/**
* Clicks on the tile view button which exits tile layout.
*/
async clickExitTileViewButton() {
await this.getButton(EXIT_TILE_VIEW_BUTTON).click();
}
/**
* Clicks on the hangup button that ends the conference.
*/
async clickHangupButton(): Promise<void> {
this.participant.log('Clicking on: Hangup Button');
await this.getButton(HANGUP).click();
}
/**
* Clicks on the settings toolbar button which opens or closes the settings panel.
*/
async clickSettingsButton() {
await this.clickButtonInOverflowMenu(SETTINGS);
}
/**
* Ensure the overflow menu is open and clicks on a specified button.
* @param accessibilityLabel The accessibility label of the button to be clicked.
@@ -159,6 +217,10 @@ export default class Toolbar {
private async clickButtonInOverflowMenu(accessibilityLabel: string) {
await this.openOverflowMenu();
// sometimes the overflow button tooltip is over the last entry in the menu,
// so let's move focus away before clicking the button
await this.participant.driver.$('#overflow-context-menu').moveTo();
this.participant.log(`Clicking on: ${accessibilityLabel}`);
await this.getButton(accessibilityLabel).click();
@@ -170,7 +232,7 @@ export default class Toolbar {
* @private
*/
private async isOverflowMenuOpen() {
return await this.participant.driver.$$(`[aria-label^="${OVERFLOW_MENU}"]`).length > 0;
return await this.participant.driver.$$(`aria/${OVERFLOW_MENU}`).length > 0;
}
/**
@@ -215,7 +277,7 @@ export default class Toolbar {
* @private
*/
private async waitForOverFlowMenu(visible: boolean) {
await this.participant.driver.$(`[aria-label^="${OVERFLOW_MENU}"]`).waitForDisplayed({
await this.getButton(OVERFLOW_MENU).waitForDisplayed({
reverse: !visible,
timeout: 3000,
timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`

View File

@@ -33,36 +33,6 @@ describe('Audio only - ', () => {
await setAudioOnlyAndCheck(false);
});
/**
* Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
* p2 participant sees a video mute state for the former.
* @param enable
*/
async function setAudioOnlyAndCheck(enable: boolean) {
const { p1 } = ctx;
await p1.getVideoQualityDialog().setVideoQuality(enable);
await verifyVideoMute(enable);
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
.waitForDisplayed({ reverse: !enable });
}
/**
* Verifies that p1 and p2 see p1 as video muted or not.
* @param muted
*/
async function verifyVideoMute(muted: boolean) {
const { p1, p2 } = ctx;
// Verify the observer sees the testee in the desired muted state.
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
// Verify the testee sees itself in the desired muted state.
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
}
/**
* Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1
* as video muted.
@@ -92,3 +62,33 @@ describe('Audio only - ', () => {
await verifyVideoMute(false);
});
});
/**
* Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
* p2 participant sees a video mute state for the former.
* @param enable
*/
async function setAudioOnlyAndCheck(enable: boolean) {
const { p1 } = ctx;
await p1.getVideoQualityDialog().setVideoQuality(enable);
await verifyVideoMute(enable);
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
.waitForDisplayed({ reverse: !enable });
}
/**
* Verifies that p1 and p2 see p1 as video muted or not.
* @param muted
*/
async function verifyVideoMute(muted: boolean) {
const { p1, p2 } = ctx;
// Verify the observer sees the testee in the desired muted state.
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
// Verify the testee sees itself in the desired muted state.
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
}

View File

@@ -0,0 +1,45 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('DisplayName - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx, {
skipDisplayName: true
});
});
it('check change', async () => {
const { p1, p2 } = ctx;
// default remote display name
const defaultDisplayName = await p1.driver.execute(() => config.defaultRemoteDisplayName);
const p1EndpointId = await p1.getEndpointId();
const p2EndpointId = await p2.getEndpointId();
// Checks whether default display names are set and shown, when both sides still miss the display name.
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EndpointId)).toBe(defaultDisplayName);
expect(await p2.getFilmstrip().getRemoteDisplayName(p1EndpointId)).toBe(defaultDisplayName);
const randomName = `Name${Math.trunc(Math.random() * 1_000_000)}`;
await p2.setLocalDisplayName(randomName);
expect(await p2.getLocalDisplayName()).toBe(randomName);
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EndpointId)).toBe(randomName);
});
it('check persistence', async () => {
const { p2 } = ctx;
const randomName = `Name${Math.trunc(Math.random() * 1_000_000)}`;
await p2.setLocalDisplayName(randomName);
expect(await p2.getLocalDisplayName()).toBe(randomName);
await p2.hangup();
await ensureTwoParticipants(ctx, {
skipDisplayName: true
});
expect(await p2.getLocalDisplayName()).toBe(randomName);
});
});

View File

@@ -0,0 +1,22 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('End Conference - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
});
it('hangup call and check', async () => {
const { p1 } = ctx;
const url = await p1.driver.getUrl();
await p1.getToolbar().clickHangupButton();
await p1.driver.waitUntil(
async () => await p1.driver.getUrl() !== url,
{
timeout: 5000,
timeoutMsg: 'p1 did not navigate away from the conference'
}
);
});
});

View File

@@ -0,0 +1,53 @@
import process from 'node:process';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Fake Dial-In - ', () => {
it('join participant', async () => {
// we execute fake dial in only if the real dial in is not enabled
// check rest url is not configured
if (process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = true;
return;
}
await ensureOneParticipant(ctx);
// check dial-in is enabled, so skip
if (await ctx.p1.driver.execute(() => Boolean(
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts && config.hosts.muc))) {
ctx.skipSuiteTests = true;
}
});
it('open invite dialog', async () => {
await ctx.p1.getInviteDialog().open();
await ctx.p1.getInviteDialog().clickCloseButton();
});
it('invite second participant', async () => {
if (!await ctx.p1.isInMuc()) {
// local participant did not join abort
return;
}
await ensureTwoParticipants(ctx);
});
it('wait for audio from second participant', async () => {
const { p1 } = ctx;
if (!await p1.isInMuc()) {
// local participant did not join abort
return;
}
await waitForAudioFromDialInParticipant(p1);
await cleanup(p1);
});
});

View File

@@ -0,0 +1,70 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Self view - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
});
it('hide from menu', async () => {
const { p1 } = ctx;
await checkSelfViewHidden(p1, false);
await p1.getFilmstrip().hideSelfView();
await checkSelfViewHidden(p1, true, true);
await p1.getToolbar().clickEnterTileViewButton();
await checkSelfViewHidden(p1, true);
});
it('show from settings', async () => {
const { p1 } = ctx;
await toggleSelfViewFromSettings(p1, false);
await checkSelfViewHidden(p1, false);
});
it('hide from settings', async () => {
const { p1 } = ctx;
await toggleSelfViewFromSettings(p1, true);
await checkSelfViewHidden(p1, true, true);
});
it('check in alone meeting', async () => {
const { p1, p2 } = ctx;
await checkSelfViewHidden(p1, true);
await p2.hangup();
await checkSelfViewHidden(p1, true);
});
});
/**
* Toggles the self view option from the settings dialog.
*/
async function toggleSelfViewFromSettings(participant: Participant, hide: boolean) {
await participant.getToolbar().clickSettingsButton();
const settings = participant.getSettingsDialog();
await settings.waitForDisplay();
await settings.setHideSelfView(hide);
await settings.submit();
}
/**
* Checks whether the local self view is displayed or not.
*/
async function checkSelfViewHidden(participant: Participant, hidden: boolean, checkNotification = false) {
if (checkNotification) {
await participant.getNotifications().waitForReEnableSelfViewNotification();
await participant.getNotifications().closeReEnableSelfViewNotification();
}
await participant.getFilmstrip().assertSelfViewIsHidden(hidden);
}

View File

@@ -0,0 +1,464 @@
import type { ChainablePromiseElement } from 'webdriverio';
import type { Participant } from '../../helpers/Participant';
import { checkSubject, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
const MAIN_ROOM_NAME = 'Main room';
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
const LIST_ITEM_CONTAINER = 'list-item-container';
describe('BreakoutRooms ', () => {
it('check support', async () => {
await ensureTwoParticipants(ctx);
if (!await ctx.p1.isBreakoutRoomsSupported()) {
ctx.skipSuiteTests = true;
}
});
it('add breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be no breakout rooms initially, list is sent with a small delay
await p1.driver.pause(2000);
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
// add one breakout room
await p1BreakoutRooms.addBreakoutRoom();
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 2000,
timeoutMsg: 'No breakout room added for p1'
});
// second participant should also see one breakout room
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
timeout: 2000,
timeoutMsg: 'No breakout room seen by p2'
});
});
it('join breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be one breakout room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 1000,
timeoutMsg: 'No breakout room seen by p1'
});
const roomsList = await p1BreakoutRooms.getRooms();
expect(roomsList.length).toBe(1);
// join the room
await roomsList[0].joinRoom();
// the participant should see the main room as the only breakout room
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].name === MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeoutMsg: 'P1 did not join breakout room'
});
// the second participant should see one participant in the breakout room
await p2.driver.waitUntil(
async () => {
const list = await p2.getBreakoutRooms().getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
});
});
it('leave breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// leave room
await p1BreakoutRooms.leaveBreakoutRoom();
// there should be one breakout room and that should not be the main room
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].name !== MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeoutMsg: 'P1 did not leave breakout room'
});
// the second participant should see no participants in the breakout room
await p2.driver.waitUntil(
async () => {
const list = await p2.getBreakoutRooms().getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'P2 is seeing p1 in the breakout room'
});
});
it('remove breakout room', async () => {
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// remove the room
await (await p1BreakoutRooms.getRooms())[0].removeRoom();
// there should be no breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
timeout: 2000,
timeoutMsg: 'Breakout room was not removed for p1'
});
// the second participant should also see no breakout rooms
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
timeout: 2000,
timeoutMsg: 'Breakout room was not removed for p2'
});
});
it('auto assign', async () => {
await ensureThreeParticipants(ctx);
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// create two rooms
await p1BreakoutRooms.addBreakoutRoom();
await p1BreakoutRooms.addBreakoutRoom();
// there should be two breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
timeout: 2000,
timeoutMsg: 'Breakout room was not created by p1'
});
// auto assign participants to rooms
await p1BreakoutRooms.autoAssignToBreakoutRooms();
// each room should have one participant
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
});
// the second participant should see one participant in the main room
const p2BreakoutRooms = p2.getBreakoutRooms();
await p2.driver.waitUntil(
async () => {
if (await p2BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p2BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 1 && list[1].participantCount === 1
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
}, {
timeout: 2000,
timeoutMsg: 'P2 is not seeing p1 in the main room'
});
});
it('close breakout room', async () => {
const { p1, p2, p3 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be two non-empty breakout rooms
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing two breakout rooms'
});
// close the first room
await (await p1BreakoutRooms.getRooms())[0].closeRoom();
// there should be two rooms and first one should be empty
await p1.driver.waitUntil(
async () => {
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount === 0 || list[1].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing an empty breakout room'
});
// there should be two participants in the main room, either p2 or p3 got moved to the main room
const checkParticipants = async (p: Participant) => {
await p.driver.waitUntil(
async () => {
const isInBreakoutRoom = await p.isInBreakoutRoom();
const breakoutRooms = p.getBreakoutRooms();
if (isInBreakoutRoom) {
if (await breakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await breakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list.every(r => { // eslint-disable-line arrow-body-style
return r.name === MAIN_ROOM_NAME ? r.participantCount === 2 : r.participantCount === 0;
});
}
if (await breakoutRooms.getRoomsCount() !== 2) {
return false;
}
const list = await breakoutRooms.getRooms();
if (list?.length !== 2) {
return false;
}
return list[0].participantCount + list[1].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
});
};
await checkParticipants(p2);
await checkParticipants(p3);
});
it('send participants to breakout room', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
// because the participants rejoin so fast, the meeting is not properly ended,
// so the previous breakout rooms would still be there.
// To avoid this issue we use a different meeting
ctx.roomName += '-breakout-rooms';
await ensureTwoParticipants(ctx);
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be no breakout rooms
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
// add one breakout room
await p1BreakoutRooms.addBreakoutRoom();
// there should be one empty room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
timeout: 2000,
timeoutMsg: 'No breakout room added for p1'
});
// send the second participant to the first breakout room
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, (await p1BreakoutRooms.getRooms())[0].name);
// there should be one room with one participant
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
});
it('collapse breakout room', async () => {
const { p1 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// there should be one breakout room with one participant
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
// get id of the breakout room participant
const breakoutList = p1.driver.$(`#${BREAKOUT_ROOMS_LIST_ID}`);
const breakoutRoomItem = await breakoutList.$$(`.${LIST_ITEM_CONTAINER}`).find(
async el => {
const id = await el.getAttribute('id');
return id !== '' && id !== null;
}) as ChainablePromiseElement;
const pId = await breakoutRoomItem.getAttribute('id');
const breakoutParticipant = p1.driver.$(`//div[@id="${pId}"]`);
expect(await breakoutParticipant.isDisplayed()).toBe(true);
// collapse the first
await (await p1BreakoutRooms.getRooms())[0].collapse();
// the participant should not be visible
expect(await breakoutParticipant.isDisplayed()).toBe(false);
// the collapsed room should still have one participant
expect((await p1BreakoutRooms.getRooms())[0].participantCount).toBe(1);
});
it('rename breakout room', async () => {
const myNewRoomName = `breakout-${crypto.randomUUID()}`;
const { p1, p2 } = ctx;
const p1BreakoutRooms = p1.getBreakoutRooms();
// let's rename breakout room and see it in local and remote
await (await p1BreakoutRooms.getRooms())[0].renameRoom(myNewRoomName);
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].name === myNewRoomName;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
await checkSubject(p2, myNewRoomName);
// leave room
await p2.getBreakoutRooms().leaveBreakoutRoom();
// there should be one empty room
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
expect((await p2.getBreakoutRooms().getRooms())[0].name).toBe(myNewRoomName);
// send the second participant to the first breakout room
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);
// there should be one room with one participant
await p1.driver.waitUntil(
async () => {
const list = await p1BreakoutRooms.getRooms();
if (list?.length !== 1) {
return false;
}
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not rename for p1'
});
await checkSubject(p2, myNewRoomName);
});
});

View File

@@ -0,0 +1,121 @@
import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
describe('Codec selection - ', () => {
it('asymmetric codecs', async () => {
await ensureOneParticipant(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ]
}
}
});
await ensureTwoParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP8', 'VP9', 'AV1' ]
}
}
});
const { p1, p2 } = ctx;
// Check if media is playing on both endpoints.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Check if p1 is sending VP9 and p2 is sending VP8 as per their codec preferences.
// Except on Firefox because it doesn't support VP9 encode.
if (p1.driver.isFirefox) {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
} else {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
}
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
});
it('asymmetric codecs with AV1', async () => {
await ensureThreeParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
}
}
});
const { p1, p2, p3 } = ctx;
// Check if media is playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
// Except on Firefox because it doesn't support AV1/VP9 encode and AV1 decode.
if (p1.driver.isFirefox) {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
} else {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
}
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
// If there is a Firefox ep in the call, all other eps will switch to VP9.
if (p1.driver.isFirefox) {
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
} else {
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
}
});
it('codec switch over', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureTwoParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ]
}
}
});
const { p1, p2 } = ctx;
// Disable this test on Firefox because it doesn't support VP9 encode.
if (p1.driver.isFirefox) {
return;
}
// Check if p1 and p2 are encoding in VP9 which is the default codec.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
await ensureThreeParticipants(ctx, {
configOverwrite: {
videoQuality: {
codecPreferenceOrder: [ 'VP8' ]
}
}
});
const { p3 } = ctx;
// Check if all three participants are encoding in VP8 now.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
await p3.hangup();
// Check of p1 and p2 have switched to VP9.
await p1.driver.waitUntil(
async () => await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'
}
);
await p2.driver.waitUntil(
async () => await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'
}
);
});
});

View File

@@ -0,0 +1,353 @@
import { SET_AUDIO_ONLY } from '../../../react/features/base/audio-only/actionTypes';
import type { Participant } from '../../helpers/Participant';
import {
ensureFourParticipants,
ensureOneParticipant,
ensureThreeParticipants,
ensureTwoParticipants
} from '../../helpers/participants';
describe('Desktop sharing - ', () => {
it('start', async () => {
await ensureTwoParticipants(ctx, {
configOverwrite: {
p2p: {
enabled: true
}
}
});
const { p1, p2 } = ctx;
await p2.getToolbar().clickDesktopSharingButton();
// Check if a remote screen share tile is created on p1.
await checkForScreensharingTile(p2, p1);
// Check if a local screen share tile is created on p2.
await checkForScreensharingTile(p2, p2);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
it('stop', async () => {
const { p1, p2 } = ctx;
await p2.getToolbar().clickStopDesktopSharingButton();
// Check if the local screen share thumbnail disappears on p2.
await checkForScreensharingTile(p2, p2, true);
// Check if the remote screen share thumbnail disappears on p1.
await checkForScreensharingTile(p1, p2, true);
});
/**
* Ensures screen share is still visible when the call switches from p2p to jvb connection.
*/
it('p2p to jvb switch', async () => {
await ctx.p2.getToolbar().clickDesktopSharingButton();
await ensureThreeParticipants(ctx);
const { p1, p2, p3 } = ctx;
// Check if a remote screen share tile is created on all participants.
await checkForScreensharingTile(p2, p1);
await checkForScreensharingTile(p2, p2);
await checkForScreensharingTile(p2, p2);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
* Ensure screen share is still visible when the call switches from jvb to p2p and back.
*/
it('p2p to jvb switch and back', async () => {
const { p1, p2, p3 } = ctx;
await p3.hangup();
// Check if a remote screen share tile is created on p1 and p2 after switching back to p2p.
await checkForScreensharingTile(p2, p1);
await checkForScreensharingTile(p2, p2);
// The video should be playing.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Start desktop share on p1.
await p1.getToolbar().clickDesktopSharingButton();
// Check if a new tile for p1's screen share is created on both p1 and p2.
await checkForScreensharingTile(p1, p1);
await checkForScreensharingTile(p1, p2);
await ensureThreeParticipants(ctx);
await checkForScreensharingTile(p1, p3);
await checkForScreensharingTile(p2, p3);
// The large video should be playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
* Ensure that screen share is still visible in jvb connection when share is toggled while the users are
* in p2p mode, i.e., share is restarted when user is in p2p mode and then the call switches over to jvb mode.
*/
it('stop screen sharing and back', async () => {
const { p1, p2, p3 } = ctx;
// Stop share on both p1 and p2.
await p1.getToolbar().clickStopDesktopSharingButton();
await p2.getToolbar().clickStopDesktopSharingButton();
await p3.hangup();
// Start share on both p1 and p2.
await p1.getToolbar().clickDesktopSharingButton();
await p2.getToolbar().clickDesktopSharingButton();
// Check if p1 and p2 can see each other's shares in p2p.
await checkForScreensharingTile(p1, p2);
await checkForScreensharingTile(p2, p1);
// Add p3 back to the conference and check if p1 and p2's shares are visible on p3.
await ensureThreeParticipants(ctx);
await checkForScreensharingTile(p1, p3);
await checkForScreensharingTile(p2, p3);
// The large video should be playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
* Ensures screen share is visible when a muted screen share track is added to the conference, i.e.,
* users starts and stops the share before anyone else joins the call.
* The call switches to jvb and then back to p2p.
*/
it('screen sharing toggle before others join', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureOneParticipant(ctx, {
configOverwrite: {
p2p: {
enabled: true
}
}
});
const { p1 } = ctx;
// p1 starts share when alone in the call.
await p1.getToolbar().clickDesktopSharingButton();
await checkForScreensharingTile(p1, p1);
// p1 stops share.
await p1.getToolbar().clickStopDesktopSharingButton();
// Call switches to jvb.
await ensureThreeParticipants(ctx);
const { p2, p3 } = ctx;
// p1 starts share again when call switches to jvb.
await p1.getToolbar().clickDesktopSharingButton();
// Check p2 and p3 are able to see p1's share.
await checkForScreensharingTile(p1, p2);
await checkForScreensharingTile(p1, p3);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// p3 leaves the call.
await p3.hangup();
// Make sure p2 see's p1's share after the call switches back to p2p.
await checkForScreensharingTile(p1, p2);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// p2 starts share when in p2p.
await p2.getToolbar().clickDesktopSharingButton();
// Makes sure p2's share is visible on p1.
await checkForScreensharingTile(p2, p1);
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
* A case where a non-dominant speaker is sharing screen for a participant in low bandwidth mode
* where only a screen share can be received. A bug fixed in jvb 0c5dd91b where the video was not received.
*/
it('audio only and non dominant screen share', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureOneParticipant(ctx);
const { p1 } = ctx;
// a workaround to directly set audio only mode without going through the rest of the settings in the UI
await p1.driver.execute(type => {
APP.store.dispatch({
type,
audioOnly: true
});
APP.conference.onToggleAudioOnly();
}, SET_AUDIO_ONLY);
await p1.getToolbar().clickAudioMuteButton();
await ensureThreeParticipants(ctx, { skipInMeetingChecks: true });
const { p2, p3 } = ctx;
await p3.getToolbar().clickAudioMuteButton();
await p3.getToolbar().clickDesktopSharingButton();
await checkForScreensharingTile(p3, p1);
await checkForScreensharingTile(p3, p2);
// the video should be playing
await p1.driver.waitUntil(async () =>
await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
timeout: 5_000,
timeoutMsg: 'expected remote screen share to be on large'
});
});
/**
* A case where first participant is muted (a&v) and enters low bandwidth mode,
* the second one is audio muted only and the one sharing (the third) is dominant speaker.
* A problem fixed in jitsi-meet 3657c19e and d6ab0a72.
*/
it('audio only and dominant screen share', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureOneParticipant(ctx, {
configOverwrite: {
startWithAudioMuted: true,
startWithVideoMuted: true
}
});
const { p1 } = ctx;
// a workaround to directly set audio only mode without going through the rest of the settings in the UI
await p1.driver.execute(type => {
APP.store.dispatch({
type,
audioOnly: true
});
APP.conference.onToggleAudioOnly();
}, SET_AUDIO_ONLY);
await ensureTwoParticipants(ctx, {
skipInMeetingChecks: true,
configOverwrite: {
startWithAudioMuted: true
}
});
await ensureThreeParticipants(ctx, {
skipInMeetingChecks: true
});
const { p2, p3 } = ctx;
await p3.getToolbar().clickDesktopSharingButton();
await checkForScreensharingTile(p3, p1);
await checkForScreensharingTile(p3, p2);
// The desktop sharing participant should be on large
expect(await p1.driver.execute(() => APP.UI.getLargeVideoID())).toBe(`${await p3.getEndpointId()}-v1`);
// the video should be playing
await p1.driver.waitUntil(async () =>
await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
timeout: 5_000,
timeoutMsg: 'expected remote screen share to be on large'
});
});
/**
* Test screensharing with lastN. We add p4 with lastN=2 and verify that it receives the expected streams.
*/
it('with lastN', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureThreeParticipants(ctx);
const { p1, p2, p3 } = ctx;
await p3.getToolbar().clickDesktopSharingButton();
await p1.getToolbar().clickAudioMuteButton();
await p3.getToolbar().clickAudioMuteButton();
await ensureFourParticipants(ctx, {
configOverwrite: {
channelLastN: 2,
startWithAudioMuted: true
}
});
const { p4 } = ctx;
// We now have p1, p2, p3, p4.
// p3 is screensharing.
// p1, p3, p4 are audio muted, so p2 should eventually become dominant speaker.
// Participants should display p3 on-stage because it is screensharing.
await checkForScreensharingTile(p3, p1);
await checkForScreensharingTile(p3, p2);
await checkForScreensharingTile(p3, p4);
// And the video should be playing
expect(await p4.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
const p1EndpointId = await p1.getEndpointId();
// p4 has lastN=2 and has selected p3. With p2 being dominant speaker p4 should eventually
// see video for [p3, p2] and p1 as ninja.
await p4.driver.$(`//span[@id='participant_${p1EndpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000
});
const p2EndpointId = await p2.getEndpointId();
await p4.driver.waitUntil(async () =>
await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
p2EndpointId) && await p4.driver.$(
`//span[@id="participant_${p2EndpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: 'expected remote video to be received 15s'
});
await p4.driver.waitUntil(async () =>
await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
p2EndpointId) && await p4.driver.$(
`//span[@id="participant_${p2EndpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: 'expected remote video to be received 15s'
});
// Let's switch and check, muting participant 2 and unmuting 1 will leave participant 1 as dominant
await p1.getToolbar().clickAudioUnmuteButton();
await p2.getToolbar().clickAudioMuteButton();
// Participant4 should eventually see video for [p3, p1] and p2 as a ninja.
await p4.driver.$(`//span[@id='participant_${p2EndpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000
});
await p4.driver.waitUntil(async () =>
await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
p1EndpointId) && await p4.driver.$(
`//span[@id="participant_${p1EndpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: 'expected remote video to be received 15s'
});
});
});
/**
* Check if a screensharing tile is displayed on the observer.
*/
async function checkForScreensharingTile(sharer: Participant, observer: Participant, reverse = false) {
await observer.driver.$(`//span[@id='participant_${await sharer.getEndpointId()}-v1']`).waitForDisplayed({
timeout: 3_000,
reverse
});
}

View File

@@ -0,0 +1,34 @@
import { ensureOneParticipant } from '../../helpers/participants';
describe('Chat Panel - ', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);
});
it('start closed', async () => {
expect(await ctx.p1.getChatPanel().isOpen()).toBe(false);
});
it('open', async () => {
const { p1 } = ctx;
await p1.getToolbar().clickChatButton();
expect(await p1.getChatPanel().isOpen()).toBe(true);
});
it('use shortcut to close', async () => {
const chatPanel = ctx.p1.getChatPanel();
await chatPanel.pressShortcut();
expect(await chatPanel.isOpen()).toBe(false);
});
it('use shortcut to open', async () => {
const chatPanel = ctx.p1.getChatPanel();
await chatPanel.pressShortcut();
expect(await chatPanel.isOpen()).toBe(true);
});
it('use button to open', async () => {
const { p1 } = ctx;
await p1.getToolbar().clickCloseChatButton();
expect(await p1.getChatPanel().isOpen()).toBe(false);
});
});

View File

@@ -0,0 +1,83 @@
import https from 'node:https';
import process from 'node:process';
import { ensureOneParticipant } from '../../helpers/participants';
import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Dial-In - ', () => {
it('join participant', async () => {
// check rest url is configured
if (!process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = true;
return;
}
await ensureOneParticipant(ctx);
// check dial-in is enabled
if (!await ctx.p1.driver.execute(() => Boolean(
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts && config.hosts.muc))) {
ctx.skipSuiteTests = true;
}
});
it('retrieve pin', async () => {
const dialInPin = await ctx.p1.getInviteDialog().getPinNumber();
await ctx.p1.getInviteDialog().clickCloseButton();
if (dialInPin.length === 0) {
console.error('dial-in.test.no-pin');
}
expect(dialInPin.length >= 9).toBe(true);
ctx.dialInPin = dialInPin;
});
it('invite dial-in participant', async () => {
if (!await ctx.p1.isInMuc()) {
// local participant did not join abort
return;
}
const restUrl = process.env.DIAL_IN_REST_URL?.replace('{0}', ctx.dialInPin);
// we have already checked in the first test that DIAL_IN_REST_URL exist so restUrl cannot be ''
const responseData: string = await new Promise((resolve, reject) => {
https.get(restUrl || '', res => {
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.on('end', () => {
ctx.times.restAPIExecutionTS = performance.now();
resolve(data);
});
}).on('error', err => {
console.error('dial-in.test.restAPI.request.fail');
console.error(err);
reject(err);
});
});
console.log(`dial-in.test.call_session_history_id:${JSON.parse(responseData).call_session_history_id}`);
});
it('wait for audio from dial-in participant', async () => {
const { p1 } = ctx;
if (!await p1.isInMuc()) {
// local participant did not join abort
return;
}
await waitForAudioFromDialInParticipant(p1);
await cleanup(p1);
});
});

View File

@@ -0,0 +1,39 @@
import type { Participant } from '../../helpers/Participant';
/**
* Helper functions for dial-in related operations.
* To be able to create a fake dial-in test that will run most of the logic for the real dial-in test.
*/
/**
* Waits for the audio from the dial-in participant.
* @param participant
*/
export async function waitForAudioFromDialInParticipant(participant: Participant) {
// waits 15 seconds for the participant to join
await participant.waitForParticipants(1, `dial-in.test.jigasi.participant.no.join.for:${
ctx.times.restAPIExecutionTS + 15_000} ms.`);
const joinedTS = performance.now();
console.log(`dial-in.test.jigasi.participant.join.after:${joinedTS - ctx.times.restAPIExecutionTS}`);
await participant.waitForIceConnected();
await participant.waitForRemoteStreams(1);
await participant.waitForSendReceiveData(20_000, 'dial-in.test.jigasi.participant.no.audio.after.join');
console.log(`dial-in.test.jigasi.participant.received.audio.after.join:${performance.now() - joinedTS} ms.`);
}
/**
* Cleans up the dial-in participant by kicking it if the local participant is a moderator.
* @param participant
*/
export async function cleanup(participant: Participant) {
// cleanup
if (await participant.isModerator()) {
const jigasiEndpointId = await participant.driver.execute(() => APP.conference.listMembers()[0].getId());
await participant.getFilmstrip().kickParticipant(jigasiEndpointId);
}
}

Some files were not shown because too many files have changed in this diff Show More