mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-03-12 22:50:20 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ada6150971 | ||
|
|
62d1ee606a | ||
|
|
1e1de6acf0 | ||
|
|
e1e0fba4ff | ||
|
|
97be3fa399 | ||
|
|
fbdee85c4b | ||
|
|
b71b4c8c45 | ||
|
|
a8d123ede4 | ||
|
|
fb8e451e2e | ||
|
|
5dd7944bdb | ||
|
|
48732c6784 | ||
|
|
97146ed8a7 | ||
|
|
78b17c8d17 | ||
|
|
9732d1be86 | ||
|
|
a836187620 | ||
|
|
a0cad4c595 | ||
|
|
19b9eac84a | ||
|
|
068e33fc0f | ||
|
|
00c6bee2fd | ||
|
|
748ead7e13 | ||
|
|
174c4418fb | ||
|
|
64494cab81 | ||
|
|
50d0092e30 | ||
|
|
fdbd7239ff | ||
|
|
a30958ab23 | ||
|
|
aca55172e4 | ||
|
|
c6cce9253c | ||
|
|
c23684e11c | ||
|
|
30595584a3 |
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 d’une source stéréo n’est 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 l’enregistrement 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.",
|
||||
|
||||
@@ -410,6 +410,7 @@
|
||||
"sendPrivateMessageTitle": "私信回复?",
|
||||
"serviceUnavailable": "服务不可用",
|
||||
"sessTerminated": "通话已结束",
|
||||
"sessTerminatedReason": "会议已经结束",
|
||||
"sessionRestarted": "由于连接问题,呼叫重新启动。",
|
||||
"shareAudio": "继续",
|
||||
"shareAudioTitle": "如何分享音频",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
24
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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[]>}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -350,6 +350,7 @@ const Notification = ({
|
||||
onClick = { onDismiss }
|
||||
size = { 20 }
|
||||
src = { IconCloseLarge }
|
||||
tabIndex = { 0 }
|
||||
testId = { `${titleKey || descriptionKey}-dismiss` } />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './actions.any';
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import './middleware.any';
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
16
tests/pageobjects/BasePageObject.ts
Normal file
16
tests/pageobjects/BasePageObject.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
230
tests/pageobjects/BreakoutRooms.ts
Normal file
230
tests/pageobjects/BreakoutRooms.ts
Normal 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();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
22
tests/pageobjects/ChatPanel.ts
Normal file
22
tests/pageobjects/ChatPanel.ts
Normal 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' ]);
|
||||
}
|
||||
}
|
||||
@@ -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}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
44
tests/pageobjects/InviteDialog.ts
Normal file
44
tests/pageobjects/InviteDialog.ts
Normal 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, '');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'}`
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
45
tests/specs/2way/displayName.spec.ts
Normal file
45
tests/specs/2way/displayName.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
22
tests/specs/2way/endConference.spec.ts
Normal file
22
tests/specs/2way/endConference.spec.ts
Normal 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'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
53
tests/specs/2way/fakeDialInAudio.spec.ts
Normal file
53
tests/specs/2way/fakeDialInAudio.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
70
tests/specs/2way/selfView.spec.ts
Normal file
70
tests/specs/2way/selfView.spec.ts
Normal 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);
|
||||
}
|
||||
464
tests/specs/3way/breakoutRooms.spec.ts
Normal file
464
tests/specs/3way/breakoutRooms.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
121
tests/specs/3way/codecSelection.spec.ts
Normal file
121
tests/specs/3way/codecSelection.spec.ts
Normal 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'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
353
tests/specs/4way/desktopSharing.spec.ts
Normal file
353
tests/specs/4way/desktopSharing.spec.ts
Normal 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
|
||||
});
|
||||
}
|
||||
34
tests/specs/alone/chatPanel.spec.ts
Normal file
34
tests/specs/alone/chatPanel.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
83
tests/specs/alone/dialInAudio.spec.ts
Normal file
83
tests/specs/alone/dialInAudio.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
39
tests/specs/helpers/DialIn.ts
Normal file
39
tests/specs/helpers/DialIn.ts
Normal 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
Reference in New Issue
Block a user