Compare commits

...

15 Commits

Author SHA1 Message Date
damencho
5ff3219935 debug: Drop. 2025-10-02 14:40:39 -05:00
bgrozev
386bdbfc22 test: Use the directory as parentSuite. (#16493)
* test: Use the directory as a parent suite.
2025-10-02 14:30:57 -05:00
Jaya Allamsetty
a45453e391 fix(RTCStats) Stop sending dominantSpeaker events.
They are handled in lib-jitsi-meet now.
2025-10-01 18:01:57 -04:00
Jaya Allamsetty
07554a156b chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2097.0.0+58646fc3...v2099.0.0+89536686
2025-10-01 18:00:18 -04:00
bgrozev
70c3c8db13 test: Refactor, update and fix JaaS tests (#16463)
* ref: Move the jaas util out of specs/.
* ref: Extract a more generic joinMuc utility.
* ref: Rename joinMuc to joinJaasMuc.
* ref: Move tileView.spec.ts out of 2way, use joinMuc.
* ref: Enforce that "name" is p1, p2, p3, p4 using types.
* fix: Fix mute test filename.
* ref: Split the chat test into jaas and iframe tests.
* test: Add webhook verification to jaas visitor tests.
* ref: Remove the iframe/visitors test (ported to jaas).
* ref: Move the transcriptions test to jaas.
* ref: Make getEndpointId work from outside the iframe.
* ref: Remove TestProperties.useIFrameApi. Use the flag in IParticipantOptions instead.
* ref: Do not set a special tenant when the iFrame API is used, leave it to tests to determine.
* ref: Remove the jaas-specific tests from iframe/participantsPresnce (will be re-added under jaas/ later).
* ref: Move the dial in/out tests to jaas/.
* Add tests for jaas join/leave webhooks (port back from iframe/participantsPresence).
* config: Fallback to IFRAME_TENANT and JWT_* for jaas configuration.
* ref: Simplify boolean expression.
* ref: Remove the skipFirstModerator option (unused).
* ref: Do not override token if specified.
* fix: Do not generate token for alone/invite test.
* ref: Extract more dial-in utilities.
* test: Verify Invite UI in jaas.
* Do not generate token for dial in (case covered in jaas/).
* ref: Remove preferGenerateToken (unused).
* ref: Move mute utils in their own helper.
* fix: Fix setting the jaas enabled flag.
* Do not run alone/invite for jaas (temp fix).
* fix: Switch back to meeting window.
* Do not run alone/dialInAudio on jaas.
* Disable the SIP jibri test (broken).
2025-10-01 11:40:02 -05:00
damencho
9bb1c36508 fix(chat): Fixes sending message after system message. 2025-10-01 05:09:46 -05:00
damencho
a93ca9d7c4 fix(conference): Fixes showing max users notification. 2025-09-30 16:59:47 -05:00
damencho
d2f20c49af feat(visitors): Sends msg history to new visitor nodes.
When opening the connection to a new visitor node we send all the messages in current history to populate that new history and newly joined visitors will see the messages from the main room.
2025-09-30 07:02:19 -05:00
damencho
c5f82d4f20 fix(follow-me): Moves follow-me state to its feature. 2025-09-30 07:02:11 -05:00
Hristo Terezov
36a3e700e1 Complete French and Canadian French translations (#16461)
* feat(lang): Complete French and Canadian French translations

- Added 91 missing French translation keys to main-fr.json
- Updated Canadian French (main-fr-CA.json) with complete translation coverage
- Applied authentic Canadian French terminology (réunion vs conférence, É.-U. vs États-Unis)
- Removed 44 legacy keys from Canadian French to match English/French structure
- All files now have identical key coverage (1,469 keys each)
- Maintains regional linguistic preferences while ensuring 100% feature coverage
2025-09-29 16:12:05 -05:00
Hristo Terezov
77464ddcc4 fix(participants-pane): Allow multiline text in footer context menu
Fixes text truncation issue in participants pane footer context menu
items (the three-dot menu). Menu items now wrap naturally to multiple
lines instead of being truncated mid-word, improving readability for
languages with longer text strings like French.

The fix uses standard CSS properties (whiteSpace, wordBreak,
overflowWrap) without browser-specific prefixes. It is specific to
the footer context menu in the participants pane and does not affect
other context menus.
2025-09-29 16:10:19 -05:00
Дамян Минков
36ce5a1661 feat(token_verification): Adds more token failure reasons on verify room. (#16473)
* feat(token_verification): Adds more token failure reasons on verify room.

* squash: Update resources/prosody-plugins/token/util.lib.lua

Co-authored-by: bgrozev <boris@jitsi.org>

---------

Co-authored-by: bgrozev <boris@jitsi.org>
2025-09-29 08:22:31 -05:00
xinfei.wu
23c831e9b0 fix: check if asapKeyServer is empty string 2025-09-27 08:27:29 -05:00
Дамян Минков
e6fbeb9458 feat(conference): Process unauthenticated access disabled error. (#16465)
* feat(conference): Process unauthenticated access disabled error.

Shows notification with a button to login.

* squash: Fix texts.

* feat(visitors): Propagate and use allowUnauthenticatedAccess.

* squash: Avoids always sending a value, even when not set.

* squash: Rename error.

* squash: Fix comments.

* squash: Move check before log.
2025-09-26 14:05:19 -05:00
damencho
e15a59c994 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2095.0.0+43bbd502...v2097.0.0+58646fc3
2025-09-26 14:05:05 -05:00
70 changed files with 1846 additions and 1455 deletions

View File

@@ -11,24 +11,17 @@
"copyStream": "Copier le lien de diffusion en direct",
"countryNotSupported": "Nous ne prenons pas encore cette destination en charge.",
"countryReminder": "Vous appelez en dehors des É.-U.? Veuillez vous assurer de commencer par le code de pays!",
"defaultEmail": "Votre email par défaut",
"disabled": "Vous ne pouvez pas inviter d'autres personnes.",
"failedToAdd": "L'ajout de membres a échoué",
"footerText": "Les appels sont désactivés.",
"googleEmail": "Gmail",
"inviteMoreHeader": "Vous êtes seul(e) dans la réunion",
"inviteMoreMailSubject": "Rejoindre une réunion {{appName}}",
"inviteMorePrompt": "Inviter d'autres personnes",
"linkCopied": "Lien copié dans le presse-papiers",
"loading": "Rechercher des personnes et des numéros de téléphone",
"loadingNumber": "Validation du numéro de téléphone",
"loadingPeople": "Rechercher des personnes à inviter",
"noResults": "Aucun résultat de recherche correspondant",
"noValidNumbers": "Veuillez entrer un numéro de téléphone",
"outlookEmail": "Outlook",
"phoneNumbers": "Numéros de téléphone",
"searchNumbers": "Ajouter des numéros de téléphone",
"searchPeople": "Rechercher des personnes",
"searchPeopleAndNumbers": "Rechercher des personnes ou ajouter des numéros de téléphone",
"searching": "Recherche…",
"shareInvite": "Partager l'invitation à la réunion",
"shareLink": "Partager le lien de la réunion pour inviter d'autres personnes",
@@ -116,9 +109,12 @@
}
},
"chat": {
"disabled": "L'envoi de messages de chat est désactivé.",
"enter": "Entrez dans le salon",
"error": "Erreur : votre message n'a pas été envoyé. Raison : {{error}}",
"everyone": "Tout le monde",
"fieldPlaceHolder": "Tapez votre message ici",
"guestsChatIndicator": "(invité)",
"lobbyChatMessageTo": "Message de salle d'attente à {{recipient}}",
"message": "Message",
"messageAccessibleTitle": "{{user}} dit: ",
@@ -129,12 +125,26 @@
"nickname": {
"popover": "Choisissez un nom d'affichage",
"title": "Entrer un nom d'affichage pour utiliser le clavardage",
"titleWithPolls": "Entrer un nom d'affichage pour utiliser le clavardage"
"titleWithCC": "Entrez un pseudonyme pour utiliser le chat et les sous-titres",
"titleWithPolls": "Entrer un nom d'affichage pour utiliser le clavardage",
"titleWithPollsAndCC": "Entrez un pseudonyme pour utiliser le chat, les sondages et les sous-titres",
"titleWithPollsAndCCAndFileSharing": "Entrez un pseudonyme pour utiliser le chat, les sondages, les sous-titres et les fichiers"
},
"noMessagesMessage": "Il n'y a pas encore de messages dans cette réunion. Démarrez une conversation ici !",
"noMessagesMessage": "Il n'y a pas encore de messages dans cette réunion. Démarrez une conversation ici!",
"privateNotice": "Message privé à {{recipient}}",
"sendButton": "Envoyer",
"smileysPanel": "Panneaux des Émojis",
"systemDisplayName": "Système",
"tabs": {
"chat": "Chat",
"closedCaptions": "ST",
"fileSharing": "Fichiers",
"polls": "Sondages"
},
"title": "Clavardage",
"titleWithCC": "ST",
"titleWithFeatures": "Chat et",
"titleWithFileSharing": "Fichiers",
"titleWithPolls": "Clavardage",
"you": "vous"
},
@@ -145,6 +155,10 @@
"dontShowAgain": "Ne plus m'afficher ceci",
"installExtensionText": "Installer l'extension pour l'intégration de Google Calendar et Office 365"
},
"closedCaptionsTab": {
"emptyState": "Le contenu des sous-titres sera disponible quand un modérateur les aura démarrés",
"startClosedCaptionsButton": "Démarrer les sous-titres"
},
"connectingOverlay": {
"joiningRoom": "Connexion à la réunion en cours…"
},
@@ -161,8 +175,7 @@
"FETCH_SESSION_ID": "Obtention d'un identifiant de session…",
"GET_SESSION_ID_ERROR": "Obtenir une erreur d'identifiant de session: {{code}}",
"GOT_SESSION_ID": "Obtention d'un identifiant de session… Terminée",
"LOW_BANDWIDTH": "La vidéo de {{displayName}} a été coupée pour économiser de la bande passante",
"RECONNECTING": "Un problème de réseau est survenu. Reconnexion en cours…"
"LOW_BANDWIDTH": "La vidéo de {{displayName}} a été coupée pour économiser de la bande passante"
},
"connectionindicator": {
"address": "Adresse :",
@@ -183,6 +196,7 @@
"more": "Afficher plus",
"no": "non",
"packetloss": "Perte de paquet :",
"participant_id": "ID du participant:",
"quality": {
"good": "Bonne",
"inactive": "Inactive",
@@ -221,8 +235,9 @@
"joinInBrowser": "Rejoindre depuis le navigateur",
"launchMeetingLabel": "Comment voulez-vous rejoindre la réunion ?",
"launchWebButton": "Démarrer dans l'application Web",
"noDesktopApp": "Vous n'avez pas l'application ?",
"noMobileApp": "Vous n'avez pas l'application ?",
"openApp": "Continuer vers l'application",
"or": "OU",
"termsAndConditions": "En continuant, vous acceptez nos <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>conditions générales dutilisation.</a>",
"title": "Démarrage de votre réunion dans {{app}} en cours…",
"titleNew": "Démarrage de votre réunion…",
@@ -263,8 +278,9 @@
"Remove": "Supprimer",
"Share": "Partager",
"Submit": "Envoyer",
"WaitForHostMsg": "La conférence n'a pas encore démarré. Si vous êtes l'hôte, veuillez vous authentifier. Sinon, veuillez attendre que l'hôte arrive.",
"WaitingForHost": "En attente de l'hôte…",
"Understand": "Je comprends, gardez-moi en sourdine pour l'instant",
"UnderstandAndUnmute": "Je comprends, veuillez me réactiver s'il vous plaît",
"WaitForHostNoAuthMsg": "La réunion n'a pas encore commencé car aucun modérateur n'est encore arrivé. Veuillez patienter.",
"WaitingForHostButton": "Attendre l'hôte",
"WaitingForHostTitle": "En attente de l'hôte…",
"Yes": "Oui",
@@ -276,19 +292,27 @@
"sharingTabs": "Options de partage"
},
"add": "Ajouter",
"addMeetingNote": "Ajouter une note à cette conférence",
"addMeetingNote": "Ajouter une note à cette réunion",
"addOptionalNote": "Ajouter une note (optionnel):",
"allow": "Autoriser",
"allowToggleCameraDialog": "Autorisez-vous {{initiatorName}} à changer votre mode de caméra?",
"allowToggleCameraTitle": "Autoriser-vous le changement de mode de caméra?",
"alreadySharedVideoMsg": "Un autre membre partage déjà une vidéo. Cette conférence permet le partage d'une seule vidéo à la fois.",
"alreadySharedVideoMsg": "Un autre membre partage déjà une vidéo. Cette réunion permet le partage d'une seule vidéo à la fois.",
"alreadySharedVideoTitle": "Seulement une vidéo à la fois peut être partagée",
"applicationWindow": "Fenêtre d'application",
"authenticationRequired": "Authentification requise",
"cameraCaptureDialog": {
"description": "Prendre et envoyer une photo en utilisant votre caméra mobile",
"ok": "Ouvrir la caméra",
"reject": "Pas maintenant",
"title": "Prendre une photo"
},
"cameraConstraintFailedError": "Votre caméra ne répond pas à certaines exigences.",
"cameraNotFoundError": "Impossible de trouver la caméra.",
"cameraNotSendingData": "Il est impossible d'accéder à la caméra. Veuillez vérifier si une autre application utilise actuellement ce dispositif, sélectionner un autre dispositif à partir du menu des paramètres ou essayer de recharger l'application.",
"cameraNotSendingDataTitle": "Impossible d'accéder à la caméra",
"cameraPermissionDeniedError": "Vous n'avez pas reçu l'autorisation d'utiliser votre caméra. Vous pouvez toujours rejoindre la conférence, mais les autres membres ne pourront pas vous voir. Utilisez le bouton de caméra dans la barre d'adresse pour corriger cela.",
"cameraPermissionDeniedError": "Vous n'avez pas reçu l'autorisation d'utiliser votre caméra. Vous pouvez toujours rejoindre la réunion, mais les autres membres ne pourront pas vous voir. Utilisez le bouton de caméra dans la barre d'adresse pour corriger cela.",
"cameraTimeoutError": "Impossible de démarrer la source vidéo. Délai dépassé!",
"cameraUnknownError": "Impossible d'utiliser la caméra pour une raison inconnue.",
"cameraUnsupportedResolutionError": "Votre caméra ne prend pas en charge la résolution vidéo nécessaire.",
"close": "Fermer",
@@ -297,28 +321,29 @@
"conferenceReloadMsg": "Nous tentons de résoudre le problème. Reconnexion dans {{seconds}} sec…",
"conferenceReloadTitle": "Malheureusement, une erreur s'est produite.",
"confirm": "Confirmer",
"confirmBack": "Retour",
"confirmNo": "Non",
"confirmYes": "Oui",
"connectError": "Oups! Une erreur s'est produite. La connexion à la conférence a échouée.",
"connectErrorWithMsg": "Oups! Une erreur s'est produite. La connexion à la conférence a échoué : {{msg}}",
"connectError": "Oups! Une erreur s'est produite. La connexion à la réunion a échouée.",
"connectErrorWithMsg": "Oups! Une erreur s'est produite. La connexion à la réunion a échoué : {{msg}}",
"connecting": "Connexion en cours",
"contactSupport": "Communiquez avec le service de soutien",
"copied": "Copié",
"copy": "Copier",
"demoteParticipantDialog": "Êtes-vous sûr de vouloir déplacer ce participant en visiteur ?",
"demoteParticipantTitle": "Déplacer en visiteur",
"dismiss": "Rejeter",
"displayNameRequired": "Un nom d'affichage est requis",
"done": "Terminé",
"e2eeDescription": "Le chiffrement de bout en bout est actuellement expérimental. Veuillez garder en tête que l'activation du chiffrement de bout en bout désactivera les services fournis côté serveur tels que : l'enregistrement, la diffusion en direct et la participation par téléphone. Gardez également en tête que la réunion ne fonctionnera que pour les personnes qui se connectent à partir de navigateurs prenant en charge les flux insérables.",
"e2eeDisabledDueToMaxModeDescription": "Impossible d'activer le chiffrement de bout en bout en raison du trop grand nombre de participants à la conférence.",
"e2eeDisabledDueToMaxModeDescription": "Impossible d'activer le chiffrement de bout en bout en raison du trop grand nombre de participants à la réunion.",
"e2eeLabel": "Activer le chiffrement de Bout-en-Bout",
"e2eeWarning": "ATTENTION : Tous les participants de cette réunion ne semblent pas prendre en charge le chiffrement de bout en bout. Si vous activez le chiffrement, ils ne pourront ni vous voir, ni vous entendre.",
"e2eeWillDisableDueToMaxModeDescription": "ATTENTION: le chiffrement de bout en bout sera automatiquement arrêté si plus de participants joignent la conférence.",
"e2eeWillDisableDueToMaxModeDescription": "ATTENTION: le chiffrement de bout en bout sera automatiquement arrêté si plus de participants joignent la réunion.",
"embedMeeting": "Intégrer la réunion",
"enterDisplayName": "Veuillez saisir votre nom d'affichage",
"error": "Erreur",
"externalInstallationMsg": "Vous devez installer notre extension de partage de bureau.",
"externalInstallationTitle": "Extension requise",
"goToStore": "Rendez-vous sur notre boutique en ligne",
"errorRoomCreationRestriction": "Vous avez essayé de rejoindre trop rapidement, veuillez revenir dans un moment.",
"gracefulShutdown": "Notre service est actuellement hors service pour l'entretien. Veuillez réessayer plus tard.",
"grantModeratorDialog": "Êtes-vous sûr de vouloir rendre ce participant modérateur ?",
"grantModeratorTitle": "Nommer modérateur",
@@ -326,57 +351,65 @@
"hideShareAudioHelper": "Ne plus afficher ce dialogue",
"incorrectPassword": "Nom d'utilisateur ou mot de passe incorrect",
"incorrectRoomLockPassword": "Mot de passe incorrect",
"inlineInstallExtension": "Installer maintenant",
"inlineInstallationMsg": "Vous devez installer notre extension de partage de bureau.",
"internalError": "Oups! Une erreur s'est produite. L'erreur suivante est survenue : {{error}}",
"internalErrorTitle": "Erreur interne.",
"kickMessage": "Aïe! Vous avez été expulsé de la réunion!",
"kickParticipantButton": "Expulser",
"kickParticipantDialog": "Êtes-vous certain de vouloir expulser ce participant?",
"kickParticipantTitle": "Expulser ce membre?",
"kickSystemTitle": "Oups ! Vous avez été expulsé de la réunion",
"kickTitle": "Expulsé de la réunion",
"linkMeeting": "Relier la conférence",
"linkMeetingTitle": "Relier la conférence à Salesforce",
"learnMore": "en savoir plus",
"linkMeeting": "Relier la réunion",
"linkMeetingTitle": "Relier la réunion à Salesforce",
"liveStreaming": "Diffusion en direct",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossible durant l'enregistrement",
"liveStreamingDisabledForGuestTooltip": "Les invités ne peuvent pas démarrer la diffusion en direct.",
"liveStreamingDisabledTooltip": "Démarrage de la diffusion en direct désactivé.",
"localUserControls": "Contrôles de l'utilisateur local",
"lockMessage": "Échec du verrouillage de la conférence.",
"lockMessage": "Échec du verrouillage de la réunion.",
"lockRoom": "Ajouter un mot de passe à la réunion",
"lockTitle": "Échec du verrouillage",
"login": "Connexion",
"loginQuestion": "Voulez-vous vraiment vous connecter et quitter la conférence?",
"logoutQuestion": "Êtes-vous certain de vouloir vous déconnecter et arrêter la conférence?",
"loginQuestion": "Voulez-vous vraiment vous connecter et quitter la réunion?",
"logoutQuestion": "Êtes-vous certain de vouloir vous déconnecter et arrêter la réunion?",
"logoutTitle": "Déconnexion",
"maxUsersLimitReached": "La limite du nombre maximum de membres a été atteinte. La conférence est pleine. Veuillez communiquer avec l'hôte de la réunion ou réessayer plus tard.",
"maxUsersLimitReached": "La limite du nombre maximum de membres a été atteinte. La réunion est pleine. Veuillez communiquer avec l'hôte de la réunion ou réessayer plus tard.",
"maxUsersLimitReachedTitle": "Limite du nombre de membres maximum atteinte",
"micConstraintFailedError": "Votre micro ne répond pas à certaines exigences",
"micNotFoundError": "Impossible de trouver le micro.",
"micNotSendingData": "Impossible d'accéder à votre micro. Veuillez sélectionner un autre dispositif à partir du menu des paramètres ou essayer de recharger l'application.",
"micNotSendingDataTitle": "Impossible d'accéder à votre micro",
"micPermissionDeniedError": "Vous n'avez pas accordé l'autorisation d'utilisation de votre micro. Vous pouvez toujours rejoindre la conférence, mais les autres membres ne pourront pas vous entendre. Utilisez le bouton de caméra dans la barre d'adresse pour remédier à cela.",
"micPermissionDeniedError": "Vous n'avez pas accordé l'autorisation d'utilisation de votre micro. Vous pouvez toujours rejoindre la réunion, mais les autres membres ne pourront pas vous entendre. Utilisez le bouton de caméra dans la barre d'adresse pour remédier à cela.",
"micTimeoutError": "Impossible de démarrer la source audio. Délai dépassé!",
"micUnknownError": "Impossible d'utiliser le micro pour une raison inconnue.",
"moderationAudioLabel": "Autoriser les participants à réactiver leur micro",
"moderationDesktopLabel": "Autoriser les non-modérateurs à partager leur écran",
"moderationVideoLabel": "Autoriser les participants à démarrer leur vidéo",
"muteEveryoneDialog": "Êtes-vous sûr de vouloir couper les micros de tout le monde? Vous ne pourrez plus réactiver leur micro, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneDialogModerationOn": "Les participants peuvent demander à parler n'importe quand",
"muteEveryoneElseDialog": "Une fois leur micro coupé, vous ne pourrez plus le réactiver, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneElseTitle": "Couper le micro de tout le monde sauf de {{whom}}?",
"muteEveryoneElsesDesktopDialog": "Une fois le partage arrêté, vous ne pourrez pas le redémarrer, mais ils peuvent le faire à tout moment.",
"muteEveryoneElsesDesktopTitle": "Arrêter le partage d'écran de tout le monde sauf {{whom}} ?",
"muteEveryoneElsesVideoDialog": "Une fois la caméra coupée, vous ne pourrez plus la rallumer, mais ils peuvent la rallumer à tout moment.",
"muteEveryoneElsesVideoTitle": "Couper la vidéo de tout le monde sauf {{whom}}?",
"muteEveryoneSelf": "vous",
"muteEveryoneStartMuted": "Tout le monde démarre avec le micro coupé",
"muteEveryoneTitle": "Couper le micro de tout le monde ?",
"muteEveryonesDesktopDialog": "Les participants peuvent partager leur écran à tout moment.",
"muteEveryonesDesktopDialogModerationOn": "Les participants peuvent envoyer une demande pour partager leur écran à tout moment.",
"muteEveryonesDesktopTitle": "Arrêter le partage d'écran de tout le monde ?",
"muteEveryonesVideoDialog": "Êtes-vous sûr de vouloir couper la caméra de tout le monde? Vous ne pourrez pas la réactiver, mais ils peuvent la remettre à tout moment.",
"muteEveryonesVideoDialogModerationOn": "Les participants peuvent demander à activer leur caméra n'importe quand.",
"muteEveryonesVideoDialogOk": "Désactiver",
"muteEveryonesVideoTitle": "Couper la caméra de tout le monde?",
"muteParticipantBody": "Vous ne pourrez pas réactiver leur micro, mais ils peuvent le réactiver eux-mêmes à tout moment.",
"muteParticipantButton": "Discrétion",
"muteParticipantDialog": "Êtes-vous certain de vouloir désactiver le micro de ce participant? Vous ne pourrez pas le réactiver, mais il peut le réactiver lui-même à tout moment.",
"muteParticipantTitle": "Désactiver le micro de ce membre?",
"muteParticipantsDesktopBody": "Vous ne pourrez pas démarrer leur partage d'écran, mais ils peuvent le faire à tout moment.",
"muteParticipantsDesktopBodyModerationOn": "Vous ne pourrez pas démarrer leur partage d'écran et eux non plus.",
"muteParticipantsDesktopButton": "Arrêter le partage d'écran",
"muteParticipantsDesktopDialog": "Êtes-vous sûr de vouloir désactiver le partage d'écran de ce participant ? Vous ne pourrez pas le redémarrer, mais ils peuvent le faire à tout moment.",
"muteParticipantsDesktopDialogModerationOn": "Êtes-vous sûr de vouloir désactiver le partage d'écran de ce participant ? Vous ne pourrez pas réactiver l'écran et eux non plus.",
"muteParticipantsDesktopTitle": "Désactiver le partage d'écran de ce participant ?",
"muteParticipantsVideoBody": "Vous ne pourrez pas rallumer la caméra, mais ils peuvent la rallumer à tout moment.",
"muteParticipantsVideoBodyModerationOn": "Ni vous ni le participant ne pourront rallumer la caméra.",
"muteParticipantsVideoButton": "Couper la caméra",
@@ -392,14 +425,14 @@
"permissionCameraRequiredError": "L'autorisation caméra est nécessaire pour participer aux réunions avec vidéo. Merci de l'accorder dans les paramètres",
"permissionErrorTitle": "Permission nécessaire",
"permissionMicRequiredError": "L'autorisation microphone est nécessaire pour participer aux réunions avec son. Merci de l'accorder dans les paramètres",
"popupError": "Votre navigateur bloque les fenêtres surgissantes provenant de ce site. Veuillez activer les fenêtres surgissantes dans les paramètres de sécurité de votre navigateur et réessayer.",
"popupErrorTitle": "Fenêtre surgissante bloquée",
"readMore": "plus",
"recentlyUsedObjects": "Vos objets récemment utilisés",
"recording": "Enregistrement",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible durant le direct",
"recordingDisabledForGuestTooltip": "Les invités ne peuvent pas démarrer l'enregistrement.",
"recordingDisabledTooltip": "Démarrage de l'enregistrement désactivé.",
"recordingInProgressDescription": "Cette réunion est en cours d'enregistrement et d'analyse par IA{{learnMore}}. Votre audio et vidéo ont été coupés. Si vous choisissez de vous réactiver, vous consentez à être enregistré.",
"recordingInProgressDescriptionFirstHalf": "Cette réunion est en cours d'enregistrement et d'analyse par IA",
"recordingInProgressDescriptionSecondHalf": ". Votre audio et vidéo ont été coupés. Si vous choisissez de vous réactiver, vous consentez à être enregistré.",
"recordingInProgressTitle": "Enregistrement en cours",
"rejoinNow": "Rejoindre maintenant",
"remoteControlAllowedMessage": "{{user}} a accepté votre demande de contrôle à distance!",
"remoteControlDeniedMessage": "{{user}} a refusé votre demande de contrôle à distance!",
@@ -408,6 +441,7 @@
"remoteControlShareScreenWarning": "Notez que si vous appuyez sur « Permettre », vous partagerez votre écran!",
"remoteControlStopMessage": "La séance de contrôle à distance est terminée!",
"remoteControlTitle": "Contrôle du bureau à distance",
"remoteUserControls": "Contrôles de l'utilisateur distant {{username}}",
"removePassword": "Supprimer un mot de passe",
"removeSharedVideoMsg": "Êtes-vous certain de vouloir supprimer votre vidéo partagée?",
"removeSharedVideoTitle": "Supprimer la vidéo partagée",
@@ -419,10 +453,6 @@
"screenSharingAudio": "Partager l'audio",
"screenSharingFailed": "Oups! Quelque chose s'est mal passé, nous n'avons pas pu démarrer le partage d'écran!",
"screenSharingFailedTitle": "Echec du partage d'écran!",
"screenSharingFailedToInstall": "Oups! L'installation de votre extension de partage d'écran a échouée.",
"screenSharingFailedToInstallTitle": "L'installation de l'extension de partage d'écran a échouée",
"screenSharingFirefoxPermissionDeniedError": "Une erreur s'est produite lors de la tentative de partage d'écran. Veuillez vous assurer d'avoir donné votre autorisation.",
"screenSharingFirefoxPermissionDeniedTitle": "Oups! Il est impossible de démarrer le partage d'écran!",
"screenSharingPermissionDeniedError": "Oups! Une erreur s'est produite avec les autorisations de l'extension de partage d'écran. Veuillez recharger et réessayer.",
"searchInSalesforce": "Rechercher dans Salesforce",
"searchResults": "Résultats de recherche ({{count}})",
@@ -450,11 +480,13 @@
"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 fournir un lien correct.",
"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é.",
"shareYourScreenDisabledForGuest": "Les invités ne peuvent pas partager leur écran.",
"sharedVideoDialogError": "Erreur: URL invalide",
"sharedVideoLinkPlaceholder": "lien YouTube ou lien vidéo direct",
"show": "Afficher",
@@ -512,7 +544,7 @@
"title": "Document partagé"
},
"e2ee": {
"labelToolTip": "Le son et la vidéo de cette conférence sont chiffrés de bout en bout"
"labelToolTip": "Le son et la vidéo de cette réunion sont chiffrés de bout en bout"
},
"embedMeeting": {
"title": "Intégrer cette réunion"
@@ -525,10 +557,28 @@
"bad": "Mauvaise",
"detailsLabel": "Dites-nous en plus.",
"good": "Bonne",
"rateExperience": "Évaluez votre expérience de cette conférence",
"rateExperience": "Évaluez votre expérience de cette réunion",
"star": "Étoile",
"veryBad": "Très mauvaise",
"veryGood": "Très bonne"
},
"fileSharing": {
"downloadFailedDescription": "Veuillez réessayer.",
"downloadFailedTitle": "Échec du téléchargement",
"downloadFile": "Télécharger",
"downloadStarted": "Téléchargement de fichier démarré",
"dragAndDrop": "Glisser-déposer des fichiers ici ou n'importe où à l'écran",
"fileAlreadyUploaded": "Ce fichier a déjà été téléversé dans cette réunion.",
"fileTooLargeDescription": "Veuillez vous assurer que le fichier ne dépasse pas {{ maxFileSize }}.",
"fileTooLargeTitle": "Le fichier choisi est trop volumineux",
"fileUploadProgress": "Progression du téléchargement de fichier",
"fileUploadedSuccessfully": "Fichier téléversé avec succès",
"removeFile": "Supprimer",
"removeFileSuccess": "Fichier supprimé avec succès",
"uploadFailedDescription": "Veuillez réessayer.",
"uploadFailedTitle": "Échec du téléchargement",
"uploadFile": "Partager un fichier"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Vignettes vidéos"
@@ -576,6 +626,7 @@
"noNumbers": "Aucun numéro d'appel trouvé",
"noPassword": "Aucun",
"noRoom": "Vous n'avez pas précisé de salle pour l'appel interne.",
"noWhiteboard": "Impossible de charger le tableau blanc.",
"numbers": "Numéros d'appel",
"password": "Mot de passe:",
"reachedLimit": "Vous avez atteint la limite de votre abonnement.",
@@ -583,7 +634,8 @@
"sipAudioOnly": "Adresse SIP en audio uniquement",
"title": "Partager",
"tooltip": "Lien de partage et informations d'appel interne pour cette réunion",
"upgradeOptions": "Veuillez vérifier les options de mise à niveau"
"upgradeOptions": "Veuillez vérifier les options de mise à niveau",
"whiteboardError": "Erreur de chargement du tableau blanc. Veuillez réessayer plus tard."
},
"inlineDialogFailure": {
"msg": "Nous avons rencontré un obstacle.",
@@ -613,10 +665,10 @@
"showSpeakerStats": "Afficher les statistiques d'intervenant",
"toggleChat": "Ouvrir ou fermer le clavardage",
"toggleFilmstrip": "Afficher ou masquer les icônes vidéos",
"toggleParticipantsPane": "Afficher ou masquer le volet des participants",
"toggleScreensharing": "Basculer entre la caméra et le partage d'écran",
"toggleShortcuts": "Afficher ou masquer les raccourcis clavier",
"videoMute": "Démarrer ou arrêter votre caméra",
"videoQuality": "Gérer la qualité d'appel"
"videoMute": "Démarrer ou arrêter votre caméra"
},
"largeVideo": {
"screenIsShared": "Vous êtes en train de partager votre écran",
@@ -647,6 +699,7 @@
"on": "Diffusion en direct",
"onBy": "{{name}} démarré la diffusion en continu",
"pending": "Démarrage de la diffusion en 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",
@@ -694,7 +747,8 @@
"notificationTitle": "Salle d'attente",
"passwordJoinButton": "Rejoindre",
"title": "Salle d'attente",
"toggleLabel": "Activer la salle d'attente"
"toggleLabel": "Activer la salle d'attente",
"waitForModerator": "La réunion n'a pas encore commencé car aucun modérateur n'est encore arrivé. Si vous souhaitez devenir modérateur, veuillez vous connecter. Sinon, veuillez attendre."
},
"localRecording": {
"clientState": {
@@ -737,27 +791,37 @@
"me": "moi",
"notify": {
"OldElectronAPPTitle": "Faille de sécurité !",
"allowAction": "Permettre",
"allowAll": "Tout autoriser",
"allowAudio": "Autoriser l'audio",
"allowDesktop": "Autoriser le partage d'écran",
"allowVideo": "Autoriser la vidéo",
"allowedUnmute": "Vous pouvez réactiver votre écran, votre caméra ou partager votre écran.",
"audioUnmuteBlockedDescription": "Le rétablissement du son a été bloqué temporairement en raison de limites système.",
"audioUnmuteBlockedTitle": "Rétablissement du son bloqué!",
"chatMessages": "Messages de chat",
"connectedOneMember": "{{name}} a rejoint la réunion",
"connectedThreePlusMembers": "{{name}} et {{count}} autres ont rejoint la réunion",
"connectedTwoMembers": "{{first}} et {{second}} ont rejoint la réunion",
"connectedOneMember": "{{name}} a rerejoint la réunion",
"connectedThreePlusMembers": "{{name}} et {{count}} autres ont rerejoint la réunion",
"connectedTwoMembers": "{{first}} et {{second}} ont rerejoint 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.",
"dataChannelClosedWithAudio": "La qualité de l'audio et de la vidéo peut être altérée",
"desktopMutedRemotelyTitle": "Votre partage d'écran a été arrêté par {{participantDisplayName}}",
"disabledIframe": "L'intégration Iframe est uniquement destinée à des démos, cet appel se terminera dans {{timeout}} minutes.",
"disabledIframeSecondary": "L'intégration Iframe de {{domaine}} est uniquement destinée à des démos, cet appel se terminera dans {{timeout}} minutes.",
"disabledIframeSecondaryNative": "L'intégration de {{domain}} est uniquement destinée aux fins de démonstration, cet appel se terminera dans {{timeout}} minutes.",
"disabledIframeSecondaryWeb": "L'intégration de {{domain}} est uniquement destinée aux fins de démonstration, cet appel se terminera dans {{timeout}} minutes. Veuillez utiliser <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> pour l'intégration en production !",
"disconnected": "déconnecté",
"displayNotifications": "Afficher les notifications pour",
"dontRemindMe": "Ne pas me le rappeler",
"focus": "Sujet de la conférence",
"focus": "Sujet de la réunion",
"focusFail": "{{component}} non disponible; réessayez dans {{ms}} sec",
"gifsMenu": "GIPHY",
"grantedTo": "Droits de modérateur accordés à {{to}}!",
"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": "{{displayName}} a été invité",
"invitedThreePlusMembers": "{{name}} et {{count}} autres ont été invités",
"invitedTwoMembers": "{{first}} et {{second}} ont été invités",
@@ -767,11 +831,11 @@
"leftThreePlusMembers": "{{name}} et beaucoup d'autres ont quitté la réunion",
"leftTwoMembers": "{{first}} et {{second}} ont quitté la réunion",
"linkToSalesforce": "Lien à Salesforce",
"linkToSalesforceDescription": "Vous pouvez lier le résumé de la conférence à un objet Salesforce.",
"linkToSalesforceError": "Impossible de relier la conférence à Salesforce",
"linkToSalesforceKey": "Relier cette conférence",
"linkToSalesforceProgress": "Liaison de la conférence à Salesforce…",
"linkToSalesforceSuccess": "La conférence a été reliée à Salesforce",
"linkToSalesforceDescription": "Vous pouvez lier le résumé de la réunion à un objet Salesforce.",
"linkToSalesforceError": "Impossible de relier la réunion à Salesforce",
"linkToSalesforceKey": "Relier cette réunion",
"linkToSalesforceProgress": "Liaison de la réunion à Salesforce…",
"linkToSalesforceSuccess": "La réunion a été reliée à Salesforce",
"localRecordingStarted": "{{name}} a commencé un enregistrement local.",
"localRecordingStopped": "{{name}} a arrêté un enregistrement local.",
"me": "Moi",
@@ -794,18 +858,21 @@
"newDeviceAction": "Utiliser",
"newDeviceAudioTitle": "Nouveau dispositif 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.",
"oldElectronClientDescription1": "Vous semblez utiliser une ancienne version du client Jitsi Meet qui présente des failles de sécurité connues. Veuillez vous assurer de mettre à jour vers notre ",
"oldElectronClientDescription2": "dernière build",
"oldElectronClientDescription3": " rapidement !",
"openChat": "Ouvrir le chat",
"participantWantsToJoin": "souhaite rejoindre la réunion",
"participantsWantToJoin": "souhaitent rejoindre la réunion",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) supprimé par un autre participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) défini par un autre participant",
"raiseHandAction": "Lever la main",
"raisedHand": "{{name}} voudrait parler.",
"raisedHands": "{{participantName}} et {{raisedHands}} autres personnes",
"reactionSounds": "Bloquer les réactions sonores",
"reactionSoundsForAll": "Bloquer les réactions sonores pour tous",
"screenShareNoAudio": " La case Partager l'audio n'a pas été cochée dans l'écran de sélection de la fenêtre.",
@@ -818,13 +885,22 @@
"startSilentTitle": "Vous avez rejoint sans sortie audio!",
"suboptimalBrowserWarning": "Nous craignons que votre expérience de réunion en ligne ne soit bonne ici. Nous cherchons des moyens d'améliorer cela, mais d'ici-là, essayez d'utiliser l'un des <a href='{{recommendedBrowserPageLink}}' target='_blank'>navigateurs supportés</a>.",
"suboptimalExperienceTitle": "Avertissement de navigateur",
"suggestRecordingAction": "Démarrer",
"suggestRecordingDescription": "Souhaitez-vous démarrer un enregistrement ?",
"suggestRecordingTitle": "Enregistrer cette réunion",
"unmute": "Rétablir le son",
"unmuteScreen": "Démarrer le partage d'écran",
"unmuteVideo": "Réactiver la vidéo",
"videoMutedRemotelyDescription": "Vous pouvez toujours la réactiver.",
"videoMutedRemotelyTitle": "Votre caméra a été coupée par {{participantDisplayName}}!",
"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}}",
"waitingVisitorsTitle": "La réunion n'est pas encore en direct !",
"whiteboardLimitDescription": "Veuillez sauvegarder votre progression, car la limite d'utilisation du tableau blanc sera bientôt atteinte et celui-ci sera fermé.",
"whiteboardLimitTitle": "Utiilisation du tableau blanc"
},
@@ -833,12 +909,18 @@
"admit": "Accepter",
"admitAll": "Tout accepter",
"allow": "Autoriser les participants à:",
"allowDesktop": "Autoriser le partage d'écran",
"allowVideo": "permettre la vidéo",
"askDesktop": "Demander de partager l'écran",
"askUnmute": "Demander de réactiver le micro",
"audioModeration": "Rouvrir leur micro",
"blockEveryoneMicCamera": "Bloquer tous les micros et caméras",
"breakoutRooms": "Salles annexes",
"desktopModeration": "Démarrer le partage d'écran",
"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",
@@ -846,6 +928,8 @@
"muteAll": "Couper le micro de tout le monde",
"muteEveryoneElse": "Couper le micro de tous les autres",
"reject": "Refuser",
"stopDesktop": "Arrêter le partage d'écran",
"stopEveryonesDesktop": "Arrêter le partage d'écran de tout le monde",
"stopEveryonesVideo": "Couper toutes les caméras",
"stopVideo": "Couper la vidéo",
"unblockEveryoneMicCamera": "Débloquer tous les micros et caméras",
@@ -855,11 +939,15 @@
"headings": {
"lobby": "Salle d'attente ({{count}})",
"participantsList": "Participants de la réunion ({{count}})",
"viewerRequests": "Demandes des spectateurs {{count}}",
"visitorInQueue": " (en attente {{count}})",
"visitorRequests": "(Demande {{count}} )",
"visitors": "Visiteurs {{count}}",
"visitorsList": "Spectateurs ({{count}})",
"waitingLobby": "Dans la salle d'attente ({{count}})"
},
"search": "Rechercher des participants",
"searchDescription": "Commencez à taper pour filtrer les participants",
"title": "Participants"
},
"passwordDigitsOnly": "Jusqu'à {{number}} chiffres",
@@ -868,10 +956,13 @@
"pinnedParticipant": "Participant toujours affiché",
"polls": {
"answer": {
"edit": "Modifier",
"send": "Envoyer",
"skip": "Passer",
"submit": "Envoyer"
},
"by": "Par {{ name }}",
"closeButton": "Fermer le sondage",
"create": {
"addOption": "Ajouter une option",
"answerPlaceholder": "Option {{index}}",
@@ -881,6 +972,7 @@
"pollQuestion": "Question du sondage",
"questionPlaceholder": "Poser une question",
"removeOption": "Supprimer l'option",
"save": "Enregistrer",
"send": "Envoyer"
},
"errors": {
@@ -910,9 +1002,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é.",
@@ -921,6 +1015,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.",
@@ -937,7 +1032,7 @@
"errorDialOutDisconnected": "Impossible de composer le numéro. Déconnecté",
"errorDialOutFailed": "Impossible de composer le numéro. L'appel a échoué",
"errorDialOutStatus": "Erreur lors de l'obtention de l'état d'appel sortant",
"errorMissingName": "Veuillez entrer votre nom pour entrer en conférence",
"errorMissingName": "Veuillez entrer votre nom pour entrer en réunion",
"errorNoPermissions": "Vous devez permettre l'accès microphone et caméra",
"errorStatusCode": "Erreur de numérotation, code d'état: {{status}}",
"errorValidation": "La validation du numéro a échoué",
@@ -953,6 +1048,7 @@
"or": "ou",
"premeeting": "Pré-séance",
"proceedAnyway": "Continuer quand même",
"recordingWarning": "D'autres participants peuvent enregistrer cet appel",
"screenSharingError": "Erreur de partage d'écran:",
"startWithPhone": "Commencez avec l'audio du téléphone",
"unsafeRoomConsent": "Je comprends les risques et je veux quand même rejoindre cette réunion",
@@ -1018,7 +1114,6 @@
"limitNotificationDescriptionNative": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <3> {{app}} </3>.",
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
"linkGenerated": "Nous avons généré un lien à votre enregistrement.",
"live": "EN DIRECT",
"localRecordingNoNotificationWarning": "Le démarrage de lenregistrement ne sera pas annoncé aux autres participants. Vous devrez les informer par vous-même que la réunion sera enregistrée.",
"localRecordingNoVideo": "La vidéo n'est pas en cours denregistrement",
"localRecordingStartWarning": "Assurez-vous darrêter lenregistrement vidéo avant de quitter la réunion afin de pouvoir le sauvegarder.",
@@ -1035,13 +1130,13 @@
"onBy": "{{name}} a démarré l'enregistrement",
"onlyRecordSelf": "Enregistrer seulement mon audio et ma vidéo.",
"pending": "Enregistrement de la réunion en préparation…",
"rec": "REC",
"policyError": "Vous avez essayé de démarrer un enregistrement trop rapidement. Veuillez réessayer plus tard !",
"recordAudioAndVideo": "Enregistrer l'audio et la vidéo",
"recordTranscription": "Enregistrer la transcription",
"saveLocalRecording": "Sauvegarder lenregistrement local (Beta)",
"serviceDescription": "Votre enregistrement sera sauvegardé par le service d'enregistrement",
"serviceDescriptionCloud": "Enregistrement Cloud",
"serviceDescriptionCloudInfo": "Les conférences enregistrées sont automatiquement supprimées 24h après leur heure d'enregistrement.",
"serviceDescriptionCloudInfo": "Les réunions enregistrées sont automatiquement supprimées 24h après leur heure d'enregistrement.",
"serviceName": "Service d'enregistrement",
"sessionAlreadyActive": "Cette session est déjà en cours d'enregistrement ou de diffusion.",
"showAdvancedOptions": "Afficher les options avancées",
@@ -1057,6 +1152,18 @@
"sectionList": {
"pullToRefresh": "Tirer pour rafraîchir"
},
"security": {
"about": "Vous pouvez ajouter un mot de passe à votre réunion. Les participants devront fournir le mot de passe avant de pouvoir rejoindre la réunion.",
"aboutReadOnly": "Les modérateurs peuvent ajouter un mot de passe à la réunion. Les participants devront fournir le mot de passe avant de pouvoir rejoindre la réunion.",
"insecureRoomNameWarningNative": "Le nom de la réunion nest pas sûr. Des participants non voulus pourraient rejoindre cette réunion. {{recommendAction}} En apprendre plus sur la sécurisation des réunions.",
"insecureRoomNameWarningWeb": "Le nom de la réunion nest pas sûr. Des participants non voulus pourraient rejoindre cette réunion. {{recommendAction}} En apprendre plus sur la sécurisation des réunions <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">here</a>.",
"title": "Options de sécurité",
"unsafeRoomActions": {
"meeting": "Envisagez de sécuriser votre réunion en utilisant le bouton options de sécurité.",
"prejoin": "Envisagez d'utiliser un nom plus unique",
"welcome": "Envisagez d'utiliser un nom plus unique ou choisissez en un parmi ceux suggérés"
}
},
"settings": {
"audio": "Audio",
"buttonLabel": "Paramètres",
@@ -1067,11 +1174,13 @@
"signedIn": "Accès aux événements de votre agenda en cours pour {{email}}. Cliquez sur le bouton de déconnexion ci-dessous pour terminer l'accès aux événements d'agenda.",
"title": "Calendrier"
},
"chatWithPermissions": "Le chat nécessite une autorisation",
"desktopShareFramerate": "Images par seconde pour le Partage d'écran",
"desktopShareHighFpsWarning": "Augmenter le nombre d'images par seconde pour le partage d'écran peut impacter votre bande passante. Vous devez repartager l'écran pour que ces paramètres soient utilisés.",
"desktopShareWarning": "Vous devez repartager l'écran pour que ces paramètres soient utilisés.",
"devices": "Dispositifs",
"followMe": "Tous les participants me suivent",
"followMeRecorder": "L'enregistreur me suit",
"framesPerSecond": "images par seconde",
"incomingMessage": "un message arrive",
"language": "Langue",
@@ -1095,6 +1204,7 @@
"selectMic": "Micro",
"selfView": "Affichage de votre propre vidéo",
"shortcuts": "Raccourcis",
"showSubtitlesOnStage": "Afficher les sous-titres sur scène",
"speakers": "Haut-parleurs",
"startAudioMuted": "Tous les participants débutent en sourdine",
"startReactionsMuted": "Tout le monde commence avec les réactions sonores bloquées",
@@ -1111,7 +1221,7 @@
"alertURLText": "L'URL de serveur saisi n'est pas valide",
"apply": "Appliquer",
"buildInfoSection": "Information de version",
"conferenceSection": "Conférence",
"conferenceSection": "Réunion",
"disableCallIntegration": "Désactiver l'intégration d'appels native",
"disableCrashReporting": "Désactiver les rapports de plantage",
"disableCrashReportingWarning": "Etes-vous certain de vouloir désactiver les rapports de plantage ? Le paramètre sera effectif après le redémarrage de l'application.",
@@ -1148,11 +1258,13 @@
"fearful": "Effrayé",
"happy": "Content",
"hours": "{{count}} h",
"labelTooltip": "Nombre de participants : {{count}}",
"minutes": "{{count}} min",
"name": "Nom",
"neutral": "Neutre",
"sad": "Triste",
"search": "Recherche",
"searchDescription": "Commencez à taper pour filtrer les participants",
"searchHint": "Recherche des participants",
"seconds": "{{count}} s",
"speakerStats": "Statistiques d'intervenant",
@@ -1160,7 +1272,7 @@
"surprised": "Surpris"
},
"startupoverlay": {
"genericTitle": "La conférence a besoin d'utiliser votre microphone et votre caméra.",
"genericTitle": "La réunion a besoin d'utiliser votre microphone et votre caméra.",
"policyText": " ",
"title": "{{app}} doit utiliser votre micro et votre caméra."
},
@@ -1189,6 +1301,7 @@
"closeChat": "Fermer la discussion instantanée",
"closeMoreActions": "Fermer le menu plus d'actions",
"closeParticipantsPane": "Fermer le panneau des participants",
"closedCaptions": "Sous-titres",
"collapse": "Plier",
"document": "Basculement du document partagé",
"documentClose": "Fermer le document partagé",
@@ -1218,6 +1331,7 @@
"lobbyButton": "Activer / Désactiver le mode salle d'attente",
"localRecording": "Basculement des commandes d'enregistrement local",
"lockRoom": "Basculement du mot de passe de la réunion",
"love": "Cœur",
"lowerHand": "Baisser la main",
"moreActions": "Basculement du menu d'actions supplémentaires",
"moreActionsMenu": "Menu d'actions supplémentaires",
@@ -1235,6 +1349,7 @@
"privateMessage": "",
"profile": "Modifier votre profil",
"raiseHand": "Basculement de la main levée",
"react": "Réactions aux messages",
"reactions": "Réactions",
"reactionsMenu": "Ouvrir / fermer le menu réactions",
"recording": "Basculement de l'enregistrement",
@@ -1265,6 +1380,20 @@
"videounmute": "Démarrer la vidéo"
},
"addPeople": "Ajouter des personnes à votre appel",
"advancedAudioSettings": {
"aec": {
"label": "Suppression d'écho acoustique"
},
"agc": {
"label": "Contrôle automatique du gain"
},
"ns": {
"label": "Suppression de bruit"
},
"stereo": {
"label": "Stéréo"
}
},
"audioOnlyOff": "Désactiver le mode bande passante faible",
"audioOnlyOn": "Activer le mode bande passante faible",
"audioRoute": "Sélectionner le dispositif audio",
@@ -1277,6 +1406,7 @@
"closeChat": "Fermer le clavardage",
"closeParticipantsPane": "Fermer le panneau des participants",
"closeReactionsMenu": "Fermer le menu réactions",
"closedCaptions": "Sous-titres",
"disableNoiseSuppression": "Arrêter la suppression du bruit",
"disableReactionSounds": "Vous pouvez interdire les réactions sonores à cette réunion",
"documentClose": "Fermer le document partagé",
@@ -1294,6 +1424,7 @@
"giphy": "Activer/désactiver le menu GIPHY",
"hangup": "Quitter",
"help": "Aide",
"hideWhiteboard": "Masquer le tableau blanc",
"invite": "Inviter des personnes",
"joinBreakoutRoom": "Rejoindre salle annexe",
"laugh": "Rire",
@@ -1305,6 +1436,7 @@
"lobbyButtonEnable": "Activer le mode salle d'attente / contrôle des participant(e)s",
"login": "Connexion",
"logout": "Déconnexion",
"love": "Cœur",
"lowerYourHand": "Abaisser votre main",
"moreActions": "Plus d'actions",
"moreOptions": "Plus d'options",
@@ -1330,14 +1462,17 @@
"raiseYourHand": "Lever votre 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",
"reactionLove": "Envoyer une réaction d'amour",
"reactionSilence": "Envoyer réaction silence",
"reactionSurprised": "Envoyer réaction surprise",
"reactions": "Reactions",
"security": "Options de sécurité",
"selectBackground": "Sélectionner un arrière-plan",
"shareRoom": "Inviter quelqu'un",
"shareaudio": "Partager l'audio",
"sharedvideo": "Partager une vidéo",
"shortcuts": "Voir les raccourcis",
"showWhiteboard": "Afficher le tableau blanc",
@@ -1345,12 +1480,10 @@
"speakerStats": "Statistiques d'intervenant",
"startScreenSharing": "Démarrer le partage d'écran",
"startSubtitles": "Activer les sous-titres",
"startvideoblur": "Brouiller mon arrière plan",
"stopAudioSharing": "Arrêter le partage son",
"stopScreenSharing": "Arrêter le partage d'écran",
"stopSharedVideo": "Arrêter la vidéo",
"stopSubtitles": "Désactiver les sous-titres",
"stopvideoblur": "Désactiver le brouillage d'arrière-plan",
"surprised": "Surpris",
"talkWhileMutedPopup": "Vous essayez de parler? Vous êtes en sourdine.",
"tileViewToggle": "Basculement de l'affichage mosaïque",
@@ -1363,20 +1496,20 @@
},
"transcribing": {
"ccButtonTooltip": "Activer / Désactiver les sous-titres",
"error": "Échec de la transcription. Veuillez réessayer.",
"expandedLabel": "La transcription est actuellement activée",
"failedToStart": "Échec du démarrage de la transcription",
"labelToolTip": "La réunion est transcrite",
"off": "La transcription est arrêtée",
"on": "La transcription est activée",
"pending": "Préparation de la transcription de la réunion en cours…",
"failed": "La transcription a échoué",
"labelTooltip": "La transcription de la réunion est en cours",
"labelTooltipExtra": "De plus, une transcription sera disponible plus tard.",
"openClosedCaptions": "Ouvrir les sous-titres",
"original": "Original",
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
"sourceLanguageHere": "ici",
"start": "Activer l'affichage des sous-titres",
"stop": "Désactiver l'affichage des sous-titres",
"subtitles": "sous-titres",
"subtitlesOff": "off",
"tr": "PI"
"tr": "PI",
"translateTo": "Traduire vers"
},
"unpinParticipant": "Désépingler - {{participantName}}",
"userMedia": {
@@ -1386,7 +1519,7 @@
"busy": "Libération des ressources en cours. Veuillez réessayer dans quelques minutes.",
"busyTitle": "Le service de Salle est actuellement occupé.",
"errorAlreadyInvited": "{{displayName}} a déjà été invité",
"errorInvite": "La conférence n'est pas encore configurée. Veuillez réessayer plus tard.",
"errorInvite": "La réunion n'est pas encore configurée. Veuillez réessayer plus tard.",
"errorInviteFailed": "Nous nous efforçons de régler ce problème. Veuillez réessayer plus tard.",
"errorInviteFailedTitle": "L'invitation de {{displayName}} a échoué",
"errorInviteTitle": "Erreur lors de l'invitation de la salle",
@@ -1407,8 +1540,6 @@
"ld": "LD",
"ldTooltip": "Visionnement de vidéo en basse définition",
"lowDefinition": "Basse définition",
"onlyAudioAvailable": "Seulement l'audio est disponible",
"onlyAudioSupported": "Ce navigateur prend seulement l'audio en charge.",
"performanceSettings": "Paramètres de performance",
"recording": "Enregistrement en cours",
"sd": "SD",
@@ -1418,7 +1549,10 @@
},
"videothumbnail": {
"connectionInfo": "Informations de la connexion",
"demote": "Déplacer en visiteur",
"domute": "Discrétion",
"domuteDesktop": "Arrêter le partage d'écran",
"domuteDesktopOfOthers": "Arrêter le partage d'écran de tous les autres",
"domuteOthers": "Couper le micro de tous les autres",
"domuteVideo": "Couper la caméra",
"domuteVideoOfOthers": "Couper la caméra des autres",
@@ -1470,11 +1604,24 @@
},
"visitors": {
"chatIndicator": "(visiteur)",
"joinMeeting": {
"description": "Vous êtes actuellement un observateur dans cette réunion.",
"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",
"notification": {
"description": "Pour participer lever la main.",
"demoteDescription": "Envoyé ici par {{actor}}, levez la main pour participer",
"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.",
"notAllowedPromotion": "Un participant doit d'abord autoriser votre demande.",
"requestToJoin": "Main levée",
"requestToJoinDescription": "Votre demande a été envoyée aux modérateurs. Patientez !",
"title": "Vous êtes visiteur dans cette réunion"
}
},
"waitingMessage": "Vous rejoindrez la réunion dès qu'elle sera en direct !"
},
"volumeSlider": "Curseur de volume",
"welcomepage": {
@@ -1483,7 +1630,7 @@
"roomname": "Entrer le nom de la salle"
},
"addMeetingName": "Ajouter un nom de réunion",
"appDescription": "Profitez de la conversation vidéo avec toute votre équipe. Allez-y, invitez tous ceux que vous connaissez. {{app}} est une solution 100 % libre de conférence vidéo entièrement chiffrée que vous pouvez utiliser en tout temps et gratuitement, sans avoir besoin de compte.",
"appDescription": "Profitez de la conversation vidéo avec toute votre équipe. Allez-y, invitez tous ceux que vous connaissez. {{app}} est une solution 100 % libre de réunion vidéo entièrement chiffrée que vous pouvez utiliser en tout temps et gratuitement, sans avoir besoin de compte.",
"audioVideoSwitch": {
"audio": "Voix",
"video": "Vidéo"
@@ -1492,12 +1639,13 @@
"connectCalendarButton": "Connecter votre agenda",
"connectCalendarText": "Connectez-vous à votre calendrier pour afficher toutes les réunions {{app}}. Ajoutez également les réunions de {{provider}} à votre calendrier et démarrez-les d'un simple clic.",
"enterRoomTitle": "Démarrer une nouvelle réunion",
"getHelp": "Obtenir de l'aide",
"go": "Aller",
"goSmall": "Aller",
"headerSubtitle": "Conférences sécurisées et de haute qualité",
"headerSubtitle": "Réunions sécurisées et de haute qualité",
"headerTitle": "Jitsi Meet",
"info": "Ret. arr.",
"jitsiOnMobile": "Jitsi sur mobile télécharger notre application et démarrez des conférences de n'import où",
"jitsiOnMobile": "Jitsi sur mobile télécharger notre application et démarrez des réunions de n'import où",
"join": "CRÉER / REJOINDRE",
"logo": {
"calendar": "Logo Calendar",
@@ -1523,14 +1671,15 @@
"roomnameHint": "Entrez le nom ou l'URL de la salle que vous voulez rejoindre. Vous pouvez inventer un nom, mais assurez-vous de le partager avec les participants de la réunion pour qu'ils utilisent le même nom.",
"sendFeedback": "Envoyer un commentaire",
"settings": "Paramètres",
"startMeeting": "Démarrer la conférence",
"startMeeting": "Démarrer la réunion",
"terms": "Termes",
"title": "Conférence vidéo sécurisée, riche en fonctionnalités et entièrement gratuite",
"title": "Réunion vidéo sécurisée, riche en fonctionnalités et entièrement gratuite",
"upcomingMeetings": "Vos réunions à venir"
},
"whiteboard": {
"accessibilityLabel": {
"heading": "Tableau blanc"
}
},
"screenTitle": "Tableau blanc"
}
}

View File

@@ -109,9 +109,12 @@
}
},
"chat": {
"disabled": "L'envoi de messages de chat est désactivé.",
"enter": "Entrez dans le salon",
"error": "Erreur : votre message n'a pas été envoyé. Raison : {{error}}",
"everyone": "Tout le monde",
"fieldPlaceHolder": "Tapez votre message ici",
"guestsChatIndicator": "(invité)",
"lobbyChatMessageTo": "Message de salle d'attente à {{recipient}}",
"message": "Message",
"messageAccessibleTitle": "{{user}} dit: ",
@@ -122,7 +125,10 @@
"nickname": {
"popover": "Choisissez un pseudonyme",
"title": "Entrez un pseudonyme pour utiliser le chat",
"titleWithPolls": "Entrez un pseudonyme pour utiliser le chat et les sondages"
"titleWithCC": "Entrez un pseudonyme pour utiliser le chat et les sous-titres",
"titleWithPolls": "Entrez un pseudonyme pour utiliser le chat et les sondages",
"titleWithPollsAndCC": "Entrez un pseudonyme pour utiliser le chat, les sondages et les sous-titres",
"titleWithPollsAndCCAndFileSharing": "Entrez un pseudonyme pour utiliser le chat, les sondages, les sous-titres et les fichiers"
},
"noMessagesMessage": "Il n'y a pas encore de messages dans cette réunion. Démarrez une conversation ici !",
"privateNotice": "Message privé à {{recipient}}",
@@ -131,9 +137,14 @@
"systemDisplayName": "Système",
"tabs": {
"chat": "Chat",
"closedCaptions": "ST",
"fileSharing": "Fichiers",
"polls": "Sondages"
},
"title": "Chat",
"titleWithCC": "ST",
"titleWithFeatures": "Chat et",
"titleWithFileSharing": "Fichiers",
"titleWithPolls": "Chat et Sondages",
"you": "vous"
},
@@ -144,6 +155,10 @@
"dontShowAgain": "Ne plus m'afficher ceci",
"installExtensionText": "Installer l'extension pour l'intégration de Google Calendar et Office 365"
},
"closedCaptionsTab": {
"emptyState": "Le contenu des sous-titres sera disponible une fois qu'un modérateur l'aura démarré",
"startClosedCaptionsButton": "Démarrer les sous-titres"
},
"connectingOverlay": {
"joiningRoom": "Connexion à la réunion…"
},
@@ -263,7 +278,8 @@
"Remove": "Supprimer",
"Share": "Partager",
"Submit": "Soumettre",
"WaitForHostMsg": "La conférence n'a pas encore commencé. Si vous en êtes l'hôte, veuillez vous authentifier. Sinon, veuillez attendre son arrivée.",
"Understand": "Je comprends, gardez-moi en sourdine pour l'instant",
"UnderstandAndUnmute": "Je comprends, veuillez me réactiver s'il vous plaît",
"WaitForHostNoAuthMsg": "La conférence n'a pas encore commencé car aucun modérateur n'est encore arrivé. Veuillez patienter.",
"WaitingForHostButton": "Attendre l'hôte",
"WaitingForHostTitle": "En attente de l'hôte…",
@@ -285,6 +301,12 @@
"alreadySharedVideoTitle": "Une seule vidéo partagée est autorisée à la fois",
"applicationWindow": "Fenêtre d'application",
"authenticationRequired": "Authentification requise",
"cameraCaptureDialog": {
"description": "Prendre et envoyer une photo en utilisant votre caméra mobile",
"ok": "Ouvrir la caméra",
"reject": "Pas maintenant",
"title": "Prendre une photo"
},
"cameraConstraintFailedError": "Votre caméra ne satisfait pas certaines des contraintes nécessaires.",
"cameraNotFoundError": "La caméra n'a pas été trouvée.",
"cameraNotSendingData": "Impossible d'accéder à votre caméra. Veuillez sélectionner un autre périphérique dans les paramètres ou rafraîchir la page.",
@@ -299,6 +321,7 @@
"conferenceReloadMsg": "On essaie d'arranger ça. Reconnexion dans {{seconds}} secondes…",
"conferenceReloadTitle": "Malheureusement, un problème est survenu",
"confirm": "Confirmer",
"confirmBack": "Retour",
"confirmNo": "Non",
"confirmYes": "Oui",
"connectError": "Oups ! Un problème est survenu et la connexion à la conférence est impossible.",
@@ -336,6 +359,7 @@
"kickParticipantTitle": "Expulser ce participant ?",
"kickSystemTitle": "Oups ! Vous avez été expulsé de la réunion",
"kickTitle": "Oups ! vous avez été expulsé(e) par {{participantDisplayName}}",
"learnMore": "en savoir plus",
"linkMeeting": "Relier la conférence",
"linkMeetingTitle": "Relier la conférence à Salesforce",
"liveStreaming": "Direct",
@@ -358,22 +382,34 @@
"micTimeoutError": "Impossible de démarrer la source audio. Délai dépassé!",
"micUnknownError": "Vous ne pouvez pas utiliser le microphone pour une raison inconnue.",
"moderationAudioLabel": "Autoriser les participants à réactiver leur micro",
"moderationDesktopLabel": "Autoriser les non-modérateurs à partager leur écran",
"moderationVideoLabel": "Autoriser les participants à démarrer leur vidéo",
"muteEveryoneDialog": "Êtes-vous sûr de vouloir couper les micros de tout le monde ? Vous ne pourrez plus réactiver leur micro, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneDialogModerationOn": "Les participants peuvent demander à parler n'importe quand",
"muteEveryoneElseDialog": "Une fois leur micro coupé, vous ne pourrez plus le réactiver, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneElseTitle": "Couper le micro de tout le monde sauf de {{whom}} ?",
"muteEveryoneElsesDesktopDialog": "Une fois le partage arrêté, vous ne pourrez pas le redémarrer, mais ils peuvent le faire à tout moment.",
"muteEveryoneElsesDesktopTitle": "Arrêter le partage d'écran de tout le monde sauf {{whom}} ?",
"muteEveryoneElsesVideoDialog": "Une fois la caméra coupée, vous ne pourrez plus la rallumer, mais ils peuvent la rallumer à tout moment.",
"muteEveryoneElsesVideoTitle": "Couper la vidéo de tout le monde sauf {{whom}}?",
"muteEveryoneSelf": "vous",
"muteEveryoneStartMuted": "Tout le monde démarre avec le micro coupé",
"muteEveryoneTitle": "Couper le micro de tout le monde ?",
"muteEveryonesDesktopDialog": "Les participants peuvent partager leur écran à tout moment.",
"muteEveryonesDesktopDialogModerationOn": "Les participants peuvent envoyer une demande pour partager leur écran à tout moment.",
"muteEveryonesDesktopTitle": "Arrêter le partage d'écran de tout le monde ?",
"muteEveryonesVideoDialog": "Êtes-vous sûr de vouloir couper la caméra de tout le monde? Vous ne pourrez pas la réactiver, mais ils peuvent la remettre à tout moment.",
"muteEveryonesVideoDialogModerationOn": "Les participants peuvent demander à activer leur caméra n'importe quand.",
"muteEveryonesVideoDialogOk": "Désactiver",
"muteEveryonesVideoTitle": "Couper la caméra de tout le monde?",
"muteParticipantBody": "Vous ne pourrez plus réactiver son micro, mais il pourra l'activer par lui-même à tout moment.",
"muteParticipantButton": "Couper le micro",
"muteParticipantsDesktopBody": "Vous ne pourrez pas démarrer leur partage d'écran, mais ils peuvent le faire à tout moment.",
"muteParticipantsDesktopBodyModerationOn": "Vous ne pourrez pas démarrer leur partage d'écran et eux non plus.",
"muteParticipantsDesktopButton": "Arrêter le partage d'écran",
"muteParticipantsDesktopDialog": "Êtes-vous sûr de vouloir désactiver le partage d'écran de ce participant ? Vous ne pourrez pas le redémarrer, mais ils peuvent le faire à tout moment.",
"muteParticipantsDesktopDialogModerationOn": "Êtes-vous sûr de vouloir désactiver le partage d'écran de ce participant ? Vous ne pourrez pas réactiver l'écran et eux non plus.",
"muteParticipantsDesktopTitle": "Désactiver le partage d'écran de ce participant ?",
"muteParticipantsVideoBody": "Vous ne pourrez pas rallumer la caméra, mais ils peuvent la rallumer à tout moment.",
"muteParticipantsVideoBodyModerationOn": "Ni vous ni le participant ne pourront rallumer la caméra.",
"muteParticipantsVideoButton": "Couper la caméra",
@@ -393,6 +429,10 @@
"recentlyUsedObjects": "Vos objets récemment utilisés",
"recording": "Enregistrement",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible durant le direct",
"recordingInProgressDescription": "Cette réunion est en cours d'enregistrement et d'analyse par IA{{learnMore}}. Votre audio et vidéo ont été coupés. Si vous choisissez de vous réactiver, vous consentez à être enregistré.",
"recordingInProgressDescriptionFirstHalf": "Cette réunion est en cours d'enregistrement et d'analyse par IA",
"recordingInProgressDescriptionSecondHalf": ". Votre audio et vidéo ont été coupés. Si vous choisissez de vous réactiver, vous consentez à être enregistré.",
"recordingInProgressTitle": "Enregistrement en cours",
"rejoinNow": "Rejoindre maintenant",
"remoteControlAllowedMessage": "{{user}} a accepté votre demande de prise en main à distance !",
"remoteControlDeniedMessage": "{{user}} a refusé votre demande de prise en main à distance !",
@@ -522,6 +562,23 @@
"veryBad": "Très mauvais",
"veryGood": "Très bon"
},
"fileSharing": {
"downloadFailedDescription": "Veuillez réessayer.",
"downloadFailedTitle": "Échec du téléchargement",
"downloadFile": "Télécharger",
"downloadStarted": "Téléchargement de fichier démarré",
"dragAndDrop": "Glissez et déposez des fichiers ici ou n'importe où sur l'écran",
"fileAlreadyUploaded": "Le fichier a déjà été téléchargé vers cette réunion.",
"fileTooLargeDescription": "Veuillez vous assurer que le fichier ne dépasse pas {{ maxFileSize }}.",
"fileTooLargeTitle": "Le fichier sélectionné est trop volumineux",
"fileUploadProgress": "Progression du téléchargement de fichier",
"fileUploadedSuccessfully": "Fichier téléchargé avec succès",
"removeFile": "Supprimer",
"removeFileSuccess": "Fichier supprimé avec succès",
"uploadFailedDescription": "Veuillez réessayer.",
"uploadFailedTitle": "Échec du téléchargement",
"uploadFile": "Partager un fichier"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Vignettes vidéos"
@@ -690,7 +747,8 @@
"notificationTitle": "Salle d'attente",
"passwordJoinButton": "Rejoindre",
"title": "Salle d'attente",
"toggleLabel": "Activer la salle d'attente"
"toggleLabel": "Activer la salle d'attente",
"waitForModerator": "La conférence n'a pas encore commencé car aucun modérateur n'est encore arrivé. Si vous souhaitez devenir modérateur, veuillez vous connecter. Sinon, veuillez attendre."
},
"localRecording": {
"clientState": {
@@ -733,7 +791,10 @@
"me": "moi",
"notify": {
"OldElectronAPPTitle": "Faille de sécurité !",
"allowAction": "Permettre",
"allowAll": "Tout autoriser",
"allowAudio": "Autoriser l'audio",
"allowDesktop": "Autoriser le partage d'écran",
"allowVideo": "Autoriser la vidéo",
"allowedUnmute": "Vous pouvez réactiver votre écran, votre caméra ou partager votre écran.",
"audioUnmuteBlockedDescription": "Le rétablissement du son a été bloqué temporairement en raison de limites système.",
"audioUnmuteBlockedTitle": "Rétablissement du son bloqué !",
@@ -746,8 +807,10 @@
"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.",
"dataChannelClosedWithAudio": "La qualité de l'audio et de la vidéo peut être altérée",
"desktopMutedRemotelyTitle": "Votre partage d'écran a été arrêté par {{participantDisplayName}}",
"disabledIframe": "L'intégration Iframe est uniquement destinée à des démos, cet appel se terminera dans {{timeout}} minutes.",
"disabledIframeSecondary": "L'intégration Iframe de {{domaine}} est uniquement destinée à des démos, cet appel se terminera dans {{timeout}} minutes.",
"disabledIframeSecondaryNative": "L'intégration de {{domain}} est uniquement destinée aux fins de démonstration, cet appel se terminera dans {{timeout}} minutes.",
"disabledIframeSecondaryWeb": "L'intégration de {{domain}} est uniquement destinée aux fins de démonstration, cet appel se terminera dans {{timeout}} minutes. Veuillez utiliser <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> pour l'intégration en production !",
"disconnected": "déconnecté",
"displayNotifications": "Afficher les notifications pour",
"dontRemindMe": "Ne pas me le rappeler",
@@ -802,6 +865,7 @@
"oldElectronClientDescription1": "Vous semblez utiliser une ancienne version du client Jitsi Meet qui présente des failles de sécurité connues. Veuillez vous assurer de mettre à jour vers notre ",
"oldElectronClientDescription2": "dernière build",
"oldElectronClientDescription3": " rapidement !",
"openChat": "Ouvrir le chat",
"participantWantsToJoin": "souhaite rejoindre la réunion",
"participantsWantToJoin": "souhaitent rejoindre la réunion",
"passwordRemovedRemotely": "Le $t(lockRoomPassword) a été supprimé par un autre participant",
@@ -825,6 +889,8 @@
"suggestRecordingDescription": "Souhaitez-vous démarrer un enregistrement ?",
"suggestRecordingTitle": "Enregistrer cette réunion",
"unmute": "Rétablir le son",
"unmuteScreen": "Démarrer le partage d'écran",
"unmuteVideo": "Réactiver la vidéo",
"videoMutedRemotelyDescription": "Vous pouvez toujours la réactiver.",
"videoMutedRemotelyTitle": "Votre caméra a été coupée par {{participantDisplayName}}!",
"videoUnmuteBlockedDescription": "Le rétablissement de la vidéo a été bloqué temporairement en raison de limites système.",
@@ -843,11 +909,14 @@
"admit": "Accepter",
"admitAll": "Tout accepter",
"allow": "Autoriser les participants à:",
"allowDesktop": "Autoriser le partage d'écran",
"allowVideo": "permettre la vidéo",
"askDesktop": "Demander de partager l'écran",
"askUnmute": "Demander de réactiver le micro",
"audioModeration": "Rouvrir leur micro",
"blockEveryoneMicCamera": "Bloquer tous les micros et caméras",
"breakoutRooms": "Salles annexes",
"desktopModeration": "Démarrer le partage d'écran",
"goLive": "Passer en direct",
"invite": "Inviter quelqu'un",
"lowerAllHands": "Abaisser toutes les mains",
@@ -859,6 +928,8 @@
"muteAll": "Couper le micro de tout le monde",
"muteEveryoneElse": "Couper le micro de tous les autres",
"reject": "Refuser",
"stopDesktop": "Arrêter le partage d'écran",
"stopEveryonesDesktop": "Arrêter le partage d'écran de tout le monde",
"stopEveryonesVideo": "Couper toutes les caméras",
"stopVideo": "Couper la vidéo",
"unblockEveryoneMicCamera": "Débloquer tous les micros et caméras",
@@ -868,12 +939,15 @@
"headings": {
"lobby": "Salle d'attente ({{count}})",
"participantsList": "Participants de la réunion ({{count}})",
"viewerRequests": "Demandes des spectateurs {{count}}",
"visitorInQueue": " (en attente {{count}})",
"visitorRequests": "(Demande {{count}} )",
"visitors": "Visiteurs {{count}}",
"visitorsList": "Spectateurs ({{count}})",
"waitingLobby": "Dans la salle d'attente ({{count}})"
},
"search": "Rechercher des participants",
"searchDescription": "Commencez à taper pour filtrer les participants",
"title": "Participants"
},
"passwordDigitsOnly": "Jusqu'à {{number}} chiffres",
@@ -1100,6 +1174,7 @@
"signedIn": "Accès aux événements du calendrier {{email}}. Cliquez sur le bouton se déconnecter ci-dessous pour arrêter l'accès aux événements du calendrier.",
"title": "Calendrier"
},
"chatWithPermissions": "Le chat nécessite une autorisation",
"desktopShareFramerate": "Images par seconde pour le Partage d'écran",
"desktopShareHighFpsWarning": "Augmenter le nombre d'images par seconde pour le partage d'écran peut impacter votre bande passante. Vous devez repartager l'écran pour que ces paramètres soient utilisés.",
"desktopShareWarning": "Vous devez repartager l'écran pour que ces paramètres soient utilisés.",
@@ -1129,6 +1204,7 @@
"selectMic": "Microphone",
"selfView": "Affichage de votre propre vidéo",
"shortcuts": "Raccourcis",
"showSubtitlesOnStage": "Afficher les sous-titres sur l'écran",
"speakers": "Haut-parleurs",
"startAudioMuted": "Tout le monde commence en muet",
"startReactionsMuted": "Tout le monde commence avec les réactions sonores bloquées",
@@ -1182,11 +1258,13 @@
"fearful": "Effrayé",
"happy": "Content",
"hours": "{{count}}h",
"labelTooltip": "Nombre de participants : {{count}}",
"minutes": "{{count}}m",
"name": "Nom",
"neutral": "Indifférent",
"sad": "Triste",
"search": "Recherche",
"searchDescription": "Commencez à taper pour filtrer les participants",
"searchHint": "Recherche des participants",
"seconds": "{{count}}s",
"speakerStats": "Statistiques de l'interlocuteur",
@@ -1223,6 +1301,7 @@
"closeChat": "Fermer la discussion instantanée",
"closeMoreActions": "Fermer le menu plus d'actions",
"closeParticipantsPane": "Fermer le panneau des participants",
"closedCaptions": "Sous-titres",
"collapse": "Plier",
"document": "Activer / Désactiver le document partagé",
"documentClose": "Fermer le document partagé",
@@ -1301,6 +1380,20 @@
"videounmute": "Activer votre vidéo"
},
"addPeople": "Ajouter des personnes à votre appel",
"advancedAudioSettings": {
"aec": {
"label": "Suppression d'écho acoustique"
},
"agc": {
"label": "Contrôle automatique du gain"
},
"ns": {
"label": "Suppression de bruit"
},
"stereo": {
"label": "Stéréo"
}
},
"audioOnlyOff": "Désactiver le mode bande passante réduite",
"audioOnlyOn": "Activer le mode bande passante réduite",
"audioRoute": "Sélectionner la source audio",
@@ -1313,6 +1406,7 @@
"closeChat": "Fermer le chat",
"closeParticipantsPane": "Fermer le panneau des participants",
"closeReactionsMenu": "Fermer le menu réactions",
"closedCaptions": "Sous-titres",
"disableNoiseSuppression": "Arrêter la suppression du bruit",
"disableReactionSounds": "Vous pouvez interdire les réactions sonores à cette réunion",
"documentClose": "Fermer le document partagé",
@@ -1371,6 +1465,7 @@
"reactionHeart": "Envoyer une réaction en forme de cœur",
"reactionLaugh": "Envoyer réaction rire",
"reactionLike": "Envoyer réaction approuver",
"reactionLove": "Envoyer une réaction d'amour",
"reactionSilence": "Envoyer réaction silence",
"reactionSurprised": "Envoyer réaction surprise",
"reactions": "Reactions",
@@ -1403,14 +1498,18 @@
"ccButtonTooltip": "Activer / Désactiver les sous-titres",
"expandedLabel": "La transcription est actuellement activée",
"failed": "La transcription a échoué",
"labelToolTip": "La transcription de la réunion est en cours",
"labelTooltip": "La transcription de la réunion est en cours",
"labelTooltipExtra": "Une transcription sera disponible plus tard.",
"openClosedCaptions": "Ouvrir les sous-titres",
"original": "Original",
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
"sourceLanguageHere": "ici",
"start": "Activer les sous-titres",
"stop": "Désactiver les sous-titres",
"subtitles": "sous-titres",
"subtitlesOff": "off",
"tr": "TR"
"tr": "TR",
"translateTo": "Traduire vers"
},
"unpinParticipant": "Désépingler - {{participantName}}",
"userMedia": {
@@ -1452,6 +1551,8 @@
"connectionInfo": "Informations de la connexion",
"demote": "Déplacer en visiteur",
"domute": "Couper le micro",
"domuteDesktop": "Arrêter le partage d'écran",
"domuteDesktopOfOthers": "Arrêter le partage d'écran de tous les autres",
"domuteOthers": "Couper le micro de tous les autres",
"domuteVideo": "Couper la caméra",
"domuteVideoOfOthers": "Couper la caméra des autres",
@@ -1516,6 +1617,8 @@
"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.",
"notAllowedPromotion": "Un participant doit d'abord autoriser votre demande.",
"requestToJoin": "Main levée",
"requestToJoinDescription": "Votre demande a été envoyée aux modérateurs.",
"title": "Vous êtes visiteur dans cette réunion"
},
"waitingMessage": "Vous rejoindrez la réunion dès qu'elle sera en direct !"

View File

@@ -522,6 +522,7 @@
"tokenAuthFailedWithReasons": "Sorry, you're not allowed to join this call. Possible reasons: {{reason}}",
"tokenAuthUnsupported": "Token URL is not supported.",
"transcribing": "Transcribing",
"unauthenticatedAccessDisabled": "This call requires authentication. Please login in order to proceed.",
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
"user": "User",
"userIdentifier": "User identifier",
@@ -1436,8 +1437,8 @@
"linkToSalesforce": "Link to Salesforce",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
"login": "Log-in",
"logout": "Log-out",
"login": "Log In",
"logout": "Log Out",
"love": "Heart",
"lowerYourHand": "Lower your hand",
"moreActions": "More actions",

View File

@@ -19,8 +19,6 @@ import {
endConference,
sendTones,
setAssumedBandwidthBps,
setFollowMe,
setFollowMeRecorder,
setLocalSubject,
setPassword,
setSubject
@@ -91,6 +89,7 @@ import {
togglePinStageParticipant
} from '../../react/features/filmstrip/actions.web';
import { getPinnedActiveParticipants, isStageFilmstripAvailable } from '../../react/features/filmstrip/functions.web';
import { setFollowMe, setFollowMeRecorder } from '../../react/features/follow-me/actions';
import { invite } from '../../react/features/invite/actions.any';
import {
selectParticipantInLargeVideo

10
package-lock.json generated
View File

@@ -66,7 +66,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/v2095.0.0+43bbd502/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -18260,8 +18260,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2095.0.0+43bbd502/lib-jitsi-meet.tgz",
"integrity": "sha512-Q+dRiQaCiddlVIurIPnq+evAva7miyCf7FIWhZxxwoZX32djfLn9mzTWekdTOEEFDoMj80N6S8OSseMqtkOPsg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.4.6",
@@ -39715,8 +39715,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2095.0.0+43bbd502/lib-jitsi-meet.tgz",
"integrity": "sha512-Q+dRiQaCiddlVIurIPnq+evAva7miyCf7FIWhZxxwoZX32djfLn9mzTWekdTOEEFDoMj80N6S8OSseMqtkOPsg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"requires": {
"@jitsi/js-utils": "2.4.6",
"@jitsi/logger": "2.1.1",

View File

@@ -72,7 +72,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/v2095.0.0+43bbd502/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",

View File

@@ -260,28 +260,6 @@ export const P2P_STATUS_CHANGED = 'P2P_STATUS_CHANGED';
*/
export const SEND_TONES = 'SEND_TONES';
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.
*
* {
* type: SET_FOLLOW_ME,
* enabled: boolean
* }
*/
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature that is used only by the recorder.
*
* {
* type: SET_FOLLOW_ME_RECORDER,
* enabled: boolean
* }
*/
export const SET_FOLLOW_ME_RECORDER = 'SET_FOLLOW_ME_RECORDER';
/**
* The type of (redux) action which sets the obfuscated room name.
*

View File

@@ -58,8 +58,6 @@ import {
P2P_STATUS_CHANGED,
SEND_TONES,
SET_ASSUMED_BANDWIDTH_BPS,
SET_FOLLOW_ME,
SET_FOLLOW_ME_RECORDER,
SET_OBFUSCATED_ROOM,
SET_PASSWORD,
SET_PASSWORD_FAILED,
@@ -853,38 +851,6 @@ export function sendTones(tones: string, duration: number, pause: number) {
};
}
/**
* Enables or disables the Follow Me feature.
*
* @param {boolean} enabled - Whether or not Follow Me should be enabled.
* @returns {{
* type: SET_FOLLOW_ME,
* enabled: boolean
* }}
*/
export function setFollowMe(enabled: boolean) {
return {
type: SET_FOLLOW_ME,
enabled
};
}
/**
* Enables or disables the Follow Me feature used only for the recorder.
*
* @param {boolean} enabled - Whether Follow Me should be enabled and used only by the recorder.
* @returns {{
* type: SET_FOLLOW_ME_RECORDER,
* enabled: boolean
* }}
*/
export function setFollowMeRecorder(enabled: boolean) {
return {
type: SET_FOLLOW_ME_RECORDER,
enabled
};
}
/**
* Enables or disables the Mute reaction sounds feature.
*

View File

@@ -13,6 +13,7 @@ import {
import { sendAnalytics } from '../../analytics/functions';
import { reloadNow } from '../../app/actions';
import { IStore } from '../../app/types';
import { login } from '../../authentication/actions.any';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { isVpaasMeeting } from '../../jaas/functions';
@@ -209,7 +210,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
let retryAsVisitor = false;
if (error.params?.length && error.params[0]?.visitorsSupported) {
if (error.params?.length && error.params[0]?.visitorsSupported === 'true') {
// visitors are supported, so let's try joining that way
retryAsVisitor = true;
}
@@ -251,7 +252,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
const [ type, msg ] = error.params;
let descriptionKey;
let descriptionKey, customActionNameKey, customActionHandler;
let titleKey = 'dialog.tokenAuthFailed';
if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_MAIN_PARTICIPANTS) {
@@ -263,9 +264,15 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
descriptionKey = 'visitors.notification.notAllowedPromotion';
} else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.ROOM_CREATION_RESTRICTION) {
descriptionKey = 'dialog.errorRoomCreationRestriction';
} else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.ROOM_UNAUTHENTICATED_ACCESS_DISABLED) {
titleKey = 'dialog.unauthenticatedAccessDisabled';
customActionNameKey = [ 'toolbar.login' ];
customActionHandler = [ () => dispatch(login()) ];
}
dispatch(showErrorNotification({
customActionNameKey,
customActionHandler,
descriptionKey,
hideErrorSupportLink: true,
titleKey

View File

@@ -26,8 +26,6 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_ASSUMED_BANDWIDTH_BPS,
SET_FOLLOW_ME,
SET_FOLLOW_ME_RECORDER,
SET_OBFUSCATED_ROOM,
SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE,
@@ -178,8 +176,6 @@ export interface IConferenceState {
dataChannelOpen?: boolean;
e2eeSupported?: boolean;
error?: Error;
followMeEnabled?: boolean;
followMeRecorderEnabled?: boolean;
joining?: IJitsiConference;
leaving?: IJitsiConference;
lobbyError?: boolean;
@@ -274,14 +270,6 @@ ReducerRegistry.register<IConferenceState>('features/base/conference',
return set(state, 'assumedBandwidthBps', assumedBandwidthBps);
}
case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled);
case SET_FOLLOW_ME_RECORDER:
return { ...state,
followMeRecorderEnabled: action.enabled,
followMeEnabled: action.enabled
};
case SET_START_REACTIONS_MUTED:
return set(state, 'startReactionsMuted', action.muted);

View File

@@ -744,6 +744,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
}
if (lastMessage.privateMessage) {
if (!lastMessage.participantId) {
// this is a system message we can ignore
return undefined;
}
// We show the notice if the last received message was private.
return {
id: lastMessage.participantId,

View File

@@ -19,3 +19,25 @@ export const SET_FOLLOW_ME_MODERATOR = 'SET_FOLLOW_ME_MODERATOR';
* }
*/
export const SET_FOLLOW_ME_STATE = 'SET_FOLLOW_ME_STATE';
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.
*
* {
* type: SET_FOLLOW_ME,
* enabled: boolean
* }
*/
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature that is used only by the recorder.
*
* {
* type: SET_FOLLOW_ME_RECORDER,
* enabled: boolean
* }
*/
export const SET_FOLLOW_ME_RECORDER = 'SET_FOLLOW_ME_RECORDER';

View File

@@ -1,5 +1,7 @@
import {
SET_FOLLOW_ME,
SET_FOLLOW_ME_MODERATOR,
SET_FOLLOW_ME_RECORDER,
SET_FOLLOW_ME_STATE
} from './actionTypes';
@@ -37,3 +39,35 @@ export function setFollowMeState(state?: Object) {
state
};
}
/**
* Enables or disables the Follow Me feature.
*
* @param {boolean} enabled - Whether or not Follow Me should be enabled.
* @returns {{
* type: SET_FOLLOW_ME,
* enabled: boolean
* }}
*/
export function setFollowMe(enabled: boolean) {
return {
type: SET_FOLLOW_ME,
enabled
};
}
/**
* Enables or disables the Follow Me feature used only for the recorder.
*
* @param {boolean} enabled - Whether Follow Me should be enabled and used only by the recorder.
* @returns {{
* type: SET_FOLLOW_ME_RECORDER,
* enabled: boolean
* }}
*/
export function setFollowMeRecorder(enabled: boolean) {
return {
type: SET_FOLLOW_ME_RECORDER,
enabled
};
}

View File

@@ -2,11 +2,15 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
import { set } from '../base/redux/functions';
import {
SET_FOLLOW_ME,
SET_FOLLOW_ME_MODERATOR,
SET_FOLLOW_ME_RECORDER,
SET_FOLLOW_ME_STATE
} from './actionTypes';
export interface IFollowMeState {
followMeEnabled?: boolean;
followMeRecorderEnabled?: boolean;
moderator?: string;
recorder?: boolean;
state?: {
@@ -21,7 +25,8 @@ ReducerRegistry.register<IFollowMeState>(
'features/follow-me',
(state = {}, action): IFollowMeState => {
switch (action.type) {
case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled);
case SET_FOLLOW_ME_MODERATOR: {
let newState = set(state, 'moderator', action.id);
@@ -35,6 +40,11 @@ ReducerRegistry.register<IFollowMeState>(
return newState;
}
case SET_FOLLOW_ME_RECORDER:
return { ...state,
followMeRecorderEnabled: action.enabled,
followMeEnabled: action.enabled
};
case SET_FOLLOW_ME_STATE: {
return set(state, 'state', action.state);
}

View File

@@ -17,7 +17,7 @@ import { FOLLOW_ME_COMMAND } from './constants';
* notify all listeners.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'].followMeEnabled,
/* selector */ state => state['features/follow-me'].followMeEnabled,
/* listener */ (newSelectedValue, store) => _sendFollowMeCommand(newSelectedValue || 'off', store));
/**
@@ -88,7 +88,7 @@ function _getFollowMeState(state: IReduxState) {
const stageFilmstrip = isStageFilmstripEnabled(state);
return {
recorder: state['features/base/conference'].followMeRecorderEnabled,
recorder: state['features/follow-me'].followMeRecorderEnabled,
filmstripVisible: state['features/filmstrip'].visible,
maxStageParticipants: stageFilmstrip ? state['features/base/settings'].maxStageParticipants : undefined,
nextOnStage: pinnedParticipant?.id,
@@ -130,7 +130,7 @@ function _sendFollowMeCommand(
);
return;
} else if (!state['features/base/conference'].followMeEnabled) {
} else if (!state['features/follow-me'].followMeEnabled) {
return;
}

View File

@@ -244,6 +244,27 @@ const Notification = ({
window.open(supportUrl, '_blank', 'noopener');
}, [ supportUrl ]);
const processCustomActions
= (key?: string[], handler?: Function[], type?: string[]): {
content: string; onClick: () => void; testId?: string; type?: string; }[] => {
if (key?.length && handler?.length) {
return key.map((customAction: string, customActionIndex: number) => {
return {
content: t(customAction),
onClick: () => {
if (handler?.[customActionIndex]()) {
onDismiss();
}
},
type: type?.[customActionIndex],
testId: customAction
};
});
}
return [];
};
const mapAppearanceToButtons = useCallback((): {
content: string; onClick: () => void; testId?: string; type?: string; }[] => {
switch (appearance) {
@@ -262,7 +283,7 @@ const Notification = ({
});
}
return buttons;
return processCustomActions(customActionNameKey, customActionHandler, customActionType).concat(buttons);
}
case NOTIFICATION_TYPE.WARNING:
return [
@@ -273,22 +294,7 @@ const Notification = ({
];
default:
if (customActionNameKey?.length && customActionHandler?.length) {
return customActionNameKey.map((customAction: string, customActionIndex: number) => {
return {
content: t(customAction),
onClick: () => {
if (customActionHandler?.[customActionIndex]()) {
onDismiss();
}
},
type: customActionType?.[customActionIndex],
testId: customAction
};
});
}
return [];
return processCustomActions(customActionNameKey, customActionHandler, customActionType);
}
}, [ appearance, onDismiss, customActionHandler, customActionNameKey, hideErrorSupportLink, supportUrl ]);

View File

@@ -44,7 +44,14 @@ const useStyles = makeStyles()(theme => {
right: 0,
top: '-8px',
transform: 'translateY(-100%)',
width: '283px'
width: '283px',
// Allow text in menu items to wrap to multiple lines.
'& [role="button"] > div > span, & [role="menuitem"] > div > span': {
whiteSpace: 'normal',
wordBreak: 'break-word',
overflowWrap: 'break-word'
}
},
text: {

View File

@@ -2,10 +2,8 @@ import JitsiMeetJS, { RTCStatsEvents } from '../base/lib-jitsi-meet';
import logger from './logger';
import {
DominantSpeakerData,
E2ERTTData,
FaceLandmarksData,
VideoTypeData
FaceLandmarksData
} from './types';
// TODO(saghul): expose these in libn-jitsi-meet?
@@ -46,16 +44,6 @@ class RTCStats {
JitsiMeetJS.rtcstats.sendStatsEntry('logs', logEntries);
}
/**
* Send dominant speaker data, the data will be processed by rtcstats-server and saved in the dump file.
*
* @param {Object} dominantSpeakerData - Dominant speaker data to be saved in the rtcstats dump.
* @returns {void}
*/
sendDominantSpeakerData(dominantSpeakerData: DominantSpeakerData) {
JitsiMeetJS.rtcstats.sendStatsEntry('dominantSpeaker', dominantSpeakerData);
}
/**
* Send e2e rtt data, the data will be processed by rtcstats-server and saved in the dump file.
*
@@ -87,16 +75,6 @@ class RTCStats {
JitsiMeetJS.rtcstats.sendStatsEntry('conferenceStartTimestamp', timestamp);
}
/**
* Send videoType data, the data will be processed by rtcstats-server and saved in the dump file.
*
* @param {Object} videoTypeData - The object that holds the videoType data.
* @returns {void}
*/
sendVideoTypeData(videoTypeData: VideoTypeData) {
JitsiMeetJS.rtcstats.sendStatsEntry('setVideoType', videoTypeData);
}
/**
* Send face landmarks data, the data will be processed by rtcstats-server and saved in the dump file.
*

View File

@@ -5,9 +5,8 @@ import {
CONFERENCE_JOINED,
E2E_RTT_CHANGED
} from '../base/conference/actionTypes';
import { DOMINANT_SPEAKER_CHANGED } from '../base/participants/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { TRACK_ADDED, TRACK_UPDATED } from '../base/tracks/actionTypes';
import { TRACK_UPDATED } from '../base/tracks/actionTypes';
import { ADD_FACE_LANDMARKS } from '../face-landmarks/actionTypes';
import { FaceLandmarks } from '../face-landmarks/types';
import { sendGetCustomerIdRequest } from '../jaas/functions';
@@ -47,25 +46,10 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
}
break;
}
case TRACK_ADDED: {
if (isRTCStatsEnabled(state)) {
const jitsiTrack = action?.track?.jitsiTrack;
const { ssrc, videoType } = jitsiTrack || { };
// Remote tracks store their ssrc in the jitsiTrack object. Local tracks don't. See getSsrcByTrack.
if (videoType && ssrc && !jitsiTrack.isLocal() && !jitsiTrack.isAudioTrack()) {
RTCStats.sendVideoTypeData({
ssrc,
videoType
});
}
}
break;
}
case TRACK_UPDATED: {
if (isRTCStatsEnabled(state)) {
const { videoType, jitsiTrack, muted } = action?.track || { };
const { ssrc, isLocal, videoType: trackVideoType, conference } = jitsiTrack || { };
const { jitsiTrack, muted } = action?.track || { };
const { isLocal, videoType: trackVideoType, conference } = jitsiTrack || { };
if (trackVideoType === 'camera' && conference && isLocal()) {
RTCStats.sendFaceLandmarksData({
@@ -74,27 +58,6 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
timestamp: Date.now()
});
}
// if the videoType of the remote track has changed we expect to find it in track.videoType. grep for
// trackVideoTypeChanged.
if (videoType && ssrc && !jitsiTrack.isLocal() && !jitsiTrack.isAudioTrack()) {
RTCStats.sendVideoTypeData({
ssrc,
videoType
});
}
}
break;
}
case DOMINANT_SPEAKER_CHANGED: {
if (isRTCStatsEnabled(state)) {
const { id, previousSpeakers, silence } = action.participant;
RTCStats.sendDominantSpeakerData({
dominantSpeakerEndpoint: silence ? null : id,
previousSpeakers
});
}
break;
}

View File

@@ -1,13 +1,4 @@
export type VideoTypeData = {
ssrc: number;
videoType: string;
};
export type DominantSpeakerData = {
dominantSpeakerEndpoint: string;
previousSpeakers: string[];
};
// Types for RTC stats entries.
export type E2ERTTData = {
remoteEndpointId: string;
remoteRegion: string;

View File

@@ -4,8 +4,6 @@ import { IStore } from '../app/types';
import { setTokenAuthUrlSuccess } from '../authentication/actions.web';
import { isTokenAuthEnabled } from '../authentication/functions';
import {
setFollowMe,
setFollowMeRecorder,
setStartMutedPolicy,
setStartReactionsMuted
} from '../base/conference/actions';
@@ -19,6 +17,7 @@ import { updateSettings } from '../base/settings/actions';
import { IAudioSettings } from '../base/settings/reducer';
import { getLocalVideoTrack } from '../base/tracks/functions.web';
import { appendURLHashParam } from '../base/util/uri';
import { setFollowMe, setFollowMeRecorder } from '../follow-me/actions';
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from '../keyboard-shortcuts/actions';
import { toggleBackgroundEffect } from '../virtual-background/actions';
import virtualBackgroundLogger from '../virtual-background/logger';

View File

@@ -3,13 +3,12 @@ import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import {
setFollowMe,
setFollowMeRecorder,
setStartMutedPolicy,
setStartReactionsMuted
} from '../../../base/conference/actions';
import { updateSettings } from '../../../base/settings/actions';
import Switch from '../../../base/ui/components/native/Switch';
import { setFollowMe, setFollowMeRecorder } from '../../../follow-me/actions';
import { getModeratorTabProps } from '../../functions.native';
import FormRow from './FormRow';

View File

@@ -151,12 +151,11 @@ export function getModeratorTabProps(stateful: IStateful) {
const state = toState(stateful);
const {
conference,
followMeEnabled,
followMeRecorderEnabled,
startAudioMutedPolicy,
startVideoMutedPolicy,
startReactionsMuted
} = state['features/base/conference'];
const { followMeEnabled, followMeRecorderEnabled } = state['features/follow-me'];
const { groupChatWithPermissions } = state['features/chat'];
const { disableReactionsModeration } = state['features/base/config'];
const followMeActive = isFollowMeActive(state);

View File

@@ -1,5 +1,6 @@
#### room._data fields
- allModerators - If is set to true, all authenticated participants are moderators. You need a custom module to set participants as moderators based on the field.
- allowUnauthenticatedAccess - Option that works only with tenants. When it is false only jwt authenticated participants(visitors) are allowed in the meeting and their token(or sub claim for jaas) should match the one in the room. Default value when missing is true.
- av_can_unmute - Default value is missing/true. If set to false, when the first moderator joining the room we enable AV moderation and disable the ability for participants to unmute themselves.
- av_first_moderator_joined - When av_can_unmute is set to false, this flag is used to indicate that the first moderator has joined the room and AV moderation is enabled.
- breakout_rooms - A table containing breakout rooms created in the main room. The keys are the JIDs of the breakout rooms, and the values are their subjects.

View File

@@ -17,6 +17,7 @@ local filters = require 'util.filters';
local array = require 'util.array';
local set = require 'util.set';
local json = require 'cjson.safe';
local datetime = require 'util.datetime';
local util = module:require 'util';
local is_admin = util.is_admin;
@@ -476,6 +477,46 @@ local function message_handler(event)
end
end
-- Receives history messages from the main prosody and adds them to the local history
-- this happens before the first participant joins and that participant gets the history using the standard flow on join
local function history_message_handler(event)
local origin, stanza = event.origin, event.stanza;
local delay_elem = stanza:get_child('delay', 'urn:xmpp:delay');
if not delay_elem then
return;
end
-- now let's add it history, we do not use the event as we want to keep
-- the delay element
local room = get_room_from_jid(room_jid_match_rewrite(
jid.bare(stanza.attr.from):sub(1, -(main_domain:len() + 1))..local_domain));
if not room then
return;
end
if room:get_historylength() == 0 then
return;
end
local history = room._history;
if not history then history = {}; room._history = history; end
local history_stanza = st.clone(stanza);
history_stanza.attr.to = '';
local node, host, resource = jid.split(room_jid_match_rewrite(history_stanza.attr.from));
history_stanza.attr.from = jid.join(node, local_muc_domain, resource);
-- the from in the delay extension is still the main prosody jid, not used for now
local entry = { stanza = history_stanza, timestamp = datetime.parse(delay_elem.attr.stamp) };
table.insert(history, entry);
while #history > room:get_historylength() do table.remove(history, 1) end
return false;
end
module:hook('message/host', history_message_handler);
process_host_module(local_domain, function(host_module, host)
host_module:hook('iq/host', stanza_handler, 10);
host_module:hook('message/full', message_handler);
@@ -698,6 +739,10 @@ local function iq_from_main_handler(event)
local createdTimestamp = node.attr.createdTimestamp;
room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil;
if node.attr.allowUnauthenticatedAccess then
room._data.allowUnauthenticatedAccess = node.attr.allowUnauthenticatedAccess == 'true';
end
if node.attr.lobby == 'true' then
room._main_room_lobby_enabled = true;
elseif node.attr.lobby == 'false' then

View File

@@ -94,7 +94,7 @@ function module.add_host(host_module)
token_util = module:require "token/util".new(host_module);
if asapKeyServer then
if asapKeyServer ~= "" then
-- init token util with our asap keyserver
token_util:set_asap_key_server(asapKeyServer)
end

View File

@@ -76,12 +76,21 @@ local function verify_user(session, stanza)
end
if DEBUG then module:log("debug", "Will verify token for user: %s, room: %s ", user_jid, stanza.attr.to); end
if not token_util:verify_room(session, stanza.attr.to) then
module:log("error", "Token %s not allowed to join: %s",
tostring(session.auth_token), tostring(stanza.attr.to));
session.send(
st.error_reply(
stanza, "cancel", "not-allowed", "Room and token mismatched"));
local res, err, reason = token_util:verify_room(session, stanza.attr.to);
if not res then
if not err and not reason then
reason = 'Room and token mismatched';
end
module:log('error', 'Token %s not allowed to join: %s err: %s reason: %s',
tostring(session.auth_token), tostring(stanza.attr.to), err, reason);
local response = st.error_reply(stanza, 'cancel', 'not-allowed', reason);
if err then
response:tag(err, { xmlns = 'http://jitsi.org/jitmeet' });
end
session.send(response);
return false; -- we need to just return non nil
end
if DEBUG then module:log("debug", "allowed: %s to enter/create room: %s", user_jid, stanza.attr.to); end

View File

@@ -17,6 +17,7 @@ local is_admin = util.is_admin;
local presence_check_status = util.presence_check_status;
local process_host_module = util.process_host_module;
local is_transcriber_jigasi = util.is_transcriber_jigasi;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local json = require 'cjson.safe';
-- Debug flag
@@ -70,7 +71,8 @@ local function send_visitors_iq(conference_service, room, type)
password = type ~= 'disconnect' and room:get_password() or '',
lobby = room._data.lobbyroom and 'true' or 'false',
meetingId = room._data.meetingId,
createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil
createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil,
allowUnauthenticatedAccess = room._data.allowUnauthenticatedAccess ~= nil and tostring(room._data.allowUnauthenticatedAccess) or nil
});
if type == 'update' then
@@ -166,6 +168,20 @@ local function connect_vnode(event)
-- send update initially so we can report the moderators that will join
send_visitors_iq(conference_service, room, 'update');
-- let's send message history
local event = {
room = room;
to = conference_service;
next_stanza = function() end; -- muc-get-history should define this iterator
};
module:context(main_muc_component_config):fire_event("muc-get-history", event);
for msg in event.next_stanza, event do
-- the messages stored in history has been stored before domain_mapper and
-- contain the virtual jid for a from
msg.attr.from = room_jid_match_rewrite(msg.attr.from);
room:route_stanza(msg);
end
for _, o in room:each_occupant() do
if not is_admin(o.bare_jid) then
local fmuc_pr = filter_stanza_nick_if_needed(st.clone(o:get_presence()), room);

View File

@@ -15,6 +15,7 @@ local http_get_with_retry = main_util.http_get_with_retry;
local extract_subdomain = main_util.extract_subdomain;
local starts_with = main_util.starts_with;
local table_shallow_copy = main_util.table_shallow_copy;
local get_room_from_jid = main_util.get_room_from_jid;
local cjson_safe = require 'cjson.safe'
local timer = require "util.timer";
local async = require "util.async";
@@ -374,16 +375,22 @@ end
-- it and returns false in case verification was processed
-- and was not successful
function Util:verify_room(session, room_address)
if self.allowEmptyToken and session.auth_token == nil then
--module:log("debug", "Skipped room token verification - empty tokens are allowed");
return true;
end
-- extract room name using all chars, except the not allowed ones
local room,_,_ = jid.split(room_address);
if room == nil then
log("error",
"Unable to get name of the MUC room ? to: %s", room_address);
module:log('error', 'Unable to get name of the MUC room ? to: %s', room_address);
return false, 'invalid-room-address', 'Room address is invalid';
end
local room_instance = get_room_from_jid(jid.join(room, self.muc_domain));
if not room_instance then
module:log('info', 'Room does not exists:%s %s', room, debug.traceback());
return false, 'room-does-not-exist', 'Room does not exist';
end
if self.allowEmptyToken and session.auth_token == nil then
if room_instance._data.allowUnauthenticatedAccess == false then
return false, 'authentication-required', 'Authentication required';
end
return true;
end
@@ -395,11 +402,11 @@ function Util:verify_room(session, room_address)
module:log('warn', 'session.jitsi_meet_room not string: %s', inspect(auth_room));
end
end
if not self.enableDomainVerification then
-- if auth_room is missing, this means user is anonymous (no token for
-- its domain) we let it through, jicofo is verifying creation domain
-- if auth_room is missing, this means user is anonymous (no token for its domain) we let it through
if auth_room and (room ~= auth_room and not ends_with(room, ']'..auth_room)) and auth_room ~= '*' then
return false;
return false, 'room-mismatch', 'Room does not match the room from token';
end
return true;
@@ -439,7 +446,7 @@ function Util:verify_room(session, room_address)
-- not a regex
room_to_check = auth_room;
end
-- module:log("debug", "room to check: %s", room_to_check)
if not room_to_check then
if not self.requireRoomClaim then
-- if we do not require to have the room claim, and it is missing
@@ -447,17 +454,22 @@ function Util:verify_room(session, room_address)
return true;
end
return false;
return false, 'room-name-does-not-match', 'Room name cannot be matched to the one from token.';
end
end
if session.jitsi_meet_str_tenant
and string.lower(session.jitsi_meet_str_tenant) ~= session.jitsi_web_query_prefix then
session.jitsi_meet_tenant_mismatch = true;
if room_instance._data.allowUnauthenticatedAccess == false then
return false;
end
module:log('warn', 'Tenant differs for user:%s group:%s url_tenant:%s token_tenant:%s',
session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or '',
session.jitsi_meet_context_group,
session.jitsi_web_query_prefix, session.jitsi_meet_str_tenant);
session.jitsi_meet_tenant_mismatch = true;
end
local auth_domain = string.lower(session.jitsi_meet_domain);
@@ -474,7 +486,7 @@ function Util:verify_room(session, room_address)
-- deny access if option is missing
if not self.muc_domain_base then
module:log("warn", "No 'muc_domain_base' option set, denying access!");
return false;
return false, 'server-missing-config', 'Misconfiguration of server';
end
return room_address_to_verify == jid.join(

View File

@@ -59,6 +59,12 @@ export class Participant {
private _iFrameApi: boolean = false;
/**
* Whether the current frame is the main frame. This could coincide with the Jitsi Meet frame (when it's loaded
* directly), or not (when it's loaded in an iframe).
*/
private _inMainFrame: boolean = true;
/**
* The default config to use when joining.
*
@@ -149,9 +155,17 @@ export class Participant {
*/
async getEndpointId(): Promise<string> {
if (!this._endpointId) {
const wasInMainFrame = this._inMainFrame;
await this.switchToIFrame();
this._endpointId = await this.execute(() => { // eslint-disable-line arrow-body-style
return APP?.conference?.getMyUserId();
});
if (wasInMainFrame) {
await this.switchToMainFrame();
}
}
return this._endpointId;
@@ -626,21 +640,31 @@ export class Participant {
/**
* Switches to the main frame context (outside the iFrame; where the Jitsi Meet iFrame API is available).
*
* If this Participant was initialized with iFrameApi=false this has no effect, as there aren't any other contexts.
* If this Participant was initialized with iFrameApi=false this is a no-op.
*/
async switchToMainFrame() {
if (!this._iFrameApi || this._inMainFrame) {
return;
}
await this.driver.switchFrame(null);
this._inMainFrame = true;
}
/**
* Switches to the iFrame context (inside the iFrame; where the Jitsi Meet application runs).
*
* If this Participant was initialized with iFrameApi=false this will result in an error.
* If this Participant was initialized with iFrameApi=false this is a no-op.
*/
async switchToIFrame() {
if (!this._iFrameApi || !this._inMainFrame) {
return;
}
const iframe = this.driver.$('iframe');
await this.driver.switchFrame(iframe);
this._inMainFrame = false;
}
/**

View File

@@ -2,8 +2,6 @@
* An interface that tests can export (as a TEST_PROPERTIES property) to define what they require.
*/
export type ITestProperties = {
/** The test uses the iFrame API. */
useIFrameApi: boolean;
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
useJaas: boolean;
/** The test requires the webhook proxy. */
@@ -12,7 +10,6 @@ export type ITestProperties = {
};
const defaultProperties: ITestProperties = {
useIFrameApi: false,
useWebhookProxy: false,
useJaas: false,
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]

View File

@@ -4,26 +4,67 @@
export const config = {
/** Enable debug logging. Note this includes private information from .env */
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
iframe: {
customerId: process.env.IFRAME_TENANT?.trim()?.replace('vpaas-magic-cookie-', ''),
tenant: process.env.IFRAME_TENANT?.trim(),
/** Whether the configuration specifies a JaaS account for the iFrame API tests. */
usesJaas: Boolean(process.env.JWT_PRIVATE_KEY_PATH && process.env.JWT_KID?.startsWith('vpaas-magic-cookie-')),
},
/** Whether to expect the environment to automatically elect a new moderator when the existing moderator leaves. */
autoModerator: (() => {
if (typeof process.env.AUTO_MODERATOR !== 'undefined') {
return process.env.AUTO_MODERATOR?.trim() === 'true';
}
// If not explicitly configured, fallback to recognizing whether we're running against one of the JaaS
// environments which are known to have the setting disabled.
return !Boolean(
process.env.JWT_PRIVATE_KEY_PATH && process.env.JWT_KID?.startsWith('vpaas-magic-cookie-')
);
})(),
jaas: {
customerId: (() => {
if (typeof process.env.JAAS_TENANT !== 'undefined') {
return process.env.JAAS_TENANT?.trim()?.replace('vpaas-magic-cookie-', '');
}
return process.env.IFRAME_TENANT?.trim()?.replace('vpaas-magic-cookie-', '');
})(),
/** Whether the configuration for JaaS specific tests is enabled. */
enabled: Boolean(process.env.JAAS_TENANT && process.env.JAAS_PRIVATE_KEY_PATH && process.env.JAAS_KID),
enabled: Boolean(
(process.env.JAAS_TENANT || process.env.IFRAME_TENANT)
&& (process.env.JAAS_PRIVATE_KEY_PATH || process.env.JWT_PRIVATE_KEY_PATH)
&& (process.env.JAAS_KID || process.env.JWT_KID)),
/** The JaaS key ID, used to sign the tokens. */
kid: process.env.JAAS_KID?.trim(),
kid: (() => {
if (typeof process.env.JAAS_KID !== 'undefined') {
return process.env.JAAS_KID?.trim();
}
return process.env.JWT_KID?.trim();
})(),
/** The path to the JaaS private key, used to sign JaaS tokens. */
privateKeyPath: process.env.JAAS_PRIVATE_KEY_PATH?.trim(),
privateKeyPath: (() => {
if (typeof process.env.JAAS_PRIVATE_KEY_PATH != 'undefined') {
return process.env.JAAS_PRIVATE_KEY_PATH?.trim();
}
return process.env.JWT_PRIVATE_KEY_PATH?.trim();
})(),
/** The JaaS tenant (vpaas-magic-cookie-<ID>) . */
tenant: process.env.JAAS_TENANT?.trim(),
tenant: (() => {
if (typeof process.env.JAAS_TENANT !== 'undefined') {
return process.env.JAAS_TENANT?.trim();
}
return process.env.IFRAME_TENANT?.trim();
})()
},
jwt: {
kid: process.env.JWT_KID?.trim(),
/** A pre-configured token used by some tests. */
preconfiguredToken: process.env.JWT_ACCESS_TOKEN?.trim(),
preconfiguredJwt: process.env.JWT_ACCESS_TOKEN?.trim(),
preconfiguredToken: (() => {
if (process.env.JWT_ACCESS_TOKEN) {
return { jwt: process.env.JWT_ACCESS_TOKEN?.trim() };
}
return undefined;
})(),
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH?.trim()
},
roomName: {

41
tests/helpers/jaas.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Participant } from './Participant';
import { config } from './TestsConfig';
import { joinMuc } from './joinMuc';
import { IToken, ITokenOptions, generateToken } from './token';
import { IParticipantJoinOptions, IParticipantOptions } from './types';
export function generateJaasToken(options: ITokenOptions): IToken {
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
// Don't override the keyId and keyPath if they are already set in options, allow tests to set them.
return generateToken({
...options,
keyId: options.keyId || config.jaas.kid,
keyPath: options.keyPath || config.jaas.privateKeyPath
});
}
/**
* Creates a new Participant and joins the MUC with the given options. The jaas-specific properties must be set as
* environment variables (see env.example and TestsConfig.ts). If no room name is specified, the default room name
* from the context is used.
*
* @param participantOptions
* @param joinOptions options to use when joining the MUC.
* @returns {Promise<Participant>} The Participant that has joined the MUC.
*/
export async function joinJaasMuc(
participantOptions?: Partial<IParticipantOptions>,
joinOptions?: Partial<IParticipantJoinOptions>): Promise<Participant> {
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
return await joinMuc(participantOptions, {
...joinOptions,
tenant: joinOptions?.tenant || config.jaas.tenant
});
}

View File

@@ -1,25 +1,9 @@
import { Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
import { IParticipantJoinOptions, IParticipantOptions } from '../../helpers/types';
export function generateJaasToken(options: ITokenOptions): IToken {
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
// Don't override the keyId and keyPath if they are already set in options, allow tests to set them.
return generateToken({
...options,
keyId: options.keyId || config.jaas.kid,
keyPath: options.keyPath || config.jaas.privateKeyPath
});
}
import { Participant } from './Participant';
import { IParticipantJoinOptions, IParticipantOptions } from './types';
/**
* Creates a new Participant and joins the MUC with the given options. The jaas-specific properties must be set as
* environment variables (see env.example and TestsConfig.ts). If no room name is specified, the default room name
* from the context is used.
* Creates a new Participant and joins the MUC with the given options. If no room name is specified, the default room
* name from the context is used.
*
* @param participantOptions
* @param joinOptions options to use when joining the MUC.
@@ -31,10 +15,6 @@ export async function joinMuc(
const name = participantOptions?.name || 'p1';
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
// @ts-ignore
const p = ctx[name] as Participant;
@@ -55,7 +35,6 @@ export async function joinMuc(
return await newParticipant.joinConference({
...joinOptions,
tenant: joinOptions?.tenant || config.jaas.tenant,
roomName: joinOptions?.roomName || ctx.roomName,
});
}

View File

@@ -1,6 +1,5 @@
import { P1, P2, P3, P4, Participant } from './Participant';
import { config } from './TestsConfig';
import { generateToken } from './token';
import { IJoinOptions, IParticipantOptions } from './types';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
@@ -10,31 +9,22 @@ const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
* Ensure that the first participant is moderator if there is such an option.
*
* @param {IJoinOptions} options - The options to use when joining the participant.
* @param participantOptions
* @returns {Promise<void>}
*/
export async function ensureOneParticipant(options?: IJoinOptions): Promise<void> {
const participantOps = { name: P1 } as IParticipantOptions;
export async function ensureOneParticipant(
options?: IJoinOptions, participantOptions?: IParticipantOptions): Promise<void> {
if (!participantOptions) {
participantOptions = { name: P1 };
}
participantOptions.name = P1;
if (!options?.skipFirstModerator) {
const jwtPrivateKeyPath = config.jwt.privateKeyPath;
// we prioritize the access token when iframe is not used and private key is set,
// otherwise if private key is not specified we use the access token if set
if (config.jwt.preconfiguredToken
&& ((jwtPrivateKeyPath && !ctx.testProperties.useIFrameApi && !options?.preferGenerateToken)
|| !jwtPrivateKeyPath)) {
participantOps.token = { jwt: config.jwt.preconfiguredToken };
} else if (jwtPrivateKeyPath) {
participantOps.token = generateToken({
...options?.tokenOptions,
displayName: participantOps.name,
moderator: true
});
}
if (!participantOptions.token) {
participantOptions.token = config.jwt.preconfiguredToken;
}
// make sure the first participant is moderator, if supported by deployment
await joinParticipant(participantOps, options);
await joinParticipant(participantOptions, options);
}
/**
@@ -136,23 +126,18 @@ export async function ensureFourParticipants(options?: IJoinOptions): Promise<vo
* Ensure that there are two participants.
*
* @param {IJoinOptions} options - The options to join.
* @param participantOptions
*/
export async function ensureTwoParticipants(options?: IJoinOptions): Promise<void> {
await ensureOneParticipant(options);
export async function ensureTwoParticipants(
options?: IJoinOptions, participantOptions?: IParticipantOptions): Promise<void> {
await ensureOneParticipant(options, participantOptions);
const participantOptions = { name: P2 } as IParticipantOptions;
if (options?.preferGenerateToken) {
participantOptions.token = generateToken({
...options?.tokenOptions,
displayName: participantOptions.name,
});
if (!participantOptions) {
participantOptions = { name: P2 };
}
participantOptions.name = P2;
await joinParticipant({
...participantOptions,
name: P2
}, options);
await joinParticipant(participantOptions, options);
if (options?.skipInMeetingChecks) {
return Promise.resolve();
@@ -171,8 +156,7 @@ export async function ensureTwoParticipants(options?: IJoinOptions): Promise<voi
/**
* Creates a new participant instance, or returns an existing one if it is already joined.
* @param participantOptions - The participant options, with required name set.
* @param {boolean} options - Join options.
* @param reuse whether to reuse an existing participant instance if one is available.
* @param options - Join options.
* @returns {Promise<Participant>} - The participant instance.
*/
async function joinParticipant( // eslint-disable-line max-params
@@ -180,13 +164,11 @@ async function joinParticipant( // eslint-disable-line max-params
options?: IJoinOptions
): Promise<Participant> {
participantOptions.iFrameApi = ctx.testProperties.useIFrameApi;
// @ts-ignore
const p = ctx[participantOptions.name] as Participant;
if (p) {
if (ctx.testProperties.useIFrameApi) {
if (participantOptions.iFrameApi) {
await p.switchToIFrame();
}
@@ -194,7 +176,7 @@ async function joinParticipant( // eslint-disable-line max-params
return p;
}
if (ctx.testProperties.useIFrameApi) {
if (participantOptions.iFrameApi) {
// when loading url make sure we are on the top page context or strange errors may occur
await p.switchToMainFrame();
}
@@ -209,87 +191,12 @@ async function joinParticipant( // eslint-disable-line max-params
// @ts-ignore
ctx[participantOptions.name] = newParticipant;
let tenant = options?.tenant;
if (options?.preferGenerateToken && !ctx.testProperties.useIFrameApi
&& config.iframe.usesJaas && config.iframe.tenant) {
tenant = config.iframe.tenant;
}
if (!tenant && ctx.testProperties.useIFrameApi) {
tenant = config.iframe.tenant;
}
return await newParticipant.joinConference({
...options,
tenant: tenant,
roomName: options?.roomName || ctx.roomName,
});
}
/**
* Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
* conference participants sees a specific mute state for the former.
*
* @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
* mute state is to be toggled.
* @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
* the mute state of {@code testee}.
* @returns {Promise<void>}
*/
export async function muteAudioAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickAudioMuteButton();
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
await observer.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee);
await testee.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee);
}
/**
* Unmute audio, checks if the local UI has been updated accordingly and then does the verification from
* the other observer participant perspective.
* @param testee
* @param observer
*/
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
await testee.getNotifications().closeAskToUnmuteNotification(true);
await testee.getNotifications().closeAVModerationMutedNotification(true);
await testee.getToolbar().clickAudioUnmuteButton();
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
await testee.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee, true);
await observer.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee, true);
}
/**
* Stop the video on testee and check on observer.
* @param testee
* @param observer
*/
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickVideoUnmuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
}
/**
* Starts the video on testee and check on observer.
* @param testee
* @param observer
*/
export async function muteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickVideoMuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
}
/**
* Parse a JID string.
* @param str the string to parse.

View File

@@ -37,8 +37,8 @@ export type IContext = {
export type IParticipantOptions = {
/** Whether it should use the iFrame API. */
iFrameApi?: boolean;
/** Must be 'p1', 'p2', 'p3', or 'p4'. */
name: string;
/** Determines the browser instance to use. */
name: 'p1' | 'p2' | 'p3' | 'p4';
/** An optional token to use. */
token?: IToken;
};
@@ -80,12 +80,6 @@ export type IJoinOptions = {
*/
configOverwrite?: IConfig;
/**
* When joining the first participant and jwt singing material is available and a provided token
* is available, prefer generating a new token for the first participant.
*/
preferGenerateToken?: boolean;
/**
* To be able to override the ctx generated room name. If missing the one from the context will be used.
*/
@@ -96,11 +90,6 @@ export type IJoinOptions = {
*/
skipDisplayName?: boolean;
/**
* Whether to skip setting the moderator role for the first participant (whether to use jwt for it).
*/
skipFirstModerator?: boolean;
/**
* Whether to skip in meeting checks like ice connected and send receive data. For single in meeting participant.
*/

View File

@@ -20,6 +20,20 @@ export default class ChatPanel extends BasePageObject {
await this.participant.driver.keys([ 'c' ]);
}
async sendMessage(message: string) {
if (!await this.isOpen()) {
await this.pressShortcut();
}
if (!await this.isOpen()) {
throw new Error('Chat panel failed to open');
}
const inputField = this.participant.driver.$('#chat-input');
await inputField.click();
await this.participant.driver.keys(`${message}\n`);
}
/**
* Opens the polls tab in the chat panel.
*/

View File

@@ -4,10 +4,12 @@ import {
ensureOneParticipant,
ensureTwoParticipants,
joinSecondParticipant,
} from '../../helpers/participants';
import {
muteAudioAndCheck,
unmuteAudioAndCheck,
unmuteVideoAndCheck
} from '../../helpers/participants';
} from '../helpers/mute';
describe('Mute', () => {
it('joining the meeting', () => ensureTwoParticipants());

View File

@@ -1,4 +1,5 @@
import { ensureTwoParticipants, muteVideoAndCheck, unmuteVideoAndCheck } from '../../helpers/participants';
import { ensureTwoParticipants } from '../../helpers/participants';
import { muteVideoAndCheck, unmuteVideoAndCheck } from '../helpers/mute';
describe('Stop video', () => {
it('joining the meeting', () => ensureTwoParticipants());

View File

@@ -1,5 +1,6 @@
import type { Participant } from '../../helpers/Participant';
import { ensureThreeParticipants, muteAudioAndCheck } from '../../helpers/participants';
import { ensureThreeParticipants } from '../../helpers/participants';
import { muteAudioAndCheck } from '../helpers/mute';
describe('ActiveSpeaker', () => {
it('testActiveSpeaker', async () => {

View File

@@ -3,10 +3,9 @@ import { config } from '../../helpers/TestsConfig';
import {
ensureOneParticipant,
ensureThreeParticipants, ensureTwoParticipants,
hangupAllParticipants,
unmuteAudioAndCheck,
unmuteVideoAndCheck
hangupAllParticipants
} from '../../helpers/participants';
import { unmuteAudioAndCheck, unmuteVideoAndCheck } from '../helpers/mute';
describe('AVModeration', () => {
@@ -78,8 +77,9 @@ describe('AVModeration', () => {
});
it('hangup and change moderator', async () => {
// no moderator switching if jaas is available.
if (config.iframe.usesJaas) {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!config.autoModerator) {
return;
}

View File

@@ -1,8 +1,5 @@
import {
ensureThreeParticipants,
ensureTwoParticipants,
unmuteVideoAndCheck
} from '../../helpers/participants';
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
import { unmuteVideoAndCheck } from '../helpers/mute';
const EMAIL = 'support@jitsi.org';
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';

View File

@@ -196,8 +196,9 @@ describe('Lobby', () => {
});
it('change of moderators in lobby', async () => {
// no moderator switching if jaas is available.
if (config.iframe.usesJaas) {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!config.autoModerator) {
return;
}
await hangupAllParticipants();
@@ -288,8 +289,9 @@ describe('Lobby', () => {
});
it('moderator leaves while lobby enabled', async () => {
// no moderator switching if jaas is available.
if (config.iframe.usesJaas) {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!config.autoModerator) {
return;
}
const { p1, p2, p3 } = ctx;

View File

@@ -1,11 +1,9 @@
import {
checkForScreensharingTile,
ensureOneParticipant,
ensureTwoParticipants,
hangupAllParticipants,
joinSecondParticipant,
joinThirdParticipant,
unmuteVideoAndCheck
joinThirdParticipant
} from '../../helpers/participants';
describe('StartMuted', () => {
@@ -144,134 +142,134 @@ describe('StartMuted', () => {
await p1.waitForAudioMuted(p3, false /* unmuted */);
});
it('startWithVideoMuted=true can unmute', async () => {
// Maybe disable if there is FF or Safari participant.
await hangupAllParticipants();
// Explicitly enable P2P due to a regression with unmute not updating
// large video while in P2P.
const options = {
configOverwrite: {
p2p: {
enabled: true
},
startWithVideoMuted: true
}
};
await ensureTwoParticipants(options);
const { p1, p2 } = ctx;
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
await Promise.all([
p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
]);
await unmuteVideoAndCheck(p2, p1);
await p1.getLargeVideo().assertPlaying();
});
it('startWithAudioMuted=true can unmute', async () => {
await hangupAllParticipants();
const options = {
configOverwrite: {
startWithAudioMuted: true,
testing: {
testMode: true,
debugAudioLevels: true
}
}
};
await ensureTwoParticipants(options);
const { p1, p2 } = ctx;
await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, true) ]);
await p1.getToolbar().clickAudioUnmuteButton();
await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, false) ]);
});
it('startWithAudioVideoMuted=true can unmute', async () => {
await hangupAllParticipants();
const options = {
configOverwrite: {
startWithAudioMuted: true,
startWithVideoMuted: true,
p2p: {
enabled: true
}
}
};
await ensureOneParticipant(options);
await joinSecondParticipant({
configOverwrite: {
testing: {
testMode: true,
debugAudioLevels: true
},
p2p: {
enabled: true
}
}
});
const { p1, p2 } = ctx;
await p2.waitForIceConnected();
await p2.waitForSendMedia();
await p2.waitForAudioMuted(p1, true);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
// Unmute p1's both audio and video and check on p2.
await p1.getToolbar().clickAudioUnmuteButton();
await p2.waitForAudioMuted(p1, false);
await unmuteVideoAndCheck(p1, p2);
await p2.getLargeVideo().assertPlaying();
});
it('test p2p JVB switch and switch back', async () => {
const { p1, p2 } = ctx;
// Mute p2's video just before p3 joins.
await p2.getToolbar().clickVideoMuteButton();
await joinThirdParticipant({
configOverwrite: {
p2p: {
enabled: true
}
}
});
const { p3 } = ctx;
// Unmute p2 and check if its video is being received by p1 and p3.
await unmuteVideoAndCheck(p2, p3);
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
// Mute p2's video just before p3 leaves.
await p2.getToolbar().clickVideoMuteButton();
await p3.hangup();
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getToolbar().clickVideoUnmuteButton();
// Check if p2's video is playing on p1.
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
await p1.getLargeVideo().assertPlaying();
});
// it('startWithVideoMuted=true can unmute', async () => {
// // Maybe disable if there is FF or Safari participant.
//
// await hangupAllParticipants();
//
// // Explicitly enable P2P due to a regression with unmute not updating
// // large video while in P2P.
// const options = {
// configOverwrite: {
// p2p: {
// enabled: true
// },
// startWithVideoMuted: true
// }
// };
//
// await ensureTwoParticipants(options);
//
// const { p1, p2 } = ctx;
//
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
// await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
//
// await Promise.all([
// p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
// p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
// ]);
//
// await unmuteVideoAndCheck(p2, p1);
// await p1.getLargeVideo().assertPlaying();
// });
//
// it('startWithAudioMuted=true can unmute', async () => {
// await hangupAllParticipants();
//
// const options = {
// configOverwrite: {
// startWithAudioMuted: true,
// testing: {
// testMode: true,
// debugAudioLevels: true
// }
// }
// };
//
// await ensureTwoParticipants(options);
//
// const { p1, p2 } = ctx;
//
// await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, true) ]);
// await p1.getToolbar().clickAudioUnmuteButton();
// await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, false) ]);
// });
//
// it('startWithAudioVideoMuted=true can unmute', async () => {
// await hangupAllParticipants();
//
// const options = {
// configOverwrite: {
// startWithAudioMuted: true,
// startWithVideoMuted: true,
// p2p: {
// enabled: true
// }
// }
// };
//
// await ensureOneParticipant(options);
// await joinSecondParticipant({
// configOverwrite: {
// testing: {
// testMode: true,
// debugAudioLevels: true
// },
// p2p: {
// enabled: true
// }
// }
// });
//
// const { p1, p2 } = ctx;
//
// await p2.waitForIceConnected();
// await p2.waitForSendMedia();
//
// await p2.waitForAudioMuted(p1, true);
// await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
//
// // Unmute p1's both audio and video and check on p2.
// await p1.getToolbar().clickAudioUnmuteButton();
// await p2.waitForAudioMuted(p1, false);
//
// await unmuteVideoAndCheck(p1, p2);
// await p2.getLargeVideo().assertPlaying();
// });
//
//
// it('test p2p JVB switch and switch back', async () => {
// const { p1, p2 } = ctx;
//
// // Mute p2's video just before p3 joins.
// await p2.getToolbar().clickVideoMuteButton();
//
// await joinThirdParticipant({
// configOverwrite: {
// p2p: {
// enabled: true
// }
// }
// });
//
// const { p3 } = ctx;
//
// // Unmute p2 and check if its video is being received by p1 and p3.
// await unmuteVideoAndCheck(p2, p3);
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
//
// // Mute p2's video just before p3 leaves.
// await p2.getToolbar().clickVideoMuteButton();
//
// await p3.hangup();
//
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
//
// await p2.getToolbar().clickVideoUnmuteButton();
//
// // Check if p2's video is playing on p1.
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
// await p1.getLargeVideo().assertPlaying();
// });
});

View File

@@ -1,5 +1,6 @@
import process from 'node:process';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
import { cleanup, dialIn, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
@@ -12,9 +13,18 @@ describe('Dial-In', () => {
return;
}
await ensureOneParticipant({ preferGenerateToken: true });
// This is a temporary hack to avoid failing when running against a jaas env. The same cases are covered in
// jaas/dial/dialin.spec.ts.
if (testsConfig.jaas.enabled) {
ctx.skipSuiteTests = true;
return;
}
await ensureOneParticipant();
expect(await ctx.p1.isInMuc()).toBe(true);
// check dial-in is enabled
if (!await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
@@ -41,7 +51,7 @@ describe('Dial-In', () => {
});
it('invite dial-in participant', async () => {
await dialIn(ctx.p1);
await dialIn(await ctx.p1.getDialInPin());
});
it('wait for audio from dial-in participant', async () => {

View File

@@ -1,85 +1,41 @@
import { Participant } from '../../helpers/Participant';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
import { isDialInEnabled } from '../helpers/DialIn';
import { assertDialInDisplayed, assertUrlDisplayed, isDialInEnabled, verifyMoreNumbersPage } from '../helpers/DialIn';
describe('Invite', () => {
it('join participant', () => ensureOneParticipant({ preferGenerateToken: true }));
let p1: Participant;
let dialInEnabled: boolean;
it('url displayed', async () => {
const { p1 } = ctx;
const inviteDialog = p1.getInviteDialog();
it('setup', async () => {
// This is a temporary hack to avoid failing when running against a jaas env. The same cases are covered in
// jaas/dial/dialin.spec.ts.
if (testsConfig.jaas.enabled) {
ctx.skipSuiteTests = true;
await inviteDialog.open();
await inviteDialog.waitTillOpen();
return;
}
const driverUrl = await p1.driver.getUrl();
await ensureOneParticipant();
expect(driverUrl.includes(await inviteDialog.getMeetingURL())).toBe(true);
await inviteDialog.clickCloseButton();
await inviteDialog.waitTillOpen(true);
p1 = ctx.p1;
dialInEnabled = await isDialInEnabled(p1);
});
it('url displayed', () => assertUrlDisplayed(p1));
it('dial-in displayed', async () => {
const { p1 } = ctx;
if (!await isDialInEnabled(p1)) {
if (!dialInEnabled) {
return;
}
const inviteDialog = p1.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
await assertDialInDisplayed(p1);
});
it('view more numbers', async () => {
const { p1 } = ctx;
if (!await isDialInEnabled(p1)) {
it('view more numbers page', async () => {
if (!dialInEnabled) {
return;
}
const inviteDialog = p1.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
const windows = await p1.driver.getWindowHandles();
expect(windows.length).toBe(1);
const meetingWindow = windows[0];
const displayedNumber = await inviteDialog.getDialInNumber();
const displayedPin = await inviteDialog.getPinNumber();
await inviteDialog.openDialInNumbersPage();
const newWindow = (await p1.driver.getWindowHandles()).filter(w => w !== meetingWindow);
expect(newWindow.length).toBe(1);
const moreNumbersWindow = newWindow[0];
await p1.driver.switchWindow(moreNumbersWindow);
await browser.pause(10000);
await p1.driver.$('.dial-in-numbers-list').waitForExist();
const conferenceIdMessage = p1.driver.$('//div[contains(@class, "pinLabel")]');
expect((await conferenceIdMessage.getText()).replace(/ /g, '').includes(displayedPin)).toBe(true);
const numbers = p1.driver.$$('.dial-in-number');
const nums = await numbers.filter(
async el => (await el.getText()).trim() === displayedNumber);
expect(nums.length).toBe(1);
await verifyMoreNumbersPage(p1);
});
});

View File

@@ -52,17 +52,10 @@ export async function isDialInEnabled(participant: Participant) {
/**
* Sends a request to the REST API to dial in the participant using the provided pin.
* @param participant
* @param pin the pin to use when dialing in
*/
export async function dialIn(participant: Participant) {
if (!await participant.isInMuc()) {
// local participant did not join abort
return;
}
const dialInPin = await participant.getDialInPin();
const restUrl = process.env.DIAL_IN_REST_URL?.replace('{0}', dialInPin);
export async function dialIn(pin: string) {
const restUrl = process.env.DIAL_IN_REST_URL?.replace('{0}', pin);
// 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) => {
@@ -88,3 +81,66 @@ export async function dialIn(participant: Participant) {
console.log(`dial-in.test.call_session_history_id:${JSON.parse(responseData).call_session_history_id}`);
console.log(`API response:${responseData}`);
}
export async function assertUrlDisplayed(p: Participant) {
const inviteDialog = p.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
const driverUrl = await p.driver.getUrl();
expect(driverUrl.includes(await inviteDialog.getMeetingURL())).toBe(true);
await inviteDialog.clickCloseButton();
await inviteDialog.waitTillOpen(true);
}
export async function assertDialInDisplayed(p: Participant) {
const inviteDialog = p.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
}
export async function verifyMoreNumbersPage(p: Participant) {
const inviteDialog = p.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
const windows = await p.driver.getWindowHandles();
expect(windows.length).toBe(1);
const meetingWindow = windows[0];
const displayedNumber = await inviteDialog.getDialInNumber();
const displayedPin = await inviteDialog.getPinNumber();
await inviteDialog.openDialInNumbersPage();
const newWindow = (await p.driver.getWindowHandles()).filter(w => w !== meetingWindow);
expect(newWindow.length).toBe(1);
const moreNumbersWindow = newWindow[0];
await p.driver.switchWindow(moreNumbersWindow);
await browser.pause(10000);
await p.driver.$('.dial-in-numbers-list').waitForExist();
const conferenceIdMessage = p.driver.$('//div[contains(@class, "pinLabel")]');
expect((await conferenceIdMessage.getText()).replace(/ /g, '').includes(displayedPin)).toBe(true);
const numbers = p.driver.$$('.dial-in-number');
const nums = await numbers.filter(
async el => (await el.getText()).trim() === displayedNumber);
expect(nums.length).toBe(1);
await p.driver.switchWindow(meetingWindow);
}

View File

@@ -0,0 +1,64 @@
import { Participant } from '../../helpers/Participant';
/**
* Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
* conference participants sees a specific mute state for the former.
*
* @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
* mute state is to be toggled.
* @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
* the mute state of {@code testee}.
* @returns {Promise<void>}
*/
export async function muteAudioAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickAudioMuteButton();
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
await observer.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee);
await testee.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee);
}
/**
* Unmute audio, checks if the local UI has been updated accordingly and then does the verification from
* the other observer participant perspective.
* @param testee
* @param observer
*/
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
await testee.getNotifications().closeAskToUnmuteNotification(true);
await testee.getNotifications().closeAVModerationMutedNotification(true);
await testee.getToolbar().clickAudioUnmuteButton();
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
await testee.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee, true);
await observer.getParticipantsPane().assertAudioMuteIconIsDisplayed(testee, true);
}
/**
* Stop the video on testee and check on observer.
* @param testee
* @param observer
*/
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickVideoUnmuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
}
/**
* Starts the video on testee and check on observer.
* @param testee
* @param observer
*/
export async function muteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickVideoMuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
}

View File

@@ -2,20 +2,19 @@ import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
import { fetchJson } from '../../helpers/utils';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { joinMuc } from '../../helpers/joinMuc';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Chat', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants();
describe('iFrame API for Chat', () => {
let p1: Participant, p2: Participant;
const { p1, p2 } = ctx;
it('setup', async () => {
p1 = await joinMuc({ name: 'p1', iFrameApi: true, token: testsConfig.jwt.preconfiguredToken });
p2 = await joinMuc({ name: 'p2', iFrameApi: true });
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
@@ -24,19 +23,11 @@ describe('Chat', () => {
return;
}
// let's populate endpoint ids
await Promise.all([
p1.getEndpointId(),
p2.getEndpointId()
]);
await p1.switchToMainFrame();
await p2.switchToMainFrame();
});
it('send message', async () => {
const { p1, p2 } = ctx;
await p1.switchToMainFrame();
await p2.switchToMainFrame();
await p2.getIframeAPI().addEventListener('chatUpdated');
await p2.getIframeAPI().addEventListener('incomingMessage');
await p1.getIframeAPI().addEventListener('outgoingMessage');
@@ -65,7 +56,7 @@ describe('Chat', () => {
privateMessage: boolean;
} = await p2.getIframeAPI().getEventResult('incomingMessage');
expect(incomingMessageEvent).toEqual({
expect(incomingMessageEvent).toMatchObject({
from: await p1.getEndpointId(),
message: testMessage,
nick: p1.name,
@@ -88,8 +79,6 @@ describe('Chat', () => {
});
it('toggle chat', async () => {
const { p1, p2 } = ctx;
await p2.getIframeAPI().executeCommand('toggleChat');
await testSendGroupMessageWithChatOpen(p1, p2);
@@ -100,13 +89,10 @@ describe('Chat', () => {
});
it('private chat', async () => {
const { p1, p2 } = ctx;
const testMessage = 'Hello private world!';
const p2Id = await p2.getEndpointId();
const p1Id = await p1.getEndpointId();
await p1.getIframeAPI().executeCommand('initiatePrivateChat', p2Id);
await p1.getIframeAPI().executeCommand('sendChatMessage', testMessage, p2Id);
await p1.getIframeAPI().executeCommand('initiatePrivateChat', await p2.getEndpointId());
await p1.getIframeAPI().executeCommand('sendChatMessage', testMessage, await p2.getEndpointId());
const incomingMessageEvent = await p2.driver.waitUntil(
() => p2.getIframeAPI().getEventResult('incomingMessage'), {
@@ -114,8 +100,8 @@ describe('Chat', () => {
timeoutMsg: 'Chat was not received'
});
expect(incomingMessageEvent).toEqual({
from: p1Id,
expect(incomingMessageEvent).toMatchObject({
from: await p1.getEndpointId(),
message: testMessage,
nick: p1.name,
privateMessage: true
@@ -133,47 +119,22 @@ describe('Chat', () => {
await testSendGroupMessageWithChatOpen(p1, p2);
});
it('chat upload chat', async () => {
const { p1, p2, webhooksProxy } = ctx;
await p1.getIframeAPI().executeCommand('hangup');
await p2.getIframeAPI().executeCommand('hangup');
if (webhooksProxy) {
const event: {
data: {
preAuthenticatedLink: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('CHAT_UPLOADED');
expect('CHAT_UPLOADED').toBe(event.eventType);
expect(event.data.preAuthenticatedLink).toBeDefined();
const uploadedChat: any = await fetchJson(event.data.preAuthenticatedLink);
expect(uploadedChat.messageType).toBe('CHAT');
expect(uploadedChat.messages).toBeDefined();
expect(uploadedChat.messages.length).toBe(3);
}
});
});
/**
* Test sending a group message with the chat open.
* @param p1
* @param p2
* Send a group message from [sender], verify that it was received correctly by [receiver].
* @param sender the Participant that sends the message.
* @param receiver the Participant that receives the message.
*/
async function testSendGroupMessageWithChatOpen(p1: Participant, p2: Participant) {
async function testSendGroupMessageWithChatOpen(sender: Participant, receiver: Participant) {
const testMessage = 'Hello world again';
await p1.getIframeAPI().executeCommand('sendChatMessage', testMessage);
await sender.getIframeAPI().executeCommand('sendChatMessage', testMessage);
const chatUpdatedEvent: {
isOpen: boolean;
unreadCount: number;
} = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('chatUpdated'), {
} = await receiver.driver.waitUntil(() => receiver.getIframeAPI().getEventResult('chatUpdated'), {
timeout: 3000,
timeoutMsg: 'Chat was not updated'
});
@@ -183,16 +144,16 @@ async function testSendGroupMessageWithChatOpen(p1: Participant, p2: Participant
unreadCount: 0
});
const incomingMessageEvent = await p2.driver.waitUntil(
() => p2.getIframeAPI().getEventResult('incomingMessage'), {
const incomingMessageEvent = await receiver.driver.waitUntil(
() => receiver.getIframeAPI().getEventResult('incomingMessage'), {
timeout: 3000,
timeoutMsg: 'Chat was not received'
});
expect(incomingMessageEvent).toEqual({
from: await p1.getEndpointId(),
expect(incomingMessageEvent).toMatchObject({
from: await sender.getEndpointId(),
message: testMessage,
nick: p1.name,
nick: sender.name,
privateMessage: false
});
}

View File

@@ -1,214 +0,0 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
import {
cleanup,
dialIn,
isDialInEnabled,
waitForAudioFromDialInParticipant
} from '../helpers/DialIn';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
const customerId = testsConfig.iframe.customerId;
describe('Invite iframeAPI', () => {
let dialInDisabled: boolean;
let dialOutDisabled: boolean;
let sipJibriDisabled: boolean;
it('join participant', async () => {
await ensureOneParticipant();
const { p1 } = ctx;
// check for dial-in dial-out sip-jibri maybe
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
return;
}
dialOutDisabled = Boolean(!await p1.execute(() => config.dialOutAuthUrl));
sipJibriDisabled = Boolean(!await p1.execute(() => config.inviteServiceUrl));
// check dial-in is enabled
if (!await isDialInEnabled(ctx.p1) || !process.env.DIAL_IN_REST_URL) {
dialInDisabled = true;
}
});
it('dial-in', async () => {
if (dialInDisabled) {
return;
}
const { p1 } = ctx;
const dialInPin = await p1.getDialInPin();
expect(dialInPin.length >= 8).toBe(true);
await dialIn(p1);
if (!await p1.isInMuc()) {
// local participant did not join abort
return;
}
await waitForAudioFromDialInParticipant(p1);
await checkDialEvents(p1, 'in', 'DIAL_IN_STARTED', 'DIAL_IN_ENDED');
});
it('dial-out', async () => {
if (dialOutDisabled || !process.env.DIAL_OUT_URL) {
return;
}
const { p1 } = ctx;
await p1.switchToMainFrame();
await p1.getIframeAPI().invitePhone(process.env.DIAL_OUT_URL);
await p1.switchToIFrame();
await p1.waitForParticipants(1);
await waitForAudioFromDialInParticipant(p1);
await checkDialEvents(p1, 'out', 'DIAL_OUT_STARTED', 'DIAL_OUT_ENDED');
});
it('sip jibri', async () => {
if (sipJibriDisabled || !process.env.SIP_JIBRI_DIAL_OUT_URL) {
return;
}
const { p1 } = ctx;
await p1.switchToMainFrame();
await p1.getIframeAPI().inviteSIP(process.env.SIP_JIBRI_DIAL_OUT_URL);
await p1.switchToIFrame();
await p1.waitForParticipants(1);
await waitForAudioFromDialInParticipant(p1);
const { webhooksProxy } = ctx;
if (webhooksProxy) {
const sipCallOutStartedEvent: {
customerId: string;
data: {
participantFullJid: string;
participantId: string;
participantJid: string;
sipAddress: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_STARTED');
expect('SIP_CALL_OUT_STARTED').toBe(sipCallOutStartedEvent.eventType);
expect(sipCallOutStartedEvent.data.sipAddress).toBe(`sip:${process.env.SIP_JIBRI_DIAL_OUT_URL}`);
expect(sipCallOutStartedEvent.customerId).toBe(customerId);
const participantId = sipCallOutStartedEvent.data.participantId;
const participantJid = sipCallOutStartedEvent.data.participantJid;
const participantFullJid = sipCallOutStartedEvent.data.participantFullJid;
await cleanup(p1);
const sipCallOutEndedEvent: {
customerId: string;
data: {
direction: string;
participantFullJid: string;
participantId: string;
participantJid: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_ENDED');
expect('SIP_CALL_OUT_ENDED').toBe(sipCallOutEndedEvent.eventType);
expect(sipCallOutEndedEvent.customerId).toBe(customerId);
expect(sipCallOutEndedEvent.data.participantFullJid).toBe(participantFullJid);
expect(sipCallOutEndedEvent.data.participantId).toBe(participantId);
expect(sipCallOutEndedEvent.data.participantJid).toBe(participantJid);
} else {
await cleanup(p1);
}
});
});
/**
* Checks the dial events for a participant and clean up at the end.
* @param participant
* @param startedEventName
* @param endedEventName
* @param direction
*/
async function checkDialEvents(participant: Participant, direction: string, startedEventName: string, endedEventName: string) {
const { webhooksProxy } = ctx;
if (webhooksProxy) {
const dialInStartedEvent: {
customerId: string;
data: {
direction: string;
participantFullJid: string;
participantId: string;
participantJid: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent(startedEventName);
expect(startedEventName).toBe(dialInStartedEvent.eventType);
expect(dialInStartedEvent.data.direction).toBe(direction);
expect(dialInStartedEvent.customerId).toBe(customerId);
const participantId = dialInStartedEvent.data.participantId;
const participantJid = dialInStartedEvent.data.participantJid;
const participantFullJid = dialInStartedEvent.data.participantFullJid;
const usageEvent: {
customerId: string;
data: any;
eventType: string;
} = await webhooksProxy.waitForEvent('USAGE');
expect('USAGE').toBe(usageEvent.eventType);
expect(usageEvent.customerId).toBe(customerId);
expect(usageEvent.data.some((el: any) =>
el.participantId === participantId && el.callDirection === direction)).toBe(true);
await cleanup(participant);
const dialInEndedEvent: {
customerId: string;
data: {
direction: string;
participantFullJid: string;
participantId: string;
participantJid: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent(endedEventName);
expect(endedEventName).toBe(dialInEndedEvent.eventType);
expect(dialInEndedEvent.customerId).toBe(customerId);
expect(dialInEndedEvent.data.participantFullJid).toBe(participantFullJid);
expect(dialInEndedEvent.data.participantId).toBe(participantId);
expect(dialInEndedEvent.data.participantJid).toBe(participantJid);
} else {
await cleanup(participant);
}
}

View File

@@ -1,64 +1,18 @@
import { isEqual } from 'lodash-es';
import { P1, P2, Participant } from '../../helpers/Participant';
import { P1, P2 } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
/**
* Tests PARTICIPANT_LEFT webhook.
*/
async function checkParticipantLeftHook(p: Participant, reason: string, checkId = false, conferenceJid: string) {
const { webhooksProxy } = ctx;
if (webhooksProxy) {
// PARTICIPANT_LEFT webhook
// @ts-ignore
const event: {
customerId: string;
data: {
conference: string;
disconnectReason: string;
group: string;
id: string;
isBreakout: boolean;
name: string;
participantId: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
expect('PARTICIPANT_LEFT').toBe(event.eventType);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.disconnectReason).toBe(reason);
expect(event.data.isBreakout).toBe(false);
expect(event.data.participantId).toBe(await p.getEndpointId());
expect(event.data.name).toBe(p.name);
if (checkId) {
const jwtPayload = p.getToken()?.payload;
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
expect(event.data.group).toBe(jwtPayload?.context?.group);
expect(event.customerId).toBe(testsConfig.iframe.customerId);
}
}
}
describe('Participants presence', () => {
let conferenceJid: string = '';
it('joining the meeting', async () => {
// ensure 2 participants one moderator and one guest, we will load both with iframeAPI
await ensureTwoParticipants();
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p1, p2, webhooksProxy } = ctx;
const { p1, p2 } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
@@ -67,44 +21,20 @@ describe('Participants presence', () => {
return;
}
// let's populate endpoint ids
await Promise.all([
p1.getEndpointId(),
p2.getEndpointId()
p1.switchToMainFrame(),
p2.switchToMainFrame()
]);
await p1.switchToMainFrame();
await p2.switchToMainFrame();
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
expect(await p2.getIframeAPI().getEventResult('isModerator')).toBe(false);
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
if (webhooksProxy) {
// USAGE webhook
// @ts-ignore
const event: {
data: [
{ participantId: string; }
];
eventType: string;
} = await webhooksProxy.waitForEvent('USAGE');
expect('USAGE').toBe(event.eventType);
const p1EpId = await p1.getEndpointId();
const p2EpId = await p2.getEndpointId();
expect(event.data.filter(d => d.participantId === p1EpId
|| d.participantId === p2EpId).length).toBe(2);
}
});
it('participants info',
async () => {
const { p1, roomName, webhooksProxy } = ctx;
const { p1, roomName } = ctx;
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
expect(roomsInfo).toBeDefined();
@@ -116,9 +46,6 @@ describe('Participants presence', () => {
expect(roomNode).toBe(roomName);
const { node, resource } = parseJid(roomsInfo.jid);
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
const p1EpId = await p1.getEndpointId();
expect(node).toBe(roomName);
@@ -126,30 +53,12 @@ describe('Participants presence', () => {
expect(roomsInfo.participants.length).toBe(2);
expect(await p1.getIframeAPI().getNumberOfParticipants()).toBe(2);
if (webhooksProxy) {
// ROOM_CREATED webhook
// @ts-ignore
const event: {
data: {
conference: string;
isBreakout: boolean;
};
eventType: string;
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
expect('ROOM_CREATED').toBe(event.eventType);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
}
}
);
it('participants pane', async () => {
const { p1 } = ctx;
await p1.switchToMainFrame();
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
await p1.getIframeAPI().addEventListener('participantsPaneToggled');
@@ -163,68 +72,7 @@ describe('Participants presence', () => {
expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(false);
});
it('grant moderator', async () => {
const { p1, p2, webhooksProxy } = ctx;
const p2EpId = await p2.getEndpointId();
await p1.getIframeAPI().clearEventResults('participantRoleChanged');
await p2.getIframeAPI().clearEventResults('participantRoleChanged');
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('isModerator'), {
timeout: 3000,
timeoutMsg: 'Moderator role not granted'
});
type RoleChangedEvent = {
id: string;
role: string;
};
const event1: RoleChangedEvent = await p1.driver.waitUntil(
() => p1.getIframeAPI().getEventResult('participantRoleChanged'), {
timeout: 3000,
timeoutMsg: 'Role was not update on p1 side'
});
expect(event1?.id).toBe(p2EpId);
expect(event1?.role).toBe('moderator');
const event2: RoleChangedEvent = await p2.driver.waitUntil(
() => p2.getIframeAPI().getEventResult('participantRoleChanged'), {
timeout: 3000,
timeoutMsg: 'Role was not update on p2 side'
});
expect(event2?.id).toBe(p2EpId);
expect(event2?.role).toBe('moderator');
if (webhooksProxy) {
// ROLE_CHANGED webhook
// @ts-ignore
const event: {
data: {
grantedBy: {
participantId: string;
};
grantedTo: {
participantId: string;
};
role: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('ROLE_CHANGED');
expect('ROLE_CHANGED').toBe(event.eventType);
expect(event.data.role).toBe('moderator');
expect(event.data.grantedBy.participantId).toBe(await p1.getEndpointId());
expect(event.data.grantedTo.participantId).toBe(await p2.getEndpointId());
}
});
it('kick participant', async () => {
// we want to join second participant with token, so we can check info in webhook
await ctx.p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await ctx.p2.getIframeAPI().addEventListener('videoConferenceLeft');
await ctx.p2.switchToMainFrame();
@@ -235,13 +83,9 @@ describe('Participants presence', () => {
timeoutMsg: 'videoConferenceLeft not received'
});
await ensureTwoParticipants({
preferGenerateToken: true
});
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p1, p2, roomName, webhooksProxy } = ctx;
webhooksProxy?.clearCache();
const { p1, p2, roomName } = ctx;
const p1EpId = await p1.getEndpointId();
const p2EpId = await p2.getEndpointId();
@@ -252,10 +96,6 @@ describe('Participants presence', () => {
await p1.switchToMainFrame();
await p2.switchToMainFrame();
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
await p1.getIframeAPI().addEventListener('participantKickedOut');
await p2.getIframeAPI().addEventListener('participantKickedOut');
@@ -273,8 +113,6 @@ describe('Participants presence', () => {
timeoutMsg: 'participantKickedOut event not received on p2 side'
});
await checkParticipantLeftHook(p2, 'kicked', true, conferenceJid);
expect(eventP1).toBeDefined();
expect(eventP2).toBeDefined();
@@ -314,40 +152,15 @@ describe('Participants presence', () => {
});
it('join after kick', async () => {
const { p1, webhooksProxy } = ctx;
const { p1 } = ctx;
await p1.getIframeAPI().addEventListener('participantJoined');
await p1.getIframeAPI().addEventListener('participantMenuButtonClick');
webhooksProxy?.clearCache();
// join again
await ensureTwoParticipants();
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p2 } = ctx;
if (webhooksProxy) {
// PARTICIPANT_JOINED webhook
// @ts-ignore
const event: {
data: {
conference: string;
isBreakout: boolean;
moderator: boolean;
name: string;
participantId: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
expect('PARTICIPANT_JOINED').toBe(event.eventType);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
expect(event.data.moderator).toBe(false);
expect(event.data.name).toBe(await p2.getLocalDisplayName());
expect(event.data.participantId).toBe(await p2.getEndpointId());
expect(event.data.name).toBe(p2.name);
}
await p1.switchToMainFrame();
const event = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantJoined'), {
@@ -411,8 +224,6 @@ describe('Participants presence', () => {
expect(eventConferenceLeftP2).toBeDefined();
expect(eventConferenceLeftP2.roomName).toBe(roomName);
await checkParticipantLeftHook(p2, 'left', false, conferenceJid);
const eventReadyToCloseP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('readyToClose'), {
timeout: 2000,
timeoutMsg: 'readyToClose not received'
@@ -422,7 +233,7 @@ describe('Participants presence', () => {
});
it('dispose conference', async () => {
const { p1, roomName, webhooksProxy } = ctx;
const { p1, roomName } = ctx;
await p1.switchToMainFrame();
@@ -441,23 +252,6 @@ describe('Participants presence', () => {
expect(eventConferenceLeft).toBeDefined();
expect(eventConferenceLeft.roomName).toBe(roomName);
await checkParticipantLeftHook(p1, 'left', true, conferenceJid);
if (webhooksProxy) {
// ROOM_DESTROYED webhook
// @ts-ignore
const event: {
data: {
conference: string;
isBreakout: boolean;
};
eventType: string;
} = await webhooksProxy.waitForEvent('ROOM_DESTROYED');
expect('ROOM_DESTROYED').toBe(event.eventType);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
}
const eventReadyToClose = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('readyToClose'), {
timeout: 2000,
timeoutMsg: 'readyToClose not received'

View File

@@ -1,125 +0,0 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Visitors', () => {
it('joining the meeting', async () => {
const { webhooksProxy } = ctx;
if (webhooksProxy) {
webhooksProxy.defaultMeetingSettings = {
visitorsEnabled: true
};
}
await ensureOneParticipant();
const { p1 } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled or visitors are not supported
ctx.skipSuiteTests = true;
return;
}
await p1.driver.waitUntil(() => p1.execute(() => APP.conference._room.isVisitorsSupported()), {
timeout: 2000
}).then(async () => {
await p1.switchToMainFrame();
}).catch(() => {
ctx.skipSuiteTests = true;
});
});
it('visitor joins', async () => {
await ensureTwoParticipants({
preferGenerateToken: true,
tokenOptions: { visitor: true },
skipInMeetingChecks: true
});
const { p1, p2, webhooksProxy } = ctx;
await p2.waitForReceiveMedia(15_000, 'Visitor is not receiving media');
await p2.waitForRemoteStreams(1);
const p2Visitors = p2.getVisitors();
const p1Visitors = p1.getVisitors();
await p2.driver.waitUntil(() => p2Visitors.hasVisitorsDialog(), {
timeout: 5000,
timeoutMsg: 'Missing visitors dialog'
});
expect((await p1Visitors.getVisitorsCount()).trim()).toBe('1');
expect((await p1Visitors.getVisitorsHeaderFromParticipantsPane()).trim()).toBe('Viewers 1');
if (webhooksProxy) {
// PARTICIPANT_JOINED webhook
// @ts-ignore
const event: {
customerId: string;
data: {
avatar: string;
email: string;
group: string;
id: string;
name: string;
participantJid: string;
role: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
const jwtPayload = p2.getToken()?.payload;
expect('PARTICIPANT_JOINED').toBe(event.eventType);
expect(event.data.avatar).toBe(jwtPayload.context.user.avatar);
expect(event.data.email).toBe(jwtPayload.context.user.email);
expect(event.data.id).toBe(jwtPayload.context.user.id);
expect(event.data.group).toBe(jwtPayload.context.group);
expect(event.data.name).toBe(p2.name);
expect(event.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
expect(event.data.name).toBe(p2.name);
expect(event.data.role).toBe('visitor');
expect(event.customerId).toBe(testsConfig.iframe.customerId);
await p2.switchToMainFrame();
await p2.getIframeAPI().executeCommand('hangup');
// PARTICIPANT_LEFT webhook
// @ts-ignore
const eventLeft: {
customerId: string;
data: {
avatar: string;
email: string;
group: string;
id: string;
name: string;
participantJid: string;
role: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
expect('PARTICIPANT_LEFT').toBe(eventLeft.eventType);
expect(eventLeft.data.avatar).toBe(jwtPayload.context.user.avatar);
expect(eventLeft.data.email).toBe(jwtPayload.context.user.email);
expect(eventLeft.data.id).toBe(jwtPayload.context.user.id);
expect(eventLeft.data.group).toBe(jwtPayload.context.group);
expect(eventLeft.data.name).toBe(p2.name);
expect(eventLeft.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
expect(eventLeft.data.name).toBe(p2.name);
expect(eventLeft.data.role).toBe('visitor');
expect(eventLeft.customerId).toBe(testsConfig.iframe.customerId);
}
});
});

View File

@@ -0,0 +1,72 @@
import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import WebhookProxy from '../../helpers/WebhookProxy';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { fetchJson } from '../../helpers/utils';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
describe('JaaS CHAT_UPLOADED webhook.', () => {
const tenant = testsConfig.jaas.tenant;
const customerId = tenant?.replace('vpaas-magic-cookie-', '');
let p1: Participant, p2: Participant;
let webhooksProxy: WebhookProxy;
let fqn: string;
it('setup', async () => {
const room = ctx.roomName;
webhooksProxy = ctx.webhooksProxy;
p1 = await joinJaasMuc({ name: 'p1', token: t({ room }) });
p2 = await joinJaasMuc({ name: 'p2', token: t({ room }) });
fqn = `${testsConfig.jaas.tenant}/${room}`;
});
it('test webhook', async () => {
await p1.getChatPanel().sendMessage('foo');
await p2.getChatPanel().sendMessage('bar');
await p1.getChatPanel().sendMessage('baz');
await p1.hangup();
await p2.hangup();
const event: {
appId: string;
customerId: string;
data: {
preAuthenticatedLink: string;
};
eventType: string;
fqn: string;
} = await webhooksProxy.waitForEvent('CHAT_UPLOADED');
expect(event.appId).toBe(tenant);
expect(event.customerId).toBe(customerId);
expect(event.data.preAuthenticatedLink).toBeDefined();
expect(event.eventType).toBe('CHAT_UPLOADED');
expect(event.fqn).toBe(fqn);
const uploadedChat: any = await fetchJson(event.data.preAuthenticatedLink);
expect(uploadedChat.meetingFqn).toBe(fqn);
expect(uploadedChat.messageType).toBe('CHAT');
const messages = uploadedChat.messages;
expect(messages).toBeDefined();
expect(messages.length).toBe(3);
expect(messages[0].content).toBe('foo');
expect(messages[0].name).toBe('p1');
expect(messages[1].content).toBe('bar');
expect(messages[1].name).toBe('p2');
expect(messages[2].content).toBe('baz');
expect(messages[2].name).toBe('p1');
});
});

View File

@@ -0,0 +1,64 @@
import type { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { config as testsConfig } from '../../../helpers/TestsConfig';
import WebhookProxy from '../../../helpers/WebhookProxy';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import {
assertDialInDisplayed, assertUrlDisplayed,
dialIn,
isDialInEnabled, verifyMoreNumbersPage,
} from '../../helpers/DialIn';
import { verifyEndedWebhook, verifyStartedWebhooks, waitForMedia } from './util';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
});
describe('Dial-in', () => {
let p1: Participant, webhooksProxy: WebhookProxy;
const customerId: string = testsConfig.jaas.customerId || '';
it('setup', async () => {
const room = ctx.roomName;
if (!process.env.DIAL_IN_REST_URL) {
console.log('Dial-in test is disabled, set DIAL_IN_REST_URL to enable.');
ctx.skipSuiteTests = true;
return;
}
p1 = await joinJaasMuc({ name: 'p1', token: t({ room, moderator: true }) });
webhooksProxy = ctx.webhooksProxy;
expect(await p1.isInMuc()).toBe(true);
expect(await isDialInEnabled(p1)).toBe(true);
expect(customerId).toBeDefined();
});
it ('Invite UI', async () => {
await assertUrlDisplayed(p1);
await assertDialInDisplayed(p1);
await verifyMoreNumbersPage(p1);
});
it('dial-in', async () => {
const dialInPin = await p1.getDialInPin();
expect(dialInPin.length >= 8).toBe(true);
await dialIn(dialInPin);
await waitForMedia(p1);
const startedPayload
= await verifyStartedWebhooks(webhooksProxy, 'in', 'DIAL_IN_STARTED', customerId);
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
await p1.getFilmstrip().kickParticipant(endpointId);
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
});
});

View File

@@ -0,0 +1,51 @@
import type { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { config as testsConfig } from '../../../helpers/TestsConfig';
import WebhookProxy from '../../../helpers/WebhookProxy';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import { verifyEndedWebhook, verifyStartedWebhooks, waitForMedia } from './util';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
});
describe('Dial-out', () => {
let p1: Participant, webhooksProxy: WebhookProxy;
const dialOutUrl = process.env.DIAL_OUT_URL || '';
const customerId = testsConfig.jaas.customerId || '';
it('setup', async () => {
const room = ctx.roomName;
if (!dialOutUrl) {
console.log('Dial-out test is disabled, set DIAL_OUT_URL to enable.');
ctx.skipSuiteTests = true;
return;
}
webhooksProxy = ctx.webhooksProxy;
p1 = await joinJaasMuc({ name: 'p1', iFrameApi: true, token: t({ room, moderator: true }) });
expect(await p1.isInMuc()).toBe(true);
expect(Boolean(await p1.execute(() => config.dialOutAuthUrl))).toBe(true);
});
it('dial-out', async () => {
await p1.switchToMainFrame();
await p1.getIframeAPI().invitePhone(dialOutUrl);
await p1.switchToIFrame();
await p1.waitForParticipants(1);
await waitForMedia(p1);
const startedPayload
= await verifyStartedWebhooks(webhooksProxy, 'out', 'DIAL_OUT_STARTED', customerId);
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
await p1.getFilmstrip().kickParticipant(endpointId);
await verifyEndedWebhook(webhooksProxy, 'DIAL_OUT_ENDED', customerId, startedPayload);
});
});

View File

@@ -0,0 +1,88 @@
import type { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { config as testsConfig } from '../../../helpers/TestsConfig';
import WebhookProxy from '../../../helpers/WebhookProxy';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import { waitForMedia } from './util';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
});
describe('SIP jibri invite', () => {
let p1: Participant, webhooksProxy: WebhookProxy;
const customerId = testsConfig.jaas.customerId || '';
const dialOutUrl = process.env.SIP_JIBRI_DIAL_OUT_URL || '';
it('setup', async () => {
const room = ctx.roomName;
if (true) {
// This is temporary until we figure out how to fix it and configure it properly.
console.log('SIP jibri test is disabled.');
ctx.skipSuiteTests = true;
return;
}
if (!dialOutUrl) {
console.log('SIP jibri test is disabled, set SIP_JIBRI_DIAL_OUT_URL to enable.');
ctx.skipSuiteTests = true;
return;
}
p1 = await joinJaasMuc({ name: 'p1', iFrameApi: true, token: t({ room, moderator: true }) });
webhooksProxy = ctx.webhooksProxy;
expect(await p1.isInMuc()).toBe(true);
expect(Boolean(await p1.execute(() => config.inviteServiceUrl))).toBe(true);
});
it('sip jibri', async () => {
await p1.switchToMainFrame();
await p1.getIframeAPI().inviteSIP(dialOutUrl);
await p1.switchToIFrame();
await p1.waitForParticipants(1);
await waitForMedia(p1);
const startedEvent: {
customerId: string;
data: {
participantFullJid: string;
participantId: string;
participantJid: string;
sipAddress: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_STARTED');
expect('SIP_CALL_OUT_STARTED').toBe(startedEvent.eventType);
expect(startedEvent.data.sipAddress).toBe(`sip:${process.env.SIP_JIBRI_DIAL_OUT_URL}`);
expect(startedEvent.customerId).toBe(customerId);
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
await p1.getFilmstrip().kickParticipant(endpointId);
const endedEvent: {
customerId: string;
data: {
direction: string;
participantFullJid: string;
participantId: string;
participantJid: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_ENDED');
expect('SIP_CALL_OUT_ENDED').toBe(endedEvent.eventType);
expect(endedEvent.customerId).toBe(customerId);
expect(endedEvent.data.participantFullJid).toBe(startedEvent.data.participantFullJid);
expect(endedEvent.data.participantId).toBe(startedEvent.data.participantId);
expect(endedEvent.data.participantJid).toBe(startedEvent.data.participantJid);
});
});

View File

@@ -0,0 +1,81 @@
import { Participant } from '../../../helpers/Participant';
import WebhookProxy from '../../../helpers/WebhookProxy';
interface IStartedWebhookPayload {
direction: string;
participantFullJid: string;
participantId: string;
participantJid: string;
}
/**
* Checks the dial events for a participant and clean up at the end.
* @param webhooksProxy
* @param startedEventName
* @param direction
* @param customerId
*/
export async function verifyStartedWebhooks(
webhooksProxy: WebhookProxy,
direction: 'in' | 'out',
startedEventName: string,
customerId: string): Promise<IStartedWebhookPayload> {
const startedEvent: {
customerId: string;
data: IStartedWebhookPayload;
eventType: string;
} = await webhooksProxy.waitForEvent(startedEventName);
expect(startedEventName).toBe(startedEvent.eventType);
expect(startedEvent.data.direction).toBe(direction);
expect(startedEvent.customerId).toBe(customerId);
const usageEvent: {
customerId: string;
data: any;
eventType: string;
} = await webhooksProxy.waitForEvent('USAGE');
expect('USAGE').toBe(usageEvent.eventType);
expect(usageEvent.customerId).toBe(customerId);
expect(usageEvent.data.some((el: any) =>
el.participantId === startedEvent.data.participantId && el.callDirection === direction)).toBe(true);
return startedEvent.data;
}
export async function verifyEndedWebhook(
webhooksProxy: WebhookProxy,
endedEventName: string,
customerId: string,
startedPayload: IStartedWebhookPayload) {
const endedEvent: {
customerId: string;
data: {
direction: string;
participantFullJid: string;
participantId: string;
participantJid: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent(endedEventName);
expect(endedEventName).toBe(endedEvent.eventType);
expect(endedEvent.customerId).toBe(customerId);
expect(endedEvent.data.participantFullJid).toBe(startedPayload.participantFullJid);
expect(endedEvent.data.participantId).toBe(startedPayload.participantId);
expect(endedEvent.data.participantJid).toBe(startedPayload.participantJid);
}
/**
* Wait until there is at least one remote participant, ICE is connected, the participant has a stream, and data is
* both received and sent.
*/
export async function waitForMedia(p: Participant) {
await p.waitForParticipants(1);
await p.waitForIceConnected();
await p.waitForRemoteStreams(1);
await p.waitForSendReceiveData(20_000);
}

View File

@@ -1,6 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { TOKEN_AUTH_FAILED_TEST_ID, TOKEN_AUTH_FAILED_TITLE_TEST_ID } from '../../pageobjects/Notifications';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
setTestProperties(__filename, {
useJaas: true
@@ -9,7 +9,7 @@ setTestProperties(__filename, {
describe('XMPP login and MUC join test', () => {
it('with a valid token (wildcard room)', async () => {
console.log('Joining a MUC with a valid token (wildcard room)');
const p = await joinMuc({ token: t({ room: '*' }) });
const p = await joinJaasMuc({ token: t({ room: '*' }) });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
@@ -17,7 +17,7 @@ describe('XMPP login and MUC join test', () => {
it('with a valid token (specific room)', async () => {
console.log('Joining a MUC with a valid token (specific room)');
const p = await joinMuc({ token: t({ room: ctx.roomName }) });
const p = await joinJaasMuc({ token: t({ room: ctx.roomName }) });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
@@ -29,7 +29,7 @@ describe('XMPP login and MUC join test', () => {
token.jwt = token.jwt + 'badSignature';
const p = await joinMuc({ token });
const p = await joinJaasMuc({ token });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -41,7 +41,7 @@ describe('XMPP login and MUC join test', () => {
it('with an expired token', async () => {
console.log('Joining a MUC with an expired token');
const p = await joinMuc({ token: t({ exp: '-1m' }) });
const p = await joinJaasMuc({ token: t({ exp: '-1m' }) });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -52,7 +52,7 @@ describe('XMPP login and MUC join test', () => {
it('with a token using the wrong key ID', async () => {
console.log('Joining a MUC with a token using the wrong key ID');
const p = await joinMuc({ token: t({ keyId: 'invalid-key-id' }) });
const p = await joinJaasMuc({ token: t({ keyId: 'invalid-key-id' }) });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -63,7 +63,7 @@ describe('XMPP login and MUC join test', () => {
it('with a token for a different room', async () => {
console.log('Joining a MUC with a token for a different room');
const p = await joinMuc({ token: t({ room: ctx.roomName + 'different' }) });
const p = await joinJaasMuc({ token: t({ room: ctx.roomName + 'different' }) });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -74,7 +74,7 @@ describe('XMPP login and MUC join test', () => {
it('with a moderator token', async () => {
console.log('Joining a MUC with a moderator token');
const p = await joinMuc({ token: t({ moderator: true }) });
const p = await joinJaasMuc({ token: t({ moderator: true }) });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(true);
@@ -84,7 +84,7 @@ describe('XMPP login and MUC join test', () => {
// disabled.
it('without a token', async () => {
console.log('Joining a MUC without a token');
const p = await joinMuc();
const p = await joinJaasMuc();
expect(Boolean(await p.isInMuc())).toBe(false);

View File

@@ -1,5 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -13,19 +13,19 @@ describe('MaxOccupants limit enforcement', () => {
maxOccupants: 2
};
const p1 = await joinMuc({ token: t({ room: ctx.roomName }) });
const p2 = await joinMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
const p1 = await joinJaasMuc({ token: t({ room: ctx.roomName }) });
const p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
expect(await p1.isInMuc()).toBe(true);
expect(await p2.isInMuc()).toBe(true);
// Third participant should be rejected (exceeding maxOccupants), even if it's a moderator
let p3 = await joinMuc({ name: 'p3', token: t({ room: ctx.roomName, moderator: true }) });
let p3 = await joinJaasMuc({ name: 'p3', token: t({ room: ctx.roomName, moderator: true }) });
expect(Boolean(await p3.isInMuc())).toBe(false);
await p1.hangup();
p3 = await joinMuc({ name: 'p3', token: t({ room: ctx.roomName }) });
p3 = await joinJaasMuc({ name: 'p3', token: t({ room: ctx.roomName }) });
expect(await p3.isInMuc()).toBe(true);
});
});

View File

@@ -1,6 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { IToken } from '../../helpers/token';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -31,7 +31,7 @@ describe('Setting passcode through settings provisioning', () => {
*/
async function joinWithPassword(instanceId: string, token: IToken) {
// @ts-ignore
const p = await joinMuc({ name: instanceId, token }, { roomName: ctx.roomName });
const p = await joinJaasMuc({ name: instanceId, token }, { roomName: ctx.roomName });
await p.waitForMucJoinedOrError();
expect(await p.isInMuc()).toBe(false);

View File

@@ -1,5 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -14,7 +14,7 @@ describe('Setting passcode through settings provisioning', () => {
passcode: 'passcode-must-be-digits-only'
};
const p = await joinMuc({ token: t({ room: ctx.roomName }) }, { roomName: ctx.roomName });
const p = await joinJaasMuc({ token: t({ room: ctx.roomName }) }, { roomName: ctx.roomName });
// The settings provisioning contains an invalid passcode, the expected result is that the room is not
// configured to require a passcode.

View File

@@ -0,0 +1,178 @@
import { expect } from '@wdio/globals';
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import WebhookProxy from '../../helpers/WebhookProxy';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
/**
* Tests the basic webhooks fired for participants joining, leaving, and creating/destroying a conference:
* PARTICIPANT_JOINED, PARTICIPANT_LEFT, ROOM_CREATED, ROOM_DESTROYED, ROLE_CHANGED, USAGE.
*/
describe('Create/destroy/join/leave webhooks', () => {
let conferenceJid: string = '';
let p1: Participant, p2: Participant;
let p1EpId: string, p2EpId: string;
let webhooksProxy: WebhookProxy;
let room: string;
async function checkParticipantJoinedHook(p: Participant) {
const event: {
data: {
conference: string;
isBreakout: boolean;
moderator: boolean;
name: string;
participantId: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
expect(event.eventType).toBe('PARTICIPANT_JOINED');
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
expect(event.data.moderator).toBe(p.getToken()?.options?.moderator);
expect(event.data.name).toBe(await p.getLocalDisplayName());
expect(event.data.participantId).toBe(await p.getEndpointId());
expect(event.data.name).toBe(p.name);
}
async function checkParticipantLeftHook(p: Participant, reason: string) {
const event: {
customerId: string;
data: {
conference: string;
disconnectReason: string;
group: string;
id: string;
isBreakout: boolean;
name: string;
participantId: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
expect(event.eventType).toBe('PARTICIPANT_LEFT');
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.disconnectReason).toBe(reason);
expect(event.data.isBreakout).toBe(false);
expect(event.data.participantId).toBe(await p.getEndpointId());
expect(event.data.name).toBe(p.name);
const jwtPayload = p.getToken()?.payload;
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
expect(event.data.group).toBe(jwtPayload?.context?.group);
expect(event.customerId).toBe(testsConfig.jaas.customerId);
}
it('setup', async () => {
room = ctx.roomName;
conferenceJid = `${room}@conference.${testsConfig.jaas.tenant}.${new URL(process.env.BASE_URL || '').hostname}`;
webhooksProxy = ctx.webhooksProxy;
p1 = await joinJaasMuc({ name: 'p1', iFrameApi: true, token: t({ room, moderator: true }) });
p1EpId = await p1.getEndpointId();
expect(await p1.isModerator()).toBe(true);
await checkParticipantJoinedHook(p1);
await p1.switchToMainFrame();
p2 = await joinJaasMuc({ name: 'p2', token: t({ room }) });
p2EpId = await p2.getEndpointId();
expect(await p2.isModerator()).toBe(false);
await checkParticipantJoinedHook(p2);
});
it('USAGE webhook', async () => {
const event: {
data: [
{ participantId: string; }
];
eventType: string;
} = await webhooksProxy.waitForEvent('USAGE');
expect(event.eventType).toBe('USAGE');
expect(event.data.some(d => d.participantId === p1EpId));
expect(event.data.some(d => d.participantId === p2EpId));
});
it('ROOM_CREATED webhook', async () => {
const event: {
data: {
conference: string;
isBreakout: boolean;
};
eventType: string;
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
expect(event.eventType).toBe('ROOM_CREATED');
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
});
it('ROLE_CHANGED webhook', async () => {
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
const event: {
data: {
grantedBy: {
participantId: string;
};
grantedTo: {
participantId: string;
};
role: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('ROLE_CHANGED');
expect(event.eventType).toBe('ROLE_CHANGED');
expect(event.data.role).toBe('moderator');
expect(event.data.grantedBy.participantId).toBe(p1EpId);
expect(event.data.grantedTo.participantId).toBe(p2EpId);
});
it('kick participant', async () => {
webhooksProxy.clearCache();
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
await checkParticipantLeftHook(p2, 'kicked');
});
it('join after kick', async () => {
webhooksProxy.clearCache();
// join again
p2 = await joinJaasMuc({ name: 'p2', token: t({ room }) });
p2EpId = await p2.getEndpointId();
await checkParticipantJoinedHook(p2);
});
it('hangup', async () => {
await p2.hangup();
await checkParticipantLeftHook(p2, 'left');
});
it('dispose conference', async () => {
await p1.getIframeAPI().executeCommand('hangup');
await checkParticipantLeftHook(p1, 'left');
const event: {
data: {
conference: string;
isBreakout: boolean;
};
eventType: string;
} = await webhooksProxy.waitForEvent('ROOM_DESTROYED');
expect(event.eventType).toBe('ROOM_DESTROYED');
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
});
});

View File

@@ -2,13 +2,10 @@ import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import WebhookProxy from '../../helpers/WebhookProxy';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
// Note this just for posterity. We don't depend on the framework doing anything for us because of this flag (we
// pass it as a parameter directly)
useIFrameApi: true,
useWebhookProxy: true
});
@@ -30,7 +27,7 @@ describe('Recording and Live Streaming', () => {
it('setup', async () => {
webhooksProxy = ctx.webhooksProxy;
p = await joinMuc({ iFrameApi: true, token: t({ moderator: true }) }, { roomName: ctx.roomName });
p = await joinJaasMuc({ iFrameApi: true, token: t({ moderator: true }) }, { roomName: ctx.roomName });
// TODO: what should we do in this case? Add a config for this?
if (await p.execute(() => config.disableIframeAPI)) {

View File

@@ -3,19 +3,27 @@ import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import type WebhookProxy from '../../helpers/WebhookProxy';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useIFrameApi: true,
useJaas: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Transcriptions', () => {
it('joining the meeting', async () => {
await ensureOneParticipant();
let p1: Participant, p2: Participant;
let webhooksProxy: WebhookProxy;
const { p1 } = ctx;
it('setup', async () => {
const room = ctx.roomName;
webhooksProxy = ctx.webhooksProxy;
p1 = await joinJaasMuc({
name: 'p1',
token: t({ room, moderator: true }),
iFrameApi: true });
if (await p1.execute(() => config.disableIframeAPI || !config.transcription?.enabled)) {
// skip the test if iframeAPI or transcriptions are disabled
@@ -24,32 +32,25 @@ describe('Transcriptions', () => {
return;
}
await p1.switchToMainFrame();
await ensureTwoParticipants({
p2 = await joinJaasMuc({
name: 'p2',
token: t({ room }),
iFrameApi: true }, {
configOverwrite: {
startWithAudioMuted: true
}
});
const { p2 } = ctx;
// let's populate endpoint ids
await Promise.all([
p1.getEndpointId(),
p2.getEndpointId()
p1.switchToMainFrame(),
p2.switchToMainFrame(),
]);
await p1.switchToMainFrame();
await p2.switchToMainFrame();
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
});
it('toggle subtitles', async () => {
const { p1, p2, webhooksProxy } = ctx;
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
await p1.getIframeAPI().executeCommand('toggleSubtitles');
@@ -63,9 +64,7 @@ describe('Transcriptions', () => {
});
it('set subtitles on and off', async () => {
const { p1, p2, webhooksProxy } = ctx;
// we need to clear results or the last one will be used, form the previous time subtitles were on
// we need to clear results or the last one will be used, from the previous time subtitles were on
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
@@ -80,9 +79,7 @@ describe('Transcriptions', () => {
});
it('start/stop transcriptions via recording', async () => {
const { p1, p2, webhooksProxy } = ctx;
// we need to clear results or the last one will be used, form the previous time subtitles were on
// we need to clear results or the last one will be used, from the previous time subtitles were on
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
@@ -96,12 +93,12 @@ describe('Transcriptions', () => {
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p1 side'
timeoutMsg: 'transcribingStatusChanged event not received on p1'
}));
allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p2 side'
timeoutMsg: 'transcribingStatusChanged event not received on p2'
}));
let result = await Promise.allSettled(allTranscriptionStatusChanged);
@@ -125,12 +122,12 @@ describe('Transcriptions', () => {
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p1 side'
timeoutMsg: 'transcribingStatusChanged event not received on p1'
}));
allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p2 side'
timeoutMsg: 'transcribingStatusChanged event not received on p2'
}));
result = await Promise.allSettled(allTranscriptionStatusChanged);
@@ -149,17 +146,15 @@ describe('Transcriptions', () => {
// let's wait for destroy event before waiting for those that depends on it
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
if (webhooksProxy) {
const event: {
data: {
preAuthenticatedLink: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
const event: {
data: {
preAuthenticatedLink: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
expect('TRANSCRIPTION_UPLOADED').toBe(event.eventType);
expect(event.data.preAuthenticatedLink).toBeDefined();
}
expect('TRANSCRIPTION_UPLOADED').toBe(event.eventType);
expect(event.data.preAuthenticatedLink).toBeDefined();
});
});
@@ -178,30 +173,28 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
}));
if (webhooksProxy) {
// TRANSCRIPTION_CHUNK_RECEIVED webhook
allTranscripts.push((async () => {
const event: {
data: {
final: string;
language: string;
messageID: string;
participant: {
id: string;
name: string;
};
stable: string;
// TRANSCRIPTION_CHUNK_RECEIVED webhook
allTranscripts.push((async () => {
const event: {
data: {
final: string;
language: string;
messageID: string;
participant: {
id: string;
name: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED');
stable: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED');
expect('TRANSCRIPTION_CHUNK_RECEIVED').toBe(event.eventType);
expect('TRANSCRIPTION_CHUNK_RECEIVED').toBe(event.eventType);
event.data.stable = event.data.final;
event.data.stable = event.data.final;
return event;
})());
}
return event;
})());
const result = await Promise.allSettled(allTranscripts);

View File

@@ -1,5 +1,5 @@
import { setTestProperties } from '../../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -15,7 +15,7 @@ describe('Visitors triggered by reaching participantsSoftLimit', () => {
};
/// XXX the "name" of the participant MUST match one of the "capabilities" defined in wdio. It's not a "participant", it's an instance configuration!
const m = await joinMuc({
const m = await joinJaasMuc({
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
});
@@ -25,7 +25,7 @@ describe('Visitors triggered by reaching participantsSoftLimit', () => {
console.log('Moderator joined');
// Joining with a participant token before participantSoftLimit has been reached
const p = await joinMuc({
const p = await joinJaasMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Parti Cipant' })
});
@@ -36,7 +36,7 @@ describe('Visitors triggered by reaching participantsSoftLimit', () => {
console.log('Participant joined');
// Joining with a participant token after participantSoftLimit has been reached
const v = await joinMuc({
const v = await joinJaasMuc({
name: 'p3',
token: t({ room: ctx.roomName, displayName: 'Visi Tor' })
});

View File

@@ -1,5 +1,5 @@
import { setTestProperties } from '../../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -23,7 +23,7 @@ describe('Visitor receiving video from a single remote participant', () => {
enabled: false
}
};
const sender = await joinMuc({
const sender = await joinJaasMuc({
token: t({ room: ctx.roomName, displayName: 'Sender', moderator: true })
}, {
configOverwrite
@@ -31,7 +31,7 @@ describe('Visitor receiving video from a single remote participant', () => {
const senderEndpointId = await sender.getEndpointId();
const testVisitor = async function(instanceId: 'p1' | 'p2' | 'p3' | 'p4') {
const visitor = await joinMuc({
const visitor = await joinJaasMuc({
name: instanceId,
token: t({ room: ctx.roomName, displayName: 'Visitor', visitor: true })
}, {

View File

@@ -1,5 +1,8 @@
import { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
import { config as testsConfig } from '../../../helpers/TestsConfig';
import WebhookProxy from '../../../helpers/WebhookProxy';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -8,51 +11,122 @@ setTestProperties(__filename, {
});
describe('Visitors triggered by visitor tokens', () => {
it('test visitor tokens', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
let webhooksProxy: WebhookProxy;
let room: string;
async function verifyJoinedWebhook(participant: Participant) {
const context = participant.getToken()?.payload.context;
const event: {
customerId: string;
data: {
avatar: string;
email: string;
group: string;
id: string;
name: string;
participantJid: string;
role: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
expect('PARTICIPANT_JOINED').toBe(event.eventType);
expect(event.data.avatar).toBe(context.user.avatar);
expect(event.data.email).toBe(context.user.email);
expect(event.data.id).toBe(context.user.id);
expect(event.data.group).toBe(context.group);
expect(event.data.name).toBe(context.user.name);
if (context.user.visitor) {
expect(event.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
expect(event.data.role).toBe('visitor');
}
expect(event.customerId).toBe(testsConfig.jaas.customerId);
}
async function verifyLeftWebhook(participant: Participant) {
const context = participant.getToken()?.payload.context;
const eventLeft: {
customerId: string;
data: {
avatar: string;
email: string;
group: string;
id: string;
name: string;
participantJid: string;
role: string;
};
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
expect('PARTICIPANT_LEFT').toBe(eventLeft.eventType);
expect(eventLeft.data.avatar).toBe(context.user.avatar);
expect(eventLeft.data.email).toBe(context.user.email);
expect(eventLeft.data.id).toBe(context.user.id);
expect(eventLeft.data.group).toBe(context.group);
expect(eventLeft.data.name).toBe(context.user.name);
if (context.user.visitor) {
expect(eventLeft.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
expect(eventLeft.data.role).toBe('visitor');
}
expect(eventLeft.customerId).toBe(testsConfig.jaas.customerId);
}
it('setup', async () => {
webhooksProxy = ctx.webhooksProxy;
webhooksProxy.defaultMeetingSettings = {
visitorsEnabled: true
};
room = ctx.roomName;
});
const m = await joinMuc({
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
});
it('test visitor tokens', async () => {
expect(await m.isInMuc()).toBe(true);
expect(await m.isModerator()).toBe(true);
expect(await m.isVisitor()).toBe(false);
console.log('Moderator joined');
const moderatorToken = t({ room, displayName: 'Mo de Rator', moderator: true });
const moderator = await joinJaasMuc({ name: 'p1', token: moderatorToken });
expect(await moderator.isInMuc()).toBe(true);
expect(await moderator.isModerator()).toBe(true);
expect(await moderator.isVisitor()).toBe(false);
await verifyJoinedWebhook(moderator);
// Joining with a participant token before any visitors
const p = await joinMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Parti Cipant' })
});
const participantToken = t({ room, displayName: 'Parti Cipant' });
const participant = await joinJaasMuc({ name: 'p2', token: participantToken });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
expect(await p.isVisitor()).toBe(false);
console.log('Participant joined');
expect(await participant.isInMuc()).toBe(true);
expect(await participant.isModerator()).toBe(false);
expect(await participant.isVisitor()).toBe(false);
await verifyJoinedWebhook(participant);
// Joining with a visitor token
const v = await joinMuc({
name: 'p3',
token: t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
});
const visitorToken = t({ room, displayName: 'Visi Tor', visitor: true });
const visitor = await joinJaasMuc({ name: 'p3', token: visitorToken });
expect(await v.isInMuc()).toBe(true);
expect(await v.isModerator()).toBe(false);
expect(await v.isVisitor()).toBe(true);
console.log('Visitor joined');
expect(await visitor.isInMuc()).toBe(true);
expect(await visitor.isModerator()).toBe(false);
expect(await visitor.isVisitor()).toBe(true);
await verifyJoinedWebhook(visitor);
// Joining with a participant token after visitors...:mindblown:
const v2 = await joinMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Visi Tor 2' })
});
await participant.hangup();
await verifyLeftWebhook(participant);
expect(await v2.isInMuc()).toBe(true);
expect(await v2.isModerator()).toBe(false);
expect(await v2.isVisitor()).toBe(true);
console.log('Visitor2 joined');
// Joining with a participant token after visitors -> visitor
const participantToken2 = t({ room, displayName: 'Visi Tor 2' });
const visitor2 = await joinJaasMuc({ name: 'p2', token: participantToken2 });
expect(await visitor2.isInMuc()).toBe(true);
expect(await visitor2.isModerator()).toBe(false);
expect(await visitor2.isVisitor()).toBe(true);
await verifyJoinedWebhook(visitor2);
await visitor.hangup();
await verifyLeftWebhook(visitor);
await visitor2.hangup();
await verifyLeftWebhook(visitor2);
await moderator.hangup();
await verifyLeftWebhook(moderator);
});
});

View File

@@ -2,7 +2,7 @@ import { expect } from '@wdio/globals';
import { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
@@ -19,7 +19,7 @@ describe('Visitors', () => {
visitorsLive: false
};
moderator = await joinMuc({
moderator = await joinJaasMuc({
name: 'p1',
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
});
@@ -32,7 +32,7 @@ describe('Visitors', () => {
ctx.skipSuiteTests = true;
});
visitor = await joinMuc({
visitor = await joinJaasMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
}, {

View File

@@ -1,5 +1,7 @@
import { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
import { Participant } from '../helpers/Participant';
import { setTestProperties } from '../helpers/TestProperties';
import { config as testsConfig } from '../helpers/TestsConfig';
import { joinMuc } from '../helpers/joinMuc';
/**
* The CSS selector for local video when outside of tile view. It should
@@ -14,13 +16,16 @@ const FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR = '#filmstripLocalVideo #localVide
*/
const TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR = '.remote-videos #localVideoContainer';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('TileView', () => {
let p1: Participant, p2: Participant;
before('join the meeting', async () => {
await ensureTwoParticipants();
p1 = ctx.p1;
p2 = ctx.p2;
p1 = await joinMuc({ name: 'p1', token: testsConfig.jwt.preconfiguredToken });
p2 = await joinMuc({ name: 'p2' });
});
it('entering tile view', async () => {
await p1.getToolbar().clickEnterTileViewButton();

View File

@@ -58,7 +58,7 @@ const chromePreferences = {
};
const specs = [
'specs/**/*.spec.ts'
'specs/**/startMuted.spec.ts'
];
/**
@@ -242,13 +242,11 @@ export const config: WebdriverIO.MultiremoteConfig = {
globalAny.ctx.roomName = generateRoomName(testName);
console.log(`Using room name: ${globalAny.ctx.roomName}`);
// If we are running the iFrameApi tests, we need to mark it as such and if needed to create the proxy
// and connect to it.
if (testProperties.useWebhookProxy && testsConfig.webhooksProxy.enabled && !globalAny.ctx.webhooksProxy) {
let tenant = testsConfig.jaas.tenant;
const tenant = testsConfig.jaas.tenant;
if (!testProperties.useJaas) {
tenant = testsConfig.iframe.tenant;
throw new Error('The test tries to use WebhookProxy without JaaS.');
}
if (!tenant) {
console.log(`Can not configure WebhookProxy, missing tenant in config. Skipping ${testName}.`);
@@ -314,6 +312,14 @@ export const config: WebdriverIO.MultiremoteConfig = {
* @param {Object} context - The context object.
*/
beforeTest(test, context) {
// Use the directory under 'tests/specs' as the parent suite
const match = test.file.match(/.*\/tests\/specs\/([^\/]+)\//);
const dir = match ? match[1] : false;
if (dir) {
AllureReporter.addParentSuite(dir);
}
if (ctx.skipSuiteTests) {
context.skip();