mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-26 08:30:18 +00:00
Compare commits
18 Commits
dependabot
...
release-88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0fc232a0f | ||
|
|
0fbadd716a | ||
|
|
dbe927666e | ||
|
|
d6f5f2fec0 | ||
|
|
5c1b191dc4 | ||
|
|
45d4039b53 | ||
|
|
93871d4ec9 | ||
|
|
c5acef3824 | ||
|
|
615fcad3a3 | ||
|
|
2e201ede28 | ||
|
|
e1c99d3220 | ||
|
|
7a8bd1b348 | ||
|
|
8d1a2694a6 | ||
|
|
f93509b460 | ||
|
|
307fd0d9e1 | ||
|
|
ad028e3361 | ||
|
|
8df5a4a519 | ||
|
|
b6110a5120 |
@@ -1375,7 +1375,7 @@ export default {
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
||||
logger.log(`USER ${id} connected:`, user);
|
||||
logger.log(`USER ${id} connected`);
|
||||
APP.UI.addUser(user);
|
||||
});
|
||||
|
||||
|
||||
@@ -112,7 +112,12 @@
|
||||
"disabled": "聊天已禁用",
|
||||
"enter": "加入会议室",
|
||||
"error": "错误:你的消息未发送。原因:{{error}}",
|
||||
"everyone": "所有人",
|
||||
"fieldPlaceHolder": "在这里输入你的信息",
|
||||
"fileAccessibleTitle": "{{user}}上传了一个文件",
|
||||
"fileAccessibleTitleMe": "我上传了一个文件",
|
||||
"fileDeleted": "文件已被删除",
|
||||
"guestsChatIndicator": "(访客)",
|
||||
"lobbyChatMessageTo": "等候室聊天消息发送至{{recipient}}",
|
||||
"message": "信息",
|
||||
"messageAccessibleTitle": "{{user}}:",
|
||||
@@ -300,6 +305,12 @@
|
||||
"alreadySharedVideoTitle": "同一时间只允许一个视频分享",
|
||||
"applicationWindow": "应用程序窗口",
|
||||
"authenticationRequired": "需要身份验证",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "使用手机摄像头拍照并发送",
|
||||
"ok": "打开相机",
|
||||
"reject": "暂不使用",
|
||||
"title": "拍照"
|
||||
},
|
||||
"cameraConstraintFailedError": "你的摄像头未满足某些必要条件。",
|
||||
"cameraNotFoundError": "找不到摄像头",
|
||||
"cameraNotSendingData": "我们无法访问你的摄像头,请检查是否有其他应用程序正在使用此设备,从设置菜单中选择另一个设备,或尝试重新加载应用程序。",
|
||||
@@ -375,22 +386,34 @@
|
||||
"micTimeoutError": "无法开启音频设备,发生超时!",
|
||||
"micUnknownError": "由于未知原因,无法使用麦克风。",
|
||||
"moderationAudioLabel": "允许参会者自己解除静音",
|
||||
"moderationDesktopLabel": "允许非主持人共享屏幕",
|
||||
"moderationVideoLabel": "允许参会者自己开启视频",
|
||||
"muteEveryoneDialog": "参会者可以在任何时候解除自己的静音。",
|
||||
"muteEveryoneDialogModerationOn": "参会者可以在任何时候请求发言。",
|
||||
"muteEveryoneElseDialog": "静音后,你将无法为其解除静音,但是他们可以随时解除自己的静音。",
|
||||
"muteEveryoneElseTitle": "除了{{whom}}以外的将所有人静音?",
|
||||
"muteEveryoneElsesDesktopDialog": "一旦停止共享,你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
|
||||
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的屏幕共享?",
|
||||
"muteEveryoneElsesVideoDialog": "一旦关闭,你将无法重新开启他们的摄像头,但他们随时可以重新开启。",
|
||||
"muteEveryoneElsesVideoTitle": "除了{{whom}}以外,关闭所有人的摄像头?",
|
||||
"muteEveryoneSelf": "你自己",
|
||||
"muteEveryoneStartMuted": "现在所有人都已静音",
|
||||
"muteEveryoneTitle": "静音所有人?",
|
||||
"muteEveryonesDesktopDialog": "参会者可以随时共享他们的屏幕",
|
||||
"muteEveryonesDesktopDialogModerationOn": "参会者可以随时请求共享他们的屏幕",
|
||||
"muteEveryonesDesktopTitle": "停止所有人的屏幕共享?",
|
||||
"muteEveryonesVideoDialog": "参会者可以随时开启他们的摄像头",
|
||||
"muteEveryonesVideoDialogModerationOn": "参会者可以随时请求开启他们的摄像头",
|
||||
"muteEveryonesVideoDialogOk": "关闭",
|
||||
"muteEveryonesVideoTitle": "关闭所有人的摄像头?",
|
||||
"muteParticipantBody": "你将无法为他们解除静音,但是他们可以随时解除自己的静音。",
|
||||
"muteParticipantButton": "静音",
|
||||
"muteParticipantsDesktopBody": "你无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
|
||||
"muteParticipantsDesktopBodyModerationOn": "你和他们都无法重新开启屏幕共享",
|
||||
"muteParticipantsDesktopButton": "停止屏幕共享",
|
||||
"muteParticipantsDesktopDialog": "你确定要停止这个参会者的屏幕共享吗?你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
|
||||
"muteParticipantsDesktopDialogModerationOn": "你确定要停止这个参会者的屏幕共享吗?你和他们都无法重新开启屏幕共享。",
|
||||
"muteParticipantsDesktopTitle": "停止这个参会者的屏幕共享?",
|
||||
"muteParticipantsVideoBody": "你无法重新开启摄像头,但他们随时可以重新开启。",
|
||||
"muteParticipantsVideoBodyModerationOn": "你和他们都无法重新开启摄像头",
|
||||
"muteParticipantsVideoButton": "关闭摄像头",
|
||||
@@ -503,6 +526,7 @@
|
||||
"tokenAuthFailedWithReasons": "抱歉,你无法加入此通话,原因:",
|
||||
"tokenAuthUnsupported": "Token地址不支持",
|
||||
"transcribing": "转录中",
|
||||
"unauthenticatedAccessDisabled": "此会议需要身份验证,请先登录后继续。",
|
||||
"unlockRoom": "移除会议$t(lockRoomPassword)",
|
||||
"user": "用户",
|
||||
"userIdentifier": "用户ID",
|
||||
@@ -547,11 +571,17 @@
|
||||
"downloadFailedDescription": "请稍后重试",
|
||||
"downloadFailedTitle": "下载失败",
|
||||
"downloadFile": "下载",
|
||||
"downloadStarted": "文件下载已开始",
|
||||
"dragAndDrop": "拖拽文件到此处上传",
|
||||
"fileAlreadyUploaded": "文件已上传至本次会议",
|
||||
"fileRemovedByOther": "你的文件{{fileName}}已被移除",
|
||||
"fileTooLargeDescription": "请确保文件不超过 {{ maxFileSize }}",
|
||||
"fileTooLargeTitle": "文件太大",
|
||||
"fileUploadProgress": "文件上传进度",
|
||||
"fileUploadedSuccessfully": "文件上传成功",
|
||||
"newFileNotification": "{{participantName}}分享了{{fileName}}",
|
||||
"removeFile": "移除",
|
||||
"removeFileSuccess": "文件移除成功",
|
||||
"uploadFailedDescription": "请稍后重试",
|
||||
"uploadFailedTitle": "上传失败",
|
||||
"uploadFile": "文件共享"
|
||||
@@ -724,7 +754,8 @@
|
||||
"notificationTitle": "等候室",
|
||||
"passwordJoinButton": "加入",
|
||||
"title": "等候室",
|
||||
"toggleLabel": "开启等候室模式"
|
||||
"toggleLabel": "开启等候室模式",
|
||||
"waitForModerator": "会议尚未开始,暂无主持人入会。如需成为主持人请先登录,或耐心等待会议开始。"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -767,8 +798,10 @@
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "安全漏洞!",
|
||||
"allowAll": "允许全部",
|
||||
"allowAudio": "允许开启麦克风",
|
||||
"allowBoth": "允许音视频",
|
||||
"allowDesktop": "允许屏幕共享",
|
||||
"allowVideo": "允许开启摄像头",
|
||||
"allowedUnmute": "你可以解除麦克风静音、启动摄像头或共享屏幕。",
|
||||
"audioUnmuteBlockedDescription": "由于系统限制,麦克风解除静音操作被暂时阻止。",
|
||||
@@ -782,6 +815,7 @@
|
||||
"dataChannelClosedDescription": "桥接通道已断开,视频质量可能会被限制为最低设置",
|
||||
"dataChannelClosedDescriptionWithAudio": "桥接通道已断开,音视频可能会出现卡顿或中断",
|
||||
"dataChannelClosedWithAudio": "音视频质量可能受影响",
|
||||
"desktopMutedRemotelyTitle": "你的屏幕共享已被{{participantDisplayName}}停止",
|
||||
"disabledIframe": "嵌入仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
|
||||
"disabledIframeSecondaryNative": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
|
||||
"disabledIframeSecondaryWeb": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开。如需在正式环境嵌入,请使用<a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi服务</a>!",
|
||||
@@ -839,6 +873,7 @@
|
||||
"oldElectronClientDescription1": "你似乎正在使用存在已知安全漏洞的旧版Jitsi Meet客户端,请确保您更新到我们的",
|
||||
"oldElectronClientDescription2": "最新版本",
|
||||
"oldElectronClientDescription3": "!",
|
||||
"openChat": "打开聊天",
|
||||
"participantWantsToJoin": "想要加入会议",
|
||||
"participantsWantToJoin": "想要加入会议",
|
||||
"passwordRemovedRemotely": "其他参会者移除了$t(lockRoomPasswordUppercase)",
|
||||
@@ -862,6 +897,7 @@
|
||||
"suggestRecordingDescription": "是否需要录制本次会议?",
|
||||
"suggestRecordingTitle": "录制会议",
|
||||
"unmute": "解除静音",
|
||||
"unmuteScreen": "开始屏幕共享",
|
||||
"unmuteVideo": "开启摄像头",
|
||||
"videoMutedRemotelyDescription": "你可随时重新开启视频",
|
||||
"videoMutedRemotelyTitle": "{{participantDisplayName}}已关闭你的视频",
|
||||
@@ -881,11 +917,14 @@
|
||||
"admit": "同意加入",
|
||||
"admitAll": "全部同意加入",
|
||||
"allow": "允许参会者:",
|
||||
"allowDesktop": "允许屏幕共享",
|
||||
"allowVideo": "允许开启摄像头",
|
||||
"askDesktop": "请求共享屏幕",
|
||||
"askUnmute": "请求取消静音",
|
||||
"audioModeration": "自行解除静音",
|
||||
"blockEveryoneMicCamera": "禁用所有人的麦克风和摄像头",
|
||||
"breakoutRooms": "分组讨论室",
|
||||
"desktopModeration": "开始屏幕共享",
|
||||
"goLive": "开始直播",
|
||||
"invite": "邀请其他人",
|
||||
"lowerAllHands": "取消全部举手",
|
||||
@@ -897,6 +936,8 @@
|
||||
"muteAll": "全体静音",
|
||||
"muteEveryoneElse": "静音其他人",
|
||||
"reject": "拒绝",
|
||||
"stopDesktop": "停止屏幕共享",
|
||||
"stopEveryonesDesktop": "停止所有人的屏幕共享",
|
||||
"stopEveryonesVideo": "关闭所有人摄像头",
|
||||
"stopVideo": "关闭摄像头",
|
||||
"unblockEveryoneMicCamera": "允许所有人开启麦克风和摄像头",
|
||||
@@ -906,9 +947,11 @@
|
||||
"headings": {
|
||||
"lobby": "等候室(({{count}}人)",
|
||||
"participantsList": "会议参会者({{count}}人)",
|
||||
"viewerRequests": "观众请求({{count}}人)",
|
||||
"visitorInQueue": "(排队中:{{count}}人)",
|
||||
"visitorRequests": "(请求加入:{{count}}人)",
|
||||
"visitors": "观众(({{count}}人)",
|
||||
"visitorsList": "观众({{count}}人)",
|
||||
"waitingLobby": "在等候室等待({{count}}人)"
|
||||
},
|
||||
"search": "搜索参会者",
|
||||
@@ -929,6 +972,9 @@
|
||||
"by": "由{{ name }}发起",
|
||||
"closeButton": "结束投票",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "发送投票"
|
||||
},
|
||||
"addOption": "添加选项",
|
||||
"answerPlaceholder": "选项{{index}}",
|
||||
"cancel": "取消",
|
||||
@@ -1345,6 +1391,20 @@
|
||||
"videounmute": "打开摄像头"
|
||||
},
|
||||
"addPeople": "添加成员到通话中",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "回声消除"
|
||||
},
|
||||
"agc": {
|
||||
"label": "自动增益控制"
|
||||
},
|
||||
"ns": {
|
||||
"label": "降噪"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "立体声"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "关闭省流模式",
|
||||
"audioOnlyOn": "开启省流模式",
|
||||
"audioRoute": "选择音频设备",
|
||||
@@ -1416,6 +1476,7 @@
|
||||
"reactionHeart": "发送爱心",
|
||||
"reactionLaugh": "发送大笑",
|
||||
"reactionLike": "发送点赞",
|
||||
"reactionLove": "发送爱心",
|
||||
"reactionSilence": "发送沉默",
|
||||
"reactionSurprised": "发送惊讶",
|
||||
"reactions": "互动表情",
|
||||
@@ -1501,6 +1562,8 @@
|
||||
"connectionInfo": "连接信息",
|
||||
"demote": "设为观众",
|
||||
"domute": "静音",
|
||||
"domuteDesktop": "停止屏幕共享",
|
||||
"domuteDesktopOfOthers": "停止屏幕共享给其他人",
|
||||
"domuteOthers": "静音其他人",
|
||||
"domuteVideo": "关闭摄像头",
|
||||
"domuteVideoOfOthers": "关闭其他人摄像头",
|
||||
@@ -1565,6 +1628,8 @@
|
||||
"noMainParticipantsTitle": "会议尚未开始",
|
||||
"noVisitorLobby": "当前会议已开启等候室,暂无法加入",
|
||||
"notAllowedPromotion": "需由会议成员同意才能参与讨论",
|
||||
"requestToJoin": "举手请求",
|
||||
"requestToJoinDescription": "你的请求已发送给主持人,请稍候!",
|
||||
"title": "你当前为会议观众"
|
||||
},
|
||||
"waitingMessage": "会议开始后将自动加入"
|
||||
|
||||
@@ -112,7 +112,12 @@
|
||||
"disabled": "聊天訊息已停用",
|
||||
"enter": "加入聊天室",
|
||||
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
|
||||
"everyone": "所有人",
|
||||
"fieldPlaceHolder": "在此輸入您的訊息",
|
||||
"fileAccessibleTitle": "{{user}}上傳了一個檔案",
|
||||
"fileAccessibleTitleMe": "我上傳了一個檔案",
|
||||
"fileDeleted": "檔案已被刪除",
|
||||
"guestsChatIndicator": "(訪客)",
|
||||
"lobbyChatMessageTo": "大廳聊天訊息傳送至 {{recipient}}",
|
||||
"message": "訊息",
|
||||
"messageAccessibleTitle": "{{user}}:",
|
||||
@@ -300,6 +305,12 @@
|
||||
"alreadySharedVideoTitle": "同一時間只允許一位影像分享",
|
||||
"applicationWindow": "應用程式視窗",
|
||||
"authenticationRequired": "需要驗證",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "使用手機攝影機拍照並傳送",
|
||||
"ok": "開啟相機",
|
||||
"reject": "暫不使用",
|
||||
"title": "拍照"
|
||||
},
|
||||
"cameraConstraintFailedError": "您的網路攝影機不符合要求。",
|
||||
"cameraNotFoundError": "找不到網路攝影機。",
|
||||
"cameraNotSendingData": "我們無法存取您的網路攝影機,請檢查是否有其他應用程式正在使用這個裝置,並從裝置選單裡選擇其他設備或者重新載入。",
|
||||
@@ -375,22 +386,34 @@
|
||||
"micTimeoutError": "無法啟動音訊裝置,連線逾時!",
|
||||
"micUnknownError": "不明原因造成麥克風無法使用。",
|
||||
"moderationAudioLabel": "允許與會者自我解除靜音",
|
||||
"moderationDesktopLabel": "允許非主持人共享螢幕",
|
||||
"moderationVideoLabel": "允許與會者開啟視訊",
|
||||
"muteEveryoneDialog": "與會者可以隨時解除自己的靜音狀態。",
|
||||
"muteEveryoneDialogModerationOn": "與會者可以隨時請求發言。",
|
||||
"muteEveryoneElseDialog": "靜音後,您就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
|
||||
"muteEveryoneElseTitle": "是否要讓除了 {{whom}} 以外的人靜音?",
|
||||
"muteEveryoneElsesDesktopDialog": "一旦停止共享,您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
|
||||
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的螢幕共享?",
|
||||
"muteEveryoneElsesVideoDialog": "一旦停用,您就不能再重新開啟對方的網路攝影機,但對方隨時能重新開啟自己的網路攝影機。",
|
||||
"muteEveryoneElsesVideoTitle": "是否要關閉除了 {{whom}} 以外的人的網路攝影機?",
|
||||
"muteEveryoneSelf": "您自己",
|
||||
"muteEveryoneStartMuted": "現在所有人皆已靜音",
|
||||
"muteEveryoneTitle": "要將所有人靜音嗎?",
|
||||
"muteEveryonesDesktopDialog": "與會者可以隨時共享他們的螢幕",
|
||||
"muteEveryonesDesktopDialogModerationOn": "與會者可以隨時請求共享他們的螢幕",
|
||||
"muteEveryonesDesktopTitle": "停止所有人的螢幕共享?",
|
||||
"muteEveryonesVideoDialog": "與會者隨時可以重新開啟自己的網路攝影機。",
|
||||
"muteEveryonesVideoDialogModerationOn": "與會者可以隨時傳送開啟視訊請求。",
|
||||
"muteEveryonesVideoDialogOk": "停用",
|
||||
"muteEveryonesVideoTitle": "要關閉所有人的網路攝影機嗎?",
|
||||
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
|
||||
"muteParticipantButton": "靜音",
|
||||
"muteParticipantsDesktopBody": "您無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
|
||||
"muteParticipantsDesktopBodyModerationOn": "您和他們都無法重新開啟螢幕共享",
|
||||
"muteParticipantsDesktopButton": "停止螢幕分享",
|
||||
"muteParticipantsDesktopDialog": "您確定要停止這位與會者的螢幕共享嗎?您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
|
||||
"muteParticipantsDesktopDialogModerationOn": "您確定要停止這位與會者的螢幕共享嗎?您和他們都無法重新開啟螢幕共享。",
|
||||
"muteParticipantsDesktopTitle": "停止這位與會者的螢幕共享?",
|
||||
"muteParticipantsVideoBody": "您無法重新開啟,只有對方能自己重新開啟。",
|
||||
"muteParticipantsVideoBodyModerationOn": "您和他都無法再將視訊重新開啟。",
|
||||
"muteParticipantsVideoButton": "停用網路攝影機",
|
||||
@@ -503,6 +526,7 @@
|
||||
"tokenAuthFailedWithReasons": "抱歉,您無法參加這個通話,可能原因:{{reason}}",
|
||||
"tokenAuthUnsupported": "不支援權杖網址。",
|
||||
"transcribing": "轉錄中",
|
||||
"unauthenticatedAccessDisabled": "此會議需要身份驗證,請先登入後繼續。",
|
||||
"unlockRoom": "移除會議 $t(lockRoomPassword)",
|
||||
"user": "使用者",
|
||||
"userIdentifier": "使用者 ID",
|
||||
@@ -547,11 +571,17 @@
|
||||
"downloadFailedDescription": "請重試",
|
||||
"downloadFailedTitle": "下載失敗",
|
||||
"downloadFile": "下載",
|
||||
"downloadStarted": "檔案下載已開始",
|
||||
"dragAndDrop": "將檔案拖曳至此或畫面任一處上傳",
|
||||
"fileAlreadyUploaded": "檔案已上傳至此會議",
|
||||
"fileRemovedByOther": "您的檔案「{{fileName}}」已被移除",
|
||||
"fileTooLargeDescription": "請確認檔案未超過 {{ maxFileSize }}",
|
||||
"fileTooLargeTitle": "檔案過大",
|
||||
"fileUploadProgress": "檔案上傳進度",
|
||||
"fileUploadedSuccessfully": "檔案上傳成功",
|
||||
"newFileNotification": "{{participantName}}分享了「{{fileName}}」",
|
||||
"removeFile": "移除",
|
||||
"removeFileSuccess": "檔案移除成功",
|
||||
"uploadFailedDescription": "請重試",
|
||||
"uploadFailedTitle": "上傳失敗",
|
||||
"uploadFile": "分享檔案"
|
||||
@@ -724,7 +754,8 @@
|
||||
"notificationTitle": "大廳",
|
||||
"passwordJoinButton": "加入",
|
||||
"title": "大廳",
|
||||
"toggleLabel": "啟用大廳模式"
|
||||
"toggleLabel": "啟用大廳模式",
|
||||
"waitForModerator": "會議尚未開始,暫無主持人入會。如需成為主持人請先登入,或耐心等待會議開始。"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -767,8 +798,10 @@
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "安全漏洞!",
|
||||
"allowAll": "允許全部",
|
||||
"allowAudio": "允許音訊",
|
||||
"allowBoth": "允許音訊與視訊",
|
||||
"allowDesktop": "允許螢幕分享",
|
||||
"allowVideo": "允許視訊",
|
||||
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的螢幕。",
|
||||
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
|
||||
@@ -782,6 +815,7 @@
|
||||
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
|
||||
"dataChannelClosedDescriptionWithAudio": "橋接通道已斷開,音訊和視訊可能會受到影響。",
|
||||
"dataChannelClosedWithAudio": "音訊和視訊品質可能會降低。",
|
||||
"desktopMutedRemotelyTitle": "您的螢幕分享已被{{participantDisplayName}}停止",
|
||||
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
|
||||
"disabledIframeSecondaryNative": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷。",
|
||||
"disabledIframeSecondaryWeb": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷,請使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a> 來進行正式嵌入!",
|
||||
@@ -839,6 +873,7 @@
|
||||
"oldElectronClientDescription1": "您似乎正在使用存在已知安全漏洞的過時 Jitsi Meet 用戶端,請盡快更新至我們的",
|
||||
"oldElectronClientDescription2": "最新版本",
|
||||
"oldElectronClientDescription3": "!",
|
||||
"openChat": "開啟聊天",
|
||||
"participantWantsToJoin": "希望加入會議",
|
||||
"participantsWantToJoin": "希望加入會議",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) 已被其他與會者移除",
|
||||
@@ -862,6 +897,7 @@
|
||||
"suggestRecordingDescription": "是否要開始錄製這場會議?",
|
||||
"suggestRecordingTitle": "錄製此會議",
|
||||
"unmute": "取消靜音",
|
||||
"unmuteScreen": "開始螢幕分享",
|
||||
"unmuteVideo": "啟用視訊",
|
||||
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
|
||||
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
|
||||
@@ -881,11 +917,14 @@
|
||||
"admit": "準許",
|
||||
"admitAll": "準許所有人",
|
||||
"allow": "允許與會者能夠:",
|
||||
"allowDesktop": "允許螢幕分享",
|
||||
"allowVideo": "允許視訊",
|
||||
"askDesktop": "請求共享螢幕",
|
||||
"askUnmute": "要求解除靜音",
|
||||
"audioModeration": "自我解除靜音",
|
||||
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
|
||||
"breakoutRooms": "分組討論室",
|
||||
"desktopModeration": "開始螢幕分享",
|
||||
"goLive": "開始直播",
|
||||
"invite": "邀請他人",
|
||||
"lowerAllHands": "全部取消舉手",
|
||||
@@ -897,6 +936,8 @@
|
||||
"muteAll": "靜音所有人",
|
||||
"muteEveryoneElse": "靜音其他人",
|
||||
"reject": "拒絕",
|
||||
"stopDesktop": "停止螢幕分享",
|
||||
"stopEveryonesDesktop": "停止所有人的螢幕分享",
|
||||
"stopEveryonesVideo": "停用所有人的視訊",
|
||||
"stopVideo": "停用視訊",
|
||||
"unblockEveryoneMicCamera": "解除封鎖所有人的麥克風及網路攝影機",
|
||||
@@ -906,9 +947,11 @@
|
||||
"headings": {
|
||||
"lobby": "大廳({{count}} 人)",
|
||||
"participantsList": "會議與會者({{count}} 人)",
|
||||
"viewerRequests": "觀眾請求({{count}}人)",
|
||||
"visitorInQueue": "({{count}} 人等候中)",
|
||||
"visitorRequests": "{{count}} 人申請",
|
||||
"visitors": "訪客({{count}} 人)",
|
||||
"visitorsList": "觀眾({{count}}人)",
|
||||
"waitingLobby": "於大廳等候({{count}} 人)"
|
||||
},
|
||||
"search": "搜尋與會者",
|
||||
@@ -929,6 +972,9 @@
|
||||
"by": "由 {{ name }}",
|
||||
"closeButton": "結束投票",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "傳送投票"
|
||||
},
|
||||
"addOption": "新增選項",
|
||||
"answerPlaceholder": "選項 {{index}}",
|
||||
"cancel": "取消",
|
||||
@@ -1345,6 +1391,20 @@
|
||||
"videounmute": "啟用網路攝影機"
|
||||
},
|
||||
"addPeople": "新增人員到您的通話中",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "回聲消除"
|
||||
},
|
||||
"agc": {
|
||||
"label": "自動增益控制"
|
||||
},
|
||||
"ns": {
|
||||
"label": "降噪"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "立體聲"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "停用低頻寬模式",
|
||||
"audioOnlyOn": "啟用低頻寬模式",
|
||||
"audioRoute": "選擇音訊裝置",
|
||||
@@ -1416,6 +1476,7 @@
|
||||
"reactionHeart": "傳送愛心反應",
|
||||
"reactionLaugh": "傳送大笑反應",
|
||||
"reactionLike": "傳送比讚反應",
|
||||
"reactionLove": "傳送愛心",
|
||||
"reactionSilence": "傳送沉默反應",
|
||||
"reactionSurprised": "傳送驚訝反應",
|
||||
"reactions": "反應",
|
||||
@@ -1501,6 +1562,8 @@
|
||||
"connectionInfo": "連線資訊",
|
||||
"demote": "轉為訪客",
|
||||
"domute": "靜音",
|
||||
"domuteDesktop": "停止螢幕分享",
|
||||
"domuteDesktopOfOthers": "停止螢幕分享給其他人",
|
||||
"domuteOthers": "靜音其他人",
|
||||
"domuteVideo": "停用網路攝影機",
|
||||
"domuteVideoOfOthers": "停用其他人的網路攝影機",
|
||||
@@ -1565,6 +1628,8 @@
|
||||
"noMainParticipantsTitle": "會議尚未開始",
|
||||
"noVisitorLobby": "此會議啟用大廳,暫時無法加入",
|
||||
"notAllowedPromotion": "需由與會者同意您的申請",
|
||||
"requestToJoin": "舉手請求",
|
||||
"requestToJoinDescription": "您的請求已傳送給主持人,請稍候!",
|
||||
"title": "您是會議中的訪客"
|
||||
},
|
||||
"waitingMessage": "會議開始後您將自動加入!"
|
||||
|
||||
@@ -158,11 +158,10 @@ const VideoLayout = {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const state = APP.store.getState();
|
||||
const currentContainer = largeVideo.getCurrentContainer();
|
||||
const currentContainerType = largeVideo.getCurrentContainerType();
|
||||
const isOnLarge = this.isCurrentlyOnLarge(id);
|
||||
const state = APP.store.getState();
|
||||
const participant = getParticipantById(state, id);
|
||||
const videoTrack = getVideoTrackByParticipant(state, participant);
|
||||
const videoStream = videoTrack?.jitsiTrack;
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -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/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8860",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -18259,8 +18259,7 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-PCMJIfFWIZtDC6UA/53mT79hiqTGNCRE04/XFgWEr7KRf2QIni2tFh3hW1IPW0OjbtMAkJ1KGQpca/3l6sa5Mw==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#0af202368649360ede8dcc1ca4d055910c6b9337",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
@@ -39706,8 +39705,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-PCMJIfFWIZtDC6UA/53mT79hiqTGNCRE04/XFgWEr7KRf2QIni2tFh3hW1IPW0OjbtMAkJ1KGQpca/3l6sa5Mw==",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#0af202368649360ede8dcc1ca4d055910c6b9337",
|
||||
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-8860",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
"@jitsi/logger": "2.1.1",
|
||||
|
||||
@@ -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/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8860",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
|
||||
@@ -654,6 +654,7 @@ export interface IConfig {
|
||||
video?: boolean;
|
||||
};
|
||||
queueService: string;
|
||||
showJoinMeetingDialog?: boolean;
|
||||
};
|
||||
watchRTCConfigParams?: IWatchRTCConfiguration;
|
||||
webhookProxyUrl?: string;
|
||||
|
||||
@@ -99,6 +99,7 @@ export default [
|
||||
'disabledNotifications',
|
||||
'disabledSounds',
|
||||
'disableFilmstripAutohiding',
|
||||
'disableFocus',
|
||||
'disableInitialGUM',
|
||||
'disableInviteFunctions',
|
||||
'disableIncomingMessageSound',
|
||||
@@ -237,6 +238,7 @@ export default [
|
||||
'useTurnUdp',
|
||||
'videoQuality',
|
||||
'visitors.enableMediaOnPromote',
|
||||
'visitors.showJoinMeetingDialog',
|
||||
'watchRTCConfigParams.allowBrowserLogCollection',
|
||||
'watchRTCConfigParams.collectionInterval',
|
||||
'watchRTCConfigParams.console',
|
||||
|
||||
@@ -80,7 +80,7 @@ const options: i18next.InitOptions = {
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react as it escapes by default
|
||||
},
|
||||
load: 'languageOnly',
|
||||
load: 'all',
|
||||
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
|
||||
react: {
|
||||
// re-render when a new resource bundle is added
|
||||
|
||||
@@ -186,6 +186,9 @@ function _initLogging({ dispatch, getState }: IStore,
|
||||
Logger.addGlobalTransport(debugLogCollector);
|
||||
JitsiMeetJS.addGlobalLogTransport(debugLogCollector);
|
||||
debugLogCollector.start();
|
||||
|
||||
Logger.removeGlobalTransport(console);
|
||||
JitsiMeetJS.removeGlobalLogTransport(console);
|
||||
}
|
||||
} else if (logCollector && loggingConfig.disableLogCollector) {
|
||||
Logger.removeGlobalTransport(logCollector);
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
isRemoteScreenshareParticipant,
|
||||
isScreenShareParticipant
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
|
||||
|
||||
/**
|
||||
@@ -364,6 +365,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
|
||||
logger.debug('Remote screenshare participant joined', id);
|
||||
}
|
||||
|
||||
// Exclude the screenshare participant from the fake participant count to avoid duplicates.
|
||||
@@ -448,6 +451,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
|
||||
sortedRemoteVirtualScreenshareParticipants.delete(id);
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
|
||||
logger.debug('Remote screenshare participant left', id);
|
||||
}
|
||||
|
||||
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getRemoteScreensharesBasedOnPresence,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { FakeParticipant } from './types';
|
||||
|
||||
StateListenerRegistry.register(
|
||||
@@ -69,14 +70,19 @@ function _createOrRemoveVirtualParticipants(
|
||||
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
|
||||
|
||||
if (removedScreenshareSourceNames.length) {
|
||||
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
|
||||
fakeParticipant: FakeParticipant.RemoteScreenShare
|
||||
})));
|
||||
removedScreenshareSourceNames.forEach(id => {
|
||||
logger.debug('Dispatching participantLeft for virtual screenshare', id);
|
||||
dispatch(participantLeft(id, conference, {
|
||||
fakeParticipant: FakeParticipant.RemoteScreenShare
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (addedScreenshareSourceNames.length) {
|
||||
addedScreenshareSourceNames.forEach(id => dispatch(
|
||||
createVirtualScreenshareParticipant(id, false, conference)));
|
||||
addedScreenshareSourceNames.forEach(id => {
|
||||
logger.debug('Creating virtual screenshare participant', id);
|
||||
dispatch(createVirtualScreenshareParticipant(id, false, conference));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
|
||||
import { VIDEO_CODEC } from '../../video-quality/constants';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
|
||||
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
|
||||
import {
|
||||
@@ -57,75 +56,12 @@ export function isLargeVideoReceived({ getState }: IStore): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the local video track is encoded in AV1.
|
||||
* Returns the local video track's codec.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {boolean}
|
||||
* @returns {string?} The local video track's codec.
|
||||
*/
|
||||
export function isLocalCameraEncodingAv1({ getState }: IStore): boolean {
|
||||
const state = getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const localtrack = getLocalVideoTrack(tracks);
|
||||
|
||||
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.AV1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the local video track is encoded in H.264.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLocalCameraEncodingH264({ getState }: IStore): boolean {
|
||||
const state = getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const localtrack = getLocalVideoTrack(tracks);
|
||||
|
||||
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.H264) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the local video track is encoded in VP8.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLocalCameraEncodingVp8({ getState }: IStore): boolean {
|
||||
const state = getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const localtrack = getLocalVideoTrack(tracks);
|
||||
|
||||
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.VP8) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the local video track is encoded in VP9.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLocalCameraEncodingVp9({ getState }: IStore): boolean {
|
||||
const state = getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const localtrack = getLocalVideoTrack(tracks);
|
||||
|
||||
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.VP9) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
export function getLocalCameraEncoding({ getState }: IStore): string | undefined {
|
||||
return getLocalVideoTrack(getState()['features/base/tracks'])?.codec?.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,12 +8,9 @@ import { getJitsiMeetGlobalNS } from '../util/helpers';
|
||||
|
||||
import { setConnectionState } from './actions';
|
||||
import {
|
||||
getLocalCameraEncoding,
|
||||
getRemoteVideoType,
|
||||
isLargeVideoReceived,
|
||||
isLocalCameraEncodingAv1,
|
||||
isLocalCameraEncodingH264,
|
||||
isLocalCameraEncodingVp8,
|
||||
isLocalCameraEncodingVp9,
|
||||
isRemoteVideoReceived,
|
||||
isTestModeEnabled
|
||||
} from './functions';
|
||||
@@ -90,10 +87,7 @@ function _bindTortureHelpers(store: IStore) {
|
||||
getJitsiMeetGlobalNS().testing = {
|
||||
getRemoteVideoType: getRemoteVideoType.bind(null, store),
|
||||
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
|
||||
isLocalCameraEncodingAv1: isLocalCameraEncodingAv1.bind(null, store),
|
||||
isLocalCameraEncodingH264: isLocalCameraEncodingH264.bind(null, store),
|
||||
isLocalCameraEncodingVp8: isLocalCameraEncodingVp8.bind(null, store),
|
||||
isLocalCameraEncodingVp9: isLocalCameraEncodingVp9.bind(null, store),
|
||||
getLocalCameraEncoding: getLocalCameraEncoding.bind(null, store),
|
||||
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getVirtualScreenshareParticipantByOwnerId
|
||||
} from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { getAutoPinSetting } from '../video-layout/functions';
|
||||
|
||||
import {
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
SET_LARGE_VIDEO_DIMENSIONS,
|
||||
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
|
||||
} from './actionTypes';
|
||||
import { shouldHideLargeVideo } from './functions';
|
||||
|
||||
/**
|
||||
* Action to select the participant to be displayed in LargeVideo based on the
|
||||
@@ -34,8 +34,12 @@ export function selectParticipantInLargeVideo(participant?: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
// Skip large video updates when the large video container is hidden.
|
||||
if (shouldHideLargeVideo(state)) {
|
||||
if (isStageFilmstripAvailable(state, 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep Etherpad open.
|
||||
if (state['features/etherpad'].editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { shouldDisplayTileView } from '../video-layout/functions.any';
|
||||
|
||||
/**
|
||||
* Selector for the participant currently displaying on the large video.
|
||||
@@ -14,17 +12,3 @@ export function getLargeVideoParticipant(state: IReduxState) {
|
||||
|
||||
return getParticipantById(state, participantId ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the large video container should be hidden.
|
||||
* Large video is hidden in tile view, stage filmstrip mode (with multiple participants),
|
||||
* or when editing etherpad.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} True if large video should be hidden, false otherwise.
|
||||
*/
|
||||
export function shouldHideLargeVideo(state: IReduxState): boolean {
|
||||
return shouldDisplayTileView(state)
|
||||
|| isStageFilmstripAvailable(state, 2)
|
||||
|| Boolean(state['features/etherpad']?.editing);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
|
||||
import { selectParticipantInLargeVideo } from './actions.any';
|
||||
import { shouldHideLargeVideo } from './functions';
|
||||
|
||||
/**
|
||||
* Updates the large video when transitioning from a hidden state to visible state.
|
||||
* This ensures the large video is properly updated when exiting tile view, stage filmstrip,
|
||||
* whiteboard, or etherpad editing modes.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => shouldHideLargeVideo(state),
|
||||
/* listener */ (isHidden, { dispatch }) => {
|
||||
// When transitioning from hidden to visible state, select participant (because currently it is undefined).
|
||||
// Otherwise set it to undefined because we don't show the large video.
|
||||
if (!isHidden) {
|
||||
dispatch(selectParticipantInLargeVideo());
|
||||
} else {
|
||||
dispatch({
|
||||
type: SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
participantId: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
import './subscriber.any';
|
||||
|
||||
@@ -4,7 +4,6 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
|
||||
|
||||
import { getLargeVideoParticipant } from './functions';
|
||||
import './subscriber.any';
|
||||
|
||||
/**
|
||||
* Updates the on stage participant video.
|
||||
|
||||
@@ -467,7 +467,7 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
_knocking: knocking,
|
||||
_lobbyChatMessages: messages,
|
||||
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
|
||||
_login: showModeratorLogin,
|
||||
_login: showModeratorLogin && !state['features/base/jwt'].jwt,
|
||||
_hangUp: showHangUp,
|
||||
_isLobbyChatActive: isLobbyChatActive,
|
||||
_meetingName: getConferenceName(state),
|
||||
|
||||
@@ -96,6 +96,11 @@ function normalizeCurrentLanguage(language: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First check if the language code exists as-is (e.g., 'zh-CN', 'fr-CA')
|
||||
if (LANGUAGES.includes(language)) {
|
||||
return language;
|
||||
}
|
||||
|
||||
const [ country, lang ] = language.split('-');
|
||||
const jitsiNormalized = `${country}${lang ?? ''}`;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getReceiverVideoQualityLevel } from '../video-quality/functions';
|
||||
import { getMinHeightForQualityLvlMap } from '../video-quality/selector';
|
||||
|
||||
import { LAYOUTS } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* A selector for retrieving the current automatic pinning setting.
|
||||
@@ -113,39 +114,49 @@ export function shouldDisplayTileView(state: IReduxState) {
|
||||
* Private helper to automatically pin the latest screen share stream or unpin
|
||||
* if there are no more screen share streams.
|
||||
*
|
||||
* @param {Array<string>} screenShares - Array containing the list of all the screen sharing endpoints
|
||||
* @param {Array<string>} previousScreenShares - Array containing the list of all the screen sharing endpoints
|
||||
* before the update was triggered (including the ones that have been removed from redux because of the update).
|
||||
* @param {Array<string>} currentScreenShares - Array containing the current list of screen sharing endpoints.
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function updateAutoPinnedParticipant(
|
||||
screenShares: Array<string>, { dispatch, getState }: IStore) {
|
||||
const state = getState();
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
|
||||
previousScreenShares: Array<string>,
|
||||
currentScreenShares: Array<string>,
|
||||
{ dispatch, getState }: IStore) {
|
||||
const pinned = getPinnedParticipant(getState);
|
||||
|
||||
// if the pinned participant is shared video or some other fake participant we want to skip auto-pinning
|
||||
if (pinned?.fakeParticipant && pinned.fakeParticipant !== FakeParticipant.RemoteScreenShare) {
|
||||
logger.debug('Skipping auto-pin: pinned participant is non-screenshare virtual participant', pinned.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Unpin the screen share when the screen sharing participant leaves. Switch to tile view if no other
|
||||
// participant was pinned before screen share was auto-pinned, pin the previously pinned participant otherwise.
|
||||
if (!remoteScreenShares?.length) {
|
||||
if (!currentScreenShares?.length) {
|
||||
let participantId = null;
|
||||
|
||||
if (pinned && !screenShares.find(share => share === pinned.id)) {
|
||||
if (pinned && !previousScreenShares.find((share: string) => share === pinned.id)) {
|
||||
participantId = pinned.id;
|
||||
}
|
||||
|
||||
logger.debug('No more screenshares, unpinning or restoring previous pin', participantId);
|
||||
dispatch(pinParticipant(participantId));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const latestScreenShareParticipantId = remoteScreenShares[remoteScreenShares.length - 1];
|
||||
const latestScreenShareParticipantId = currentScreenShares[currentScreenShares.length - 1];
|
||||
|
||||
if (latestScreenShareParticipantId) {
|
||||
dispatch(pinParticipant(latestScreenShareParticipantId));
|
||||
const alreadyPinned = pinned?.id === latestScreenShareParticipantId;
|
||||
|
||||
if (!alreadyPinned) {
|
||||
logger.debug(`Auto pinning latest screen share participant: ${latestScreenShareParticipantId}`);
|
||||
dispatch(pinParticipant(latestScreenShareParticipantId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { isFollowMeActive } from '../follow-me/functions';
|
||||
import { SET_TILE_VIEW } from './actionTypes';
|
||||
import { setTileView } from './actions';
|
||||
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
|
||||
|
||||
import logger from './logger';
|
||||
import './subscriber';
|
||||
|
||||
let previousTileViewEnabled: boolean | undefined;
|
||||
@@ -27,13 +27,21 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
// we want to extract the leaving participant and check its type before actually the participant being removed.
|
||||
let shouldUpdateAutoPin = false;
|
||||
let oldScreenShares: Array<string> = [];
|
||||
|
||||
switch (action.type) {
|
||||
case PARTICIPANT_LEFT: {
|
||||
if (!getAutoPinSetting() || isFollowMeActive(store)) {
|
||||
logger.debug('Auto pinning is disabled or Follow Me is active, skipping auto pinning.');
|
||||
|
||||
break;
|
||||
}
|
||||
shouldUpdateAutoPin = Boolean(getParticipantById(store.getState(), action.participant.id)?.fakeParticipant);
|
||||
|
||||
if (shouldUpdateAutoPin) {
|
||||
// Capture the old screenshare list before the reducer runs
|
||||
oldScreenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -75,9 +83,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
|
||||
if (shouldUpdateAutoPin) {
|
||||
const screenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
|
||||
const newScreenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
|
||||
|
||||
updateAutoPinnedParticipant(screenShares, store);
|
||||
updateAutoPinnedParticipant(oldScreenShares, newScreenShares, store);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isFollowMeActive } from '../follow-me/functions';
|
||||
|
||||
import { virtualScreenshareParticipantsUpdated } from './actions';
|
||||
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].sortedRemoteVirtualScreenshareParticipants,
|
||||
@@ -22,14 +23,18 @@ StateListenerRegistry.register(
|
||||
knownSharingParticipantIds.forEach(participantId => {
|
||||
if (!newScreenSharesOrder.includes(participantId)) {
|
||||
newScreenSharesOrder.push(participantId);
|
||||
logger.debug('Adding new screenshare to list', participantId);
|
||||
}
|
||||
});
|
||||
|
||||
if (!equals(oldScreenSharesOrder, newScreenSharesOrder)) {
|
||||
logger.debug('Screenshare order changed, dispatching update');
|
||||
store.dispatch(virtualScreenshareParticipantsUpdated(newScreenSharesOrder));
|
||||
|
||||
if (getAutoPinSetting() && !isFollowMeActive(store)) {
|
||||
updateAutoPinnedParticipant(oldScreenSharesOrder, store);
|
||||
updateAutoPinnedParticipant(oldScreenSharesOrder, newScreenSharesOrder, store);
|
||||
} else {
|
||||
logger.debug('Auto pinning is disabled or Follow Me is active, skipping auto pinning of screenshare.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
if (getState()['features/visitors'].iAmVisitor) {
|
||||
|
||||
const { demoteActorDisplayName } = getState()['features/visitors'];
|
||||
const { showJoinMeetingDialog = true } = getState()['features/base/config'].visitors || {};
|
||||
|
||||
if (demoteActorDisplayName) {
|
||||
const notificationParams: INotificationProps = {
|
||||
@@ -78,7 +79,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
});
|
||||
} else {
|
||||
} else if (showJoinMeetingDialog) {
|
||||
dispatch(openDialog(JoinMeetingDialog));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Module to be enabled under main muc component
|
||||
-- Clean up the room in case it is empty and has only jibri and jigasi-transcriber left in the meeting
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
local is_transcriber = util.is_transcriber;
|
||||
local is_jibri = util.is_jibri;
|
||||
|
||||
local EMPTY_TIMEOUT = module:get_option_number('services_empty_meeting_timeout', 20);
|
||||
|
||||
module:hook('muc-occupant-joined', function (event)
|
||||
local room = event.room;
|
||||
local occupant = event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- clear the timer when someone joins
|
||||
if not is_jibri(occupant.jid) and not is_transcriber(occupant.jid) and room.empty_destroy_timer then
|
||||
room.empty_destroy_timer:stop();
|
||||
room.empty_destroy_timer = nil;
|
||||
end
|
||||
end, -100); -- make sure we are last in the chain
|
||||
|
||||
module:hook('muc-occupant-left', function (event)
|
||||
local occupant, room = event.occupant, event.room;
|
||||
|
||||
if is_admin(occupant.bare_jid) or is_jibri(occupant.jid) or is_transcriber(occupant.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_jibri(o.jid) and not is_transcriber(o.jid)
|
||||
and not is_admin(o.bare_jid) then
|
||||
-- not empty
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- seems the room only has jibri and transcriber, add a timeout to destroy the room
|
||||
room.empty_destroy_timer = module:add_timer(EMPTY_TIMEOUT, function()
|
||||
room:destroy(nil, 'Empty room with recording and/or transcribing.');
|
||||
|
||||
module:log('info',
|
||||
'the conference terminated %s as being empty for %s seconds with recording/transcribing enabled',
|
||||
room.jid, EMPTY_TIMEOUT);
|
||||
end)
|
||||
end, -100); -- the last thing to execute
|
||||
|
||||
module:hook('muc-room-destroyed', function (event)
|
||||
local room = event.room;
|
||||
if room.empty_destroy_timer then
|
||||
room.empty_destroy_timer:stop();
|
||||
room.empty_destroy_timer = nil;
|
||||
end
|
||||
end);
|
||||
|
||||
@@ -254,6 +254,11 @@ function destroy_lobby_room(room, newjid, message)
|
||||
lobby_room_obj:destroy(newjid, message);
|
||||
|
||||
module:log('info', 'Lobby room destroyed %s', lobby_room_obj.jid)
|
||||
|
||||
if room.jitsiMetadata then
|
||||
room.jitsiMetadata.lobbyEnabled = false;
|
||||
module:context(main_muc_component_config):fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end
|
||||
room._data.lobbyroom = nil;
|
||||
room._data.lobby_extra_reason = nil;
|
||||
@@ -488,9 +493,16 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
module:fire_event('jitsi-lobby-enabled', { room = room; });
|
||||
event.status_codes['104'] = true;
|
||||
notify_lobby_enabled(room, actor, true);
|
||||
|
||||
-- let's set it in the metadata and fire the event
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
room.jitsiMetadata.lobbyEnabled = true;
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
elseif room._data.lobbyroom then
|
||||
destroy_lobby_room(room, room.jid);
|
||||
destroy_lobby_room(room, room.jid, nil);
|
||||
module:fire_event('jitsi-lobby-disabled', { room = room; });
|
||||
notify_lobby_enabled(room, actor, false);
|
||||
end
|
||||
@@ -649,6 +661,13 @@ function handle_create_lobby(event)
|
||||
room._data.lobby_extra_reason = event.reason;
|
||||
room._data.lobby_skip_display_name_check = event.skip_display_name_check;
|
||||
|
||||
-- set in metadata without firing room-metadata-changed,
|
||||
-- as this is a backend call and the caller will take care of that
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
room.jitsiMetadata.lobbyEnabled = true;
|
||||
|
||||
-- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
|
||||
room:broadcast_message(
|
||||
st.message({ type='groupchat', from=room.jid })
|
||||
|
||||
@@ -10,6 +10,8 @@ local is_healthcheck_room = main_util.is_healthcheck_room;
|
||||
local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite;
|
||||
local presence_check_status = main_util.presence_check_status;
|
||||
local extract_subdomain = main_util.extract_subdomain;
|
||||
local util = module:require 'util';
|
||||
local is_transcriber = util.is_transcriber;
|
||||
|
||||
local QUEUE_MAX_SIZE = 500;
|
||||
|
||||
@@ -158,7 +160,7 @@ module:hook("muc-occupant-groupchat", function(event)
|
||||
event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
|
||||
end, 45); -- prosody check is prio 50, we want to run after it
|
||||
|
||||
module:hook('message/bare', function(event)
|
||||
local function filterTranscriptionResult(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
if stanza.attr.type ~= 'groupchat' then
|
||||
@@ -190,12 +192,6 @@ module:hook('message/bare', function(event)
|
||||
return;
|
||||
end
|
||||
|
||||
-- TODO: add optimization by moving type and certain fields like is_interim as attribute on 'json-message'
|
||||
-- using string find is roughly 70x faster than json decode for checking the value
|
||||
if string.find(json_message, '"is_interim":true', 1, true) then
|
||||
return;
|
||||
end
|
||||
|
||||
local msg_obj, error = json.decode(json_message);
|
||||
|
||||
if error then
|
||||
@@ -203,6 +199,18 @@ module:hook('message/bare', function(event)
|
||||
return true;
|
||||
end
|
||||
|
||||
if msg_obj.type == 'transcription-result' then
|
||||
if not is_transcriber(stanza.attr.from) then
|
||||
module:log('warn', 'Filtering transcription-result message from non-transcriber: %s', stanza.attr.from);
|
||||
-- Do not fire the event, and do not forward the message
|
||||
return true
|
||||
end
|
||||
if msg_obj.is_interim then
|
||||
-- Do not fire the event, but forward the message
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if msg_obj.transcript ~= nil then
|
||||
local transcription = msg_obj;
|
||||
|
||||
@@ -240,4 +248,7 @@ module:hook('message/bare', function(event)
|
||||
room = room, occupant = occupant, message = msg_obj,
|
||||
origin = event.origin,
|
||||
stanza = stanza, raw_message = json_message });
|
||||
end);
|
||||
end
|
||||
|
||||
module:hook('message/bare', filterTranscriptionResult);
|
||||
module:hook('jitsi-visitor-groupchat-pre-route', filterTranscriptionResult);
|
||||
|
||||
@@ -111,7 +111,8 @@ run_when_component_loaded(main_muc_component_host, function(host_module, host_na
|
||||
-- Check if room should be destroyed when someone leaves the main room
|
||||
|
||||
local main_room = event.room;
|
||||
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
|
||||
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room)
|
||||
or main_room.destroying then
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
@@ -69,8 +69,22 @@ local function verify_user(session, stanza)
|
||||
local user_bare_jid = jid_bare(user_jid);
|
||||
local _, user_domain = jid_split(user_jid);
|
||||
|
||||
-- allowlist for participants
|
||||
if allowlist:contains(user_domain) or allowlist:contains(user_bare_jid) then
|
||||
-- allowlist for participants, jigasi (sip & transcriber), jibri (recorder & sip)
|
||||
if allowlist:contains(user_domain)
|
||||
or allowlist:contains(user_bare_jid)
|
||||
|
||||
-- allow main participants in visitor mode
|
||||
or session.type == 's2sin'
|
||||
|
||||
-- Let Jigasi or transcriber pass throw
|
||||
or util.is_sip_jigasi(stanza)
|
||||
or util.is_transcriber_jigasi(stanza)
|
||||
|
||||
-- is jibri
|
||||
or util.is_jibri(user_jid)
|
||||
|
||||
-- Let Sip Jibri pass through
|
||||
or util.is_sip_jibri_join(stanza) then
|
||||
if DEBUG then module:log("debug", "Token not required from user in allow list: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
@@ -565,6 +565,10 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
elseif room._data.participants then
|
||||
-- This is non jaas room which has a list of participants allowed to participate in the main room
|
||||
-- but this occupant is not one of them and the room is either not live or has no participants joined
|
||||
if room:get_members_only() then
|
||||
-- if there is a lobby, let's pass it through it will wait for the main participant
|
||||
return;
|
||||
end
|
||||
session.log('warn',
|
||||
'Deny user join in the main not live meeting, not in the list of main participants');
|
||||
session.send(st.error_reply(
|
||||
@@ -700,6 +704,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
return;
|
||||
end
|
||||
|
||||
if always_visitors_enabled then
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
room.jitsiMetadata.visitorsEnabled = true;
|
||||
end
|
||||
|
||||
go_live(room);
|
||||
end);
|
||||
end
|
||||
|
||||
@@ -191,8 +191,8 @@ export class Participant {
|
||||
* @param {string} message - The message to log.
|
||||
* @returns {void}
|
||||
*/
|
||||
log(message: string): void {
|
||||
logInfo(this.driver, message);
|
||||
async log(message: string): Promise<void> {
|
||||
await logInfo(this.driver, message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,32 +678,34 @@ export class Participant {
|
||||
* Hangups the participant by leaving the page. base.html is an empty page on all deployments.
|
||||
*/
|
||||
async hangup() {
|
||||
const current = await this.driver.getUrl();
|
||||
console.log('Hanging up');
|
||||
if ((await this.driver.getUrl()).endsWith('/base.html')) {
|
||||
console.log('Already hung up');
|
||||
|
||||
// already hangup
|
||||
if (current.endsWith('/base.html')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do a hangup, to make sure unavailable presence is sent
|
||||
await this.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
|
||||
// @ts-ignore
|
||||
await this.execute(() => window.APP?.conference?.hangup());
|
||||
|
||||
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
|
||||
// change of url
|
||||
// Wait until _room is unset, which is one of the last things hangup() does.
|
||||
await this.driver.waitUntil(
|
||||
async () => {
|
||||
const u = await this.driver.getUrl();
|
||||
|
||||
// trying to debug some failures of reporting not leaving, where we see the close page in screenshot
|
||||
console.log(`initialUrl: ${current} currentUrl: ${u}`);
|
||||
|
||||
return current !== u;
|
||||
try {
|
||||
// @ts-ignore
|
||||
return await this.driver.execute(() => window.APP?.conference?._room === undefined);
|
||||
} catch (e) {
|
||||
// There seems to be a race condition with hangup() causing the page to change, and execute()
|
||||
// might fail with a Bidi error. Retry.
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
timeout: 8000,
|
||||
timeoutMsg: `${this.name} did not leave the muc in 8s initialUrl: ${current}`
|
||||
timeoutMsg: `${this.name} failed to hang up`
|
||||
}
|
||||
);
|
||||
console.log('Hung up');
|
||||
|
||||
await this.driver.url('/base.html')
|
||||
|
||||
|
||||
@@ -6,44 +6,22 @@ export type ITestProperties = {
|
||||
* A more detailed description of what the test does, to be included in the Allure report.
|
||||
*/
|
||||
description?: string;
|
||||
/** The test requires the webhook proxy to be available. */
|
||||
requireWebhookProxy: boolean;
|
||||
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
|
||||
useJaas: boolean;
|
||||
/** The test requires the webhook proxy. */
|
||||
/** The test uses the webhook proxy if available. */
|
||||
useWebhookProxy: boolean;
|
||||
usesBrowsers?: string[];
|
||||
usesBrowsers: string[];
|
||||
};
|
||||
|
||||
const defaultProperties: ITestProperties = {
|
||||
useWebhookProxy: false,
|
||||
requireWebhookProxy: false,
|
||||
useJaas: false,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
|
||||
usesBrowsers: [ 'p1' ]
|
||||
};
|
||||
|
||||
function getDefaultProperties(filename: string): ITestProperties {
|
||||
const properties = { ...defaultProperties };
|
||||
|
||||
properties.usesBrowsers = getDefaultBrowsers(filename);
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
function getDefaultBrowsers(filename: string): string[] {
|
||||
if (filename.includes('/alone/')) {
|
||||
return [ 'p1' ];
|
||||
}
|
||||
if (filename.includes('/2way/')) {
|
||||
return [ 'p1', 'p2' ];
|
||||
}
|
||||
if (filename.includes('/3way/')) {
|
||||
return [ 'p1', 'p2', 'p3' ];
|
||||
}
|
||||
if (filename.includes('/4way/')) {
|
||||
return [ 'p1', 'p2', 'p3', 'p4' ];
|
||||
}
|
||||
|
||||
// Tests outside /alone/, /2way/, /3way/, /4way/ will default to p1 only.
|
||||
return [ 'p1' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a test filename to its registered properties.
|
||||
@@ -63,7 +41,7 @@ export function setTestProperties(filename: string, properties: Partial<ITestPro
|
||||
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
|
||||
}
|
||||
|
||||
testProperties[filename] = { ...getDefaultProperties(filename), ...properties };
|
||||
testProperties[filename] = { ...defaultProperties, ...properties };
|
||||
}
|
||||
|
||||
let testFilesLoaded = false;
|
||||
@@ -101,7 +79,7 @@ export function loadTestFiles(files: string[]): void {
|
||||
require(file);
|
||||
if (!testProperties[file]) {
|
||||
// If no properties were set, apply defaults
|
||||
setTestProperties(file, getDefaultProperties(file));
|
||||
setTestProperties(file, { ...defaultProperties });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not analyze ${file}:`, (error as Error).message);
|
||||
@@ -130,5 +108,11 @@ export function loadTestFiles(files: string[]): void {
|
||||
* @returns Promise<ITestProperties> - The test properties with defaults applied
|
||||
*/
|
||||
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
|
||||
return testProperties[testFilePath] || getDefaultProperties(testFilePath);
|
||||
const properties = testProperties[testFilePath] || { ...defaultProperties };
|
||||
|
||||
if (properties.requireWebhookProxy && !properties.useWebhookProxy) {
|
||||
properties.useWebhookProxy = true;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -45,13 +45,11 @@ export function getLogs(driver: WebdriverIO.Browser) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message in the logfile.
|
||||
*
|
||||
* @param {WebdriverIO.Browser} driver - The participant in which log file to write.
|
||||
* @param {string} message - The message to add.
|
||||
* @returns {void}
|
||||
* Appends value to the log file.
|
||||
* @param {WebdriverIO.Browser} driver - The driver which log file is requested.
|
||||
* @param {string} value - The content to add to the file.
|
||||
*/
|
||||
export function logInfo(driver: WebdriverIO.Browser, message: string) {
|
||||
export function saveLogs(driver: WebdriverIO.Browser, value: string) {
|
||||
// @ts-ignore
|
||||
if (!driver.logFile) {
|
||||
return;
|
||||
@@ -59,9 +57,27 @@ export function logInfo(driver: WebdriverIO.Browser, message: string) {
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
fs.appendFileSync(driver.logFile, `${new Date().toISOString()} ${LOG_PREFIX} ${message}\n`);
|
||||
fs.appendFileSync(driver.logFile, value);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message in the logfile.
|
||||
*
|
||||
* @param {WebdriverIO.Browser} driver - The participant in which log file to write.
|
||||
* @param {string} message - The message to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function logInfo(driver: WebdriverIO.Browser, message: string) {
|
||||
// @ts-ignore
|
||||
if (!driver.logFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
return driver.execute((prefix, msg) =>
|
||||
console.log(`${new Date().toISOString()} ${prefix} ${msg}\n`),
|
||||
LOG_PREFIX, message);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,13 @@ const defaultExpectations = {
|
||||
// The first to join is a moderator.
|
||||
firstModerator: true,
|
||||
// The grantOwner function is available.
|
||||
grantModerator: true
|
||||
}
|
||||
grantModerator: true,
|
||||
// Whether the ability to set a password is available (there's a backend options which makes moderators unable
|
||||
// to set a room password unless they also happen to have a token (any valid token?))
|
||||
setPasswordAvailable: true
|
||||
},
|
||||
// We can create conferences under any tenant.
|
||||
useTenant: true
|
||||
};
|
||||
|
||||
let overrides: any = {};
|
||||
@@ -47,4 +52,6 @@ if (config.expectationsFile) {
|
||||
|
||||
export const expectations = merge(defaultExpectations, overrides);
|
||||
|
||||
console.log('Expectations:', expectations);
|
||||
if (!process.env.WDIO_WORKER_ID) {
|
||||
console.log('Expectations:', expectations);
|
||||
}
|
||||
|
||||
@@ -38,3 +38,13 @@ export async function joinMuc(
|
||||
roomName: joinOptions?.roomName || ctx.roomName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until all participants have ICE connected and have sent and received data (their PC stats are ready).
|
||||
* @param participants
|
||||
*/
|
||||
export async function waitForMedia(participants: Participant[]) {
|
||||
await Promise.all(participants.map(p =>
|
||||
p.waitForIceConnected().then(() => p.waitForSendReceiveData())
|
||||
));
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ import { config as testsConfig } from './TestsConfig';
|
||||
const https = require('https');
|
||||
|
||||
export function generateRoomName(testName: string) {
|
||||
// XXX why chose between 1 and 40 and then always pad with an extra 0?
|
||||
const rand = (Math.floor(Math.random() * 40) + 1).toString().padStart(3, '0');
|
||||
const rand = (Math.floor(Math.random() * 400) + 1).toString();
|
||||
let roomName = `${testName}-${rand}`;
|
||||
|
||||
if (testsConfig.roomName.prefix) {
|
||||
|
||||
@@ -119,7 +119,7 @@ class BreakoutRoom extends BasePageObject {
|
||||
|
||||
await listItem.click();
|
||||
|
||||
const button = listItem.$(`aria/${MORE_LABEL}`);
|
||||
const button = listItem.$(`button[title="${MORE_LABEL}"]`);
|
||||
|
||||
await button.waitForClickable();
|
||||
await button.click();
|
||||
@@ -153,7 +153,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
const addBreakoutButton = this.participant.driver.$(`aria/${ADD_BREAKOUT_ROOM}`);
|
||||
const addBreakoutButton = this.participant.driver.$(`button=${ADD_BREAKOUT_ROOM}`);
|
||||
|
||||
await addBreakoutButton.waitForDisplayed();
|
||||
await addBreakoutButton.click();
|
||||
@@ -179,7 +179,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
const leaveButton = this.participant.driver.$(`aria/${LEAVE_ROOM_LABEL}`);
|
||||
const leaveButton = this.participant.driver.$(`button=${LEAVE_ROOM_LABEL}`);
|
||||
|
||||
await leaveButton.isClickable();
|
||||
await leaveButton.click();
|
||||
@@ -189,7 +189,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
* Auto assign participants to breakout rooms.
|
||||
*/
|
||||
async autoAssignToBreakoutRooms() {
|
||||
const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
|
||||
const button = this.participant.driver.$(`button=${AUTO_ASSIGN_LABEL}`);
|
||||
|
||||
await button.waitForClickable();
|
||||
await button.click();
|
||||
@@ -204,8 +204,9 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.selectParticipant(participant);
|
||||
await participantsPane.openParticipantContextMenu(participant);
|
||||
|
||||
const sendButton = this.participant.driver.$(`aria/${roomName}`);
|
||||
const sendButton = this.participant.driver.$(`div=${roomName}`);
|
||||
|
||||
await sendButton.scrollIntoView();
|
||||
await sendButton.waitForClickable();
|
||||
await sendButton.click();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default class ChatPanel extends BasePageObject {
|
||||
}
|
||||
|
||||
async clickCreatePollButton() {
|
||||
await this.participant.driver.$('aria/Create a poll').click();
|
||||
await this.participant.driver.$('button=Create a poll').click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +95,7 @@ export default class ChatPanel extends BasePageObject {
|
||||
* Clicks the "Add option" button.
|
||||
*/
|
||||
async clickAddOptionButton() {
|
||||
await this.participant.driver.$('aria/Add option').click();
|
||||
await this.participant.driver.$('button=Add option').click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,35 +141,35 @@ export default class ChatPanel extends BasePageObject {
|
||||
* Clicks the "Save" button.
|
||||
*/
|
||||
async clickSavePollButton() {
|
||||
await this.participant.driver.$('aria/Save').click();
|
||||
await this.participant.driver.$('button=Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "Edit" button.
|
||||
*/
|
||||
async clickEditPollButton() {
|
||||
await this.participant.driver.$('aria/Edit').click();
|
||||
await this.participant.driver.$('button=Edit').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "Skip" button.
|
||||
*/
|
||||
async clickSkipPollButton() {
|
||||
await this.participant.driver.$('aria/Skip').click();
|
||||
await this.participant.driver.$('button=Skip').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "Send" button.
|
||||
*/
|
||||
async clickSendPollButton() {
|
||||
await this.participant.driver.$('aria/Send poll').click();
|
||||
await this.participant.driver.$('button=Send').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the "Send" button to be visible.
|
||||
*/
|
||||
async waitForSendButton() {
|
||||
await this.participant.driver.$('aria/Send poll').waitForExist({
|
||||
await this.participant.driver.$('button=Send').waitForExist({
|
||||
timeout: 1000,
|
||||
timeoutMsg: 'Send button not visible'
|
||||
});
|
||||
@@ -185,7 +185,7 @@ export default class ChatPanel extends BasePageObject {
|
||||
(id, ix) => document.getElementById(`poll-answer-checkbox-${id}-${ix}`)?.click(),
|
||||
pollId, index);
|
||||
|
||||
await this.participant.driver.$('aria/Submit').click();
|
||||
await this.participant.driver.$('button=Submit').click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import BasePageObject from './BasePageObject';
|
||||
|
||||
const LOCAL_VIDEO_XPATH = '//span[@id="localVideoContainer"]';
|
||||
const LOCAL_VIDEO_MENU_TRIGGER = '#local-video-menu-trigger';
|
||||
const LOCAL_USER_CONTROLS = 'aria/Local user controls';
|
||||
const LOCAL_USER_CONTROLS = 'button[title="Local user controls"]';
|
||||
const HIDE_SELF_VIEW_BUTTON_XPATH = '//div[contains(@class, "popover")]//div[@id="hideselfviewButton"]';
|
||||
|
||||
/**
|
||||
@@ -136,20 +136,6 @@ export default class Filmstrip extends BasePageObject {
|
||||
return await elem.isExisting() ? await elem.getAttribute('src') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the endpoint is dominant speaker and false otherwise.
|
||||
* Uses the dominant-speaker class on the video thumbnail in order to check.
|
||||
*
|
||||
* @param {string} endpointId - The endpoint id of the participant we want to check.
|
||||
* @returns {boolean} - True if the endpoint is dominant speaker and false otherwise.
|
||||
*/
|
||||
async isDominantSpeaker(endpointId: string) {
|
||||
const elem = this.participant.driver.$(
|
||||
`//span[@id='participant_${endpointId}' and contains(@class,'dominant-speaker')]`);
|
||||
|
||||
return await elem.isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants moderator rights to a participant.
|
||||
* @param participant
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class InviteDialog extends BaseDialog {
|
||||
|
||||
const fullText = await elem.getText();
|
||||
|
||||
this.participant.log(`Extracted text in invite dialog: ${fullText}`);
|
||||
await this.participant.log(`Extracted text in invite dialog: ${fullText}`);
|
||||
|
||||
return fullText.split(':')[1].trim();
|
||||
}
|
||||
|
||||
@@ -106,17 +106,12 @@ export default class Notifications extends BasePageObject {
|
||||
* @private
|
||||
*/
|
||||
private async closeNotification(testId: string, skipNonExisting = false) {
|
||||
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
|
||||
const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
|
||||
|
||||
if (skipNonExisting && !await notification.isExisting()) {
|
||||
if (skipNonExisting && !await closeButton.isExisting()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await notification.waitForExist();
|
||||
await notification.waitForStable();
|
||||
|
||||
const closeButton = notification.$('#close-notification');
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ export default class ParticipantsPane extends BasePageObject {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const inviteButton = this.participant.driver.$(`aria/${INVITE}`);
|
||||
const inviteButton = this.participant.driver.$(`button=${INVITE}`);
|
||||
|
||||
await inviteButton.waitForDisplayed();
|
||||
await inviteButton.click();
|
||||
@@ -262,7 +262,7 @@ export default class ParticipantsPane extends BasePageObject {
|
||||
.substring('participant-item-'.length);
|
||||
|
||||
const moreOptionsButton
|
||||
= this.participant.driver.$(`aria/More moderation options ${participantNameToReject}`);
|
||||
= this.participant.driver.$(`button[title="More moderation options ${participantNameToReject}"]`);
|
||||
|
||||
await moreOptionsButton.click();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export default class PasswordDialog extends BaseDialog {
|
||||
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await passwordInput.waitForExist();
|
||||
await passwordInput.waitForClickable({ timeout: 2000 });
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class Toolbar extends BasePageObject {
|
||||
* @private
|
||||
*/
|
||||
private getButton(accessibilityCSSSelector: string) {
|
||||
return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
|
||||
return this.participant.driver.$(`[aria-label="${accessibilityCSSSelector}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,8 +55,8 @@ export default class Toolbar extends BasePageObject {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickAudioMuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Audio Mute Button');
|
||||
async clickAudioMuteButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Audio Mute Button');
|
||||
|
||||
return this.audioMuteBtn.click();
|
||||
}
|
||||
@@ -66,8 +66,8 @@ export default class Toolbar extends BasePageObject {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickAudioUnmuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Audio Unmute Button');
|
||||
async clickAudioUnmuteButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Audio Unmute Button');
|
||||
|
||||
return this.audioUnMuteBtn.click();
|
||||
}
|
||||
@@ -91,8 +91,8 @@ export default class Toolbar extends BasePageObject {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickVideoMuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Video Mute Button');
|
||||
async clickVideoMuteButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Video Mute Button');
|
||||
|
||||
return this.videoMuteBtn.click();
|
||||
}
|
||||
@@ -102,8 +102,8 @@ export default class Toolbar extends BasePageObject {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickVideoUnmuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Video Unmute Button');
|
||||
async clickVideoUnmuteButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Video Unmute Button');
|
||||
|
||||
return this.videoUnMuteBtn.click();
|
||||
}
|
||||
@@ -113,8 +113,8 @@ export default class Toolbar extends BasePageObject {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickCloseParticipantsPaneButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Close Participants pane Button');
|
||||
async clickCloseParticipantsPaneButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Close Participants pane Button');
|
||||
|
||||
return this.getButton(CLOSE_PARTICIPANTS_PANE).click();
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export default class Toolbar extends BasePageObject {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickParticipantsPaneButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Participants pane Button');
|
||||
async clickParticipantsPaneButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Participants pane Button');
|
||||
|
||||
// Special case for participants pane button, as it contains the number of participants and its label
|
||||
// is changing
|
||||
@@ -150,8 +150,8 @@ export default class Toolbar extends BasePageObject {
|
||||
/**
|
||||
* Clicks on the raise hand button that enables participants will to speak.
|
||||
*/
|
||||
clickRaiseHandButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Raise hand Button');
|
||||
async clickRaiseHandButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Raise hand Button');
|
||||
|
||||
return this.getButton(RAISE_HAND).click();
|
||||
}
|
||||
@@ -159,8 +159,8 @@ export default class Toolbar extends BasePageObject {
|
||||
/**
|
||||
* Clicks on the chat button that opens chat panel.
|
||||
*/
|
||||
clickChatButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Chat Button');
|
||||
async clickChatButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Chat Button');
|
||||
|
||||
return this.getButton(CHAT).click();
|
||||
}
|
||||
@@ -168,8 +168,8 @@ export default class Toolbar extends BasePageObject {
|
||||
/**
|
||||
* Clicks on the chat button that closes chat panel.
|
||||
*/
|
||||
clickCloseChatButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Close Chat Button');
|
||||
async clickCloseChatButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Close Chat Button');
|
||||
|
||||
return this.getButton(CLOSE_CHAT).click();
|
||||
}
|
||||
@@ -205,8 +205,8 @@ export default class Toolbar extends BasePageObject {
|
||||
/**
|
||||
* Clicks on the hangup button that ends the conference.
|
||||
*/
|
||||
clickHangupButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Hangup Button');
|
||||
async clickHangupButton(): Promise<void> {
|
||||
await this.participant.log('Clicking on: Hangup Button');
|
||||
|
||||
return this.getButton(HANGUP).click();
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export default class Toolbar extends BasePageObject {
|
||||
// so let's move focus away before clicking the button
|
||||
await this.participant.driver.$('#overflow-context-menu').moveTo();
|
||||
|
||||
this.participant.log(`Clicking on: ${accessibilityLabel}`);
|
||||
await this.participant.log(`Clicking on: ${accessibilityLabel}`);
|
||||
await this.getButton(accessibilityLabel).click();
|
||||
|
||||
await this.closeOverflowMenu();
|
||||
@@ -248,7 +248,7 @@ export default class Toolbar extends BasePageObject {
|
||||
* @private
|
||||
*/
|
||||
private async isOverflowMenuOpen() {
|
||||
return await this.participant.driver.$$(`aria/${OVERFLOW_MENU}`).length > 0;
|
||||
return await this.participant.driver.$$(`[aria-label="${OVERFLOW_MENU}"]`).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class Visitors extends BasePageObject {
|
||||
* Returns the visitors dialog element if any.
|
||||
*/
|
||||
hasVisitorsDialog() {
|
||||
return this.participant.driver.$('aria/Joining meeting');
|
||||
return this.participant.driver.$('div=Joining meeting');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
|
||||
|
||||
describe('Fake Dial-In', () => {
|
||||
it('join participant', async () => {
|
||||
// we execute fake dial in only if the real dial in is not enabled
|
||||
|
||||
// check rest url is not configured
|
||||
if (process.env.DIAL_IN_REST_URL) {
|
||||
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
const configEnabled = await isDialInEnabled(ctx.p1);
|
||||
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
expect(configEnabled).toBe(expectations.dialIn.enabled);
|
||||
}
|
||||
|
||||
// check dial-in is enabled, so skip
|
||||
if (configEnabled) {
|
||||
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
|
||||
}
|
||||
});
|
||||
|
||||
it('open invite dialog', async () => {
|
||||
await ctx.p1.getInviteDialog().open();
|
||||
|
||||
await ctx.p1.getInviteDialog().clickCloseButton();
|
||||
});
|
||||
|
||||
it('invite second participant', async () => {
|
||||
if (!await ctx.p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureTwoParticipants();
|
||||
});
|
||||
|
||||
it('wait for audio from second participant', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (!await p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await cleanup(p1);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Single port', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('test', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
const port1 = await getRemotePort(p1);
|
||||
const port2 = await getRemotePort(p2);
|
||||
|
||||
expect(Number.isInteger(port1)).toBe(true);
|
||||
expect(Number.isInteger(port2)).toBe(true);
|
||||
expect(port1).toBe(port2);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the remote port of the participant.
|
||||
* @param participant
|
||||
*/
|
||||
async function getRemotePort(participant: Participant) {
|
||||
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.ip);
|
||||
|
||||
const parts = data.split(':');
|
||||
|
||||
return parts.length > 1 ? parseInt(parts[1], 10) : '';
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
const MY_TEST_SUBJECT = 'My Test Subject';
|
||||
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
|
||||
|
||||
describe('Subject', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
subject: MY_TEST_SUBJECT
|
||||
}
|
||||
}));
|
||||
|
||||
it('check', async () => {
|
||||
await checkSubject(ctx.p1, MY_TEST_SUBJECT);
|
||||
await checkSubject(ctx.p2, MY_TEST_SUBJECT);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check was subject set.
|
||||
*
|
||||
* @param participant
|
||||
* @param subject
|
||||
*/
|
||||
async function checkSubject(participant: Participant, subject: string) {
|
||||
const localTile = participant.driver.$(SUBJECT_XPATH);
|
||||
|
||||
await localTile.moveTo();
|
||||
|
||||
expect(await localTile.getText()).toBe(subject);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('UDP', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// just in case wait 1500, this is the interval we use for `config.pcStatsInterval`
|
||||
await p1.driver.pause(1500);
|
||||
|
||||
expect(await getProtocol(p1)).toBe('udp');
|
||||
expect(await getProtocol(p2)).toBe('udp');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the remote port of the participant.
|
||||
* @param participant
|
||||
*/
|
||||
async function getProtocol(participant: Participant) {
|
||||
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.type);
|
||||
|
||||
return data.toLowerCase();
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import { cleanup, dialIn, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
|
||||
|
||||
describe('Dial-In', () => {
|
||||
it('join participant', async () => {
|
||||
// check rest url is configured
|
||||
if (!process.env.DIAL_IN_REST_URL) {
|
||||
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 = 'JaaS is configured.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
expect(await ctx.p1.isInMuc()).toBe(true);
|
||||
|
||||
const configEnabled = await isDialInEnabled(ctx.p1);
|
||||
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
expect(configEnabled).toBe(expectations.dialIn.enabled);
|
||||
}
|
||||
|
||||
if (!configEnabled) {
|
||||
ctx.skipSuiteTests = 'The environment does not support dial-in, and no expectation has been set.';
|
||||
}
|
||||
});
|
||||
|
||||
it('retrieve pin', async () => {
|
||||
let dialInPin: string;
|
||||
|
||||
try {
|
||||
dialInPin = await ctx.p1.getDialInPin();
|
||||
} catch (e) {
|
||||
console.error('dial-in.test.no-pin');
|
||||
ctx.skipSuiteTests = 'No dial-in pin is available.';
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (dialInPin.length === 0) {
|
||||
console.error('dial-in.test.no-pin');
|
||||
ctx.skipSuiteTests = 'The dial-in pin is empty.';
|
||||
throw new Error('no pin');
|
||||
}
|
||||
|
||||
expect(dialInPin.length >= 8).toBe(true);
|
||||
});
|
||||
|
||||
it('invite dial-in participant', async () => {
|
||||
await dialIn(await ctx.p1.getDialInPin());
|
||||
});
|
||||
|
||||
it('wait for audio from dial-in participant', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (!await p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await cleanup(p1);
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import { assertDialInDisplayed, assertUrlDisplayed, isDialInEnabled, verifyMoreNumbersPage } from '../helpers/DialIn';
|
||||
|
||||
describe('Invite', () => {
|
||||
let p1: Participant;
|
||||
|
||||
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 = 'JaaS is configured.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
p1 = ctx.p1;
|
||||
|
||||
});
|
||||
|
||||
// The URL should always be displayed.
|
||||
it('url displayed', () => assertUrlDisplayed(p1));
|
||||
|
||||
it('config values', async () => {
|
||||
const dialInEnabled = await isDialInEnabled(p1);
|
||||
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
expect(dialInEnabled).toBe(expectations.dialIn.enabled);
|
||||
}
|
||||
});
|
||||
|
||||
it('dial-in displayed', async () => {
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
await assertDialInDisplayed(p1, expectations.dialIn.enabled);
|
||||
}
|
||||
});
|
||||
|
||||
it('view more numbers page', async () => {
|
||||
if (expectations.dialIn.enabled === true) {
|
||||
// TODO: assert the page is NOT shown when the expectation is false.
|
||||
await verifyMoreNumbersPage(p1);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
/**
|
||||
* Tests that the digits only password feature works.
|
||||
*
|
||||
* 1. Lock the room with a string (shouldn't work)
|
||||
* 2. Lock the room with a valid numeric password (should work)
|
||||
*/
|
||||
describe('Lock Room with Digits only', () => {
|
||||
it('join participant', () => ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
roomPasswordNumberOfDigits: 5
|
||||
}
|
||||
}));
|
||||
|
||||
it('lock room with digits only', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
expect(await p1.execute(
|
||||
() => APP.store.getState()['features/base/config'].roomPasswordNumberOfDigits === 5)).toBe(true);
|
||||
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(false);
|
||||
|
||||
// Set a non-numeric password.
|
||||
await p1SecurityDialog.addPassword('AAAAA');
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(false);
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await p1SecurityDialog.addPassword('12345');
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('iFrame API for Chat', () => {
|
||||
describe('Chat', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
|
||||
it('setup', async () => {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
import { fetchJson } from '../../helpers/utils';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
@@ -58,15 +58,16 @@ describe('JaaS CHAT_UPLOADED webhook.', () => {
|
||||
expect(uploadedChat.meetingFqn).toBe(fqn);
|
||||
expect(uploadedChat.messageType).toBe('CHAT');
|
||||
|
||||
const messages = uploadedChat.messages;
|
||||
const messages: any[] = 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');
|
||||
console.log(JSON.stringify(messages));
|
||||
expect(messages.some(m => m.name === 'p1' && m.content === 'foo')).toBe(true);
|
||||
expect(messages.some(m => m.name === 'p2' && m.content === 'bar')).toBe(true);
|
||||
expect(messages.some(m => m.name === 'p1' && m.content === 'baz')).toBe(true);
|
||||
messages.forEach(m => {
|
||||
expect(typeof m.timestamp).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,9 @@ describe('Dial-in', () => {
|
||||
|
||||
p1 = await joinJaasMuc({ name: 'p1', token: t({ room, moderator: true }) });
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
if (!webhooksProxy) {
|
||||
console.error('WebhooksProxy is not available, will not verify webhooks.');
|
||||
}
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
@@ -59,13 +62,21 @@ describe('Dial-in', () => {
|
||||
await dialIn(dialInPin);
|
||||
await waitForMedia(p1);
|
||||
|
||||
const startedPayload
|
||||
= await verifyStartedWebhooks(webhooksProxy, 'in', 'DIAL_IN_STARTED', customerId);
|
||||
let startedPayload: any;
|
||||
|
||||
if (webhooksProxy) {
|
||||
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);
|
||||
if (webhooksProxy) {
|
||||
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
|
||||
}
|
||||
|
||||
await p1.hangup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
import { verifyEndedWebhook, verifyStartedWebhooks, waitForMedia } from './util';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
describe('Dial-out', () => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
import { waitForMedia } from './util';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ setTestProperties(__filename, {
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
describe('XMPP login and MUC join test', () => {
|
||||
describe('XMPP login and MUC join', () => {
|
||||
it('with a valid token (wildcard room)', async () => {
|
||||
console.log('Joining a MUC with a valid token (wildcard room)');
|
||||
const p = await joinJaasMuc({ token: t({ room: '*' }) });
|
||||
@@ -98,9 +98,16 @@ describe('XMPP login and MUC join test', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// it('without sending a conference-request', async () => {
|
||||
// console.log('Joining a MUC without sending a conference-request');
|
||||
// // TODO verify failure
|
||||
// //expect(await joinMuc(ctx.roomName, 'p1', token)).toBe(true);
|
||||
// });
|
||||
it('without sending a conference-request', async () => {
|
||||
console.log('Joining a MUC without sending a conference-request');
|
||||
const p = await joinJaasMuc({
|
||||
token: t({ room: `${ctx.roomName}-no-cf` })
|
||||
}, {
|
||||
configOverwrite: {
|
||||
disableFocus: true // this effectively disables the sending of a conference-request
|
||||
}
|
||||
});
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@ import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('MaxOccupants limit enforcement', () => {
|
||||
describe('MaxOccupants', () => {
|
||||
it('test maxOccupants limit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
maxOccupants: 2
|
||||
|
||||
@@ -3,8 +3,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
import { IToken } from '../../helpers/token';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
// This test is separate from passcode.spec.ts, because it needs to use a different room name, and webhooksProxy is only
|
||||
// setup for the default room name.
|
||||
describe('Setting passcode through settings provisioning', () => {
|
||||
describe('Setting invalid passcode through settings provisioning', () => {
|
||||
it('With an invalid passcode', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
passcode: 'passcode-must-be-digits-only'
|
||||
|
||||
@@ -7,8 +7,8 @@ import WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ setTestProperties(__filename, {
|
||||
* TODO: read flags from config.
|
||||
* TODO: also assert "this meeting is being recorder" notificaitons are show/played?
|
||||
*/
|
||||
describe('Recording and Live Streaming', () => {
|
||||
describe('Recording and live-streaming', () => {
|
||||
const tenant = testsConfig.jaas.tenant;
|
||||
const customerId = tenant?.replace('vpaas-magic-cookie-', '');
|
||||
// TODO: read from config
|
||||
@@ -72,12 +72,20 @@ describe('Recording and Live Streaming', () => {
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
const iFrameEvent = (await p.getIframeAPI().getEventResult('recordingStatusChanged'));
|
||||
const iFrameEvent = await p.driver.waitUntil(() =>
|
||||
p.getIframeAPI().getEventResult('recordingStatusChanged'), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'recordingStatusChanged event not received'
|
||||
});
|
||||
|
||||
expect(iFrameEvent.mode).toBe('file');
|
||||
expect(iFrameEvent.on).toBe(true);
|
||||
|
||||
const linkEvent = (await p.getIframeAPI().getEventResult('recordingLinkAvailable'));
|
||||
const linkEvent = await p.driver.waitUntil(() =>
|
||||
p.getIframeAPI().getEventResult('recordingLinkAvailable'), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'recordingLinkAvailable event not received'
|
||||
});
|
||||
|
||||
expect(linkEvent.link.startsWith('https://')).toBe(true);
|
||||
expect(linkEvent.link.includes(tenant)).toBe(true);
|
||||
|
||||
@@ -6,12 +6,12 @@ import type WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Transcriptions', () => {
|
||||
describe('Transcription', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import WebhookProxy from '../../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
|
||||
it('test visitor tokens', async () => {
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
const moderatorToken = t({ room, displayName: 'Mo de Rator', moderator: true });
|
||||
const moderator = await joinJaasMuc({ name: 'p1', token: moderatorToken });
|
||||
|
||||
@@ -90,6 +91,7 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await moderator.isVisitor()).toBe(false);
|
||||
await verifyJoinedWebhook(moderator);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a participant token before any visitors
|
||||
const participantToken = t({ room, displayName: 'Parti Cipant' });
|
||||
const participant = await joinJaasMuc({ name: 'p2', token: participantToken });
|
||||
@@ -99,6 +101,7 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await participant.isVisitor()).toBe(false);
|
||||
await verifyJoinedWebhook(participant);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a visitor token
|
||||
const visitorToken = t({ room, displayName: 'Visi Tor', visitor: true });
|
||||
const visitor = await joinJaasMuc({ name: 'p3', token: visitorToken });
|
||||
@@ -108,9 +111,11 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await visitor.isVisitor()).toBe(true);
|
||||
await verifyJoinedWebhook(visitor);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await participant.hangup();
|
||||
await verifyLeftWebhook(participant);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a participant token after visitors -> visitor
|
||||
const participantToken2 = t({ room, displayName: 'Visi Tor 2' });
|
||||
const visitor2 = await joinJaasMuc({ name: 'p2', token: participantToken2 });
|
||||
@@ -120,12 +125,15 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await visitor2.isVisitor()).toBe(true);
|
||||
await verifyJoinedWebhook(visitor2);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await visitor.hangup();
|
||||
await verifyLeftWebhook(visitor);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await visitor2.hangup();
|
||||
await verifyLeftWebhook(visitor2);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await moderator.hangup();
|
||||
await verifyLeftWebhook(moderator);
|
||||
});
|
||||
|
||||
@@ -5,12 +5,12 @@ import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Visitors', () => {
|
||||
describe('Visitors live', () => {
|
||||
let visitor: Participant, moderator: Participant;
|
||||
|
||||
it('setup', async () => {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureThreeParticipants } from '../../helpers/participants';
|
||||
import { muteAudioAndCheck } from '../helpers/mute';
|
||||
|
||||
describe('ActiveSpeaker', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Active speaker', () => {
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
@@ -41,19 +46,19 @@ describe('ActiveSpeaker', () => {
|
||||
*/
|
||||
async function testActiveSpeaker(
|
||||
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
|
||||
activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
|
||||
await activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
|
||||
|
||||
const speakerEndpoint = await activeSpeaker.getEndpointId();
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Unmuting in testActiveSpeaker');
|
||||
await activeSpeaker.log('Unmuting in testActiveSpeaker');
|
||||
|
||||
// Unmute
|
||||
await activeSpeaker.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// just a debug print to go in logs
|
||||
otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
await otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
await otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
|
||||
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
|
||||
|
||||
@@ -61,21 +66,21 @@ async function testActiveSpeaker(
|
||||
const otherParticipant1Driver = otherParticipant1.driver;
|
||||
|
||||
await otherParticipant1Driver.waitUntil(
|
||||
async () => await otherParticipant1.getFilmstrip().isDominantSpeaker(speakerEndpoint),
|
||||
async () => await otherParticipant1.getLargeVideo().getResource() === speakerEndpoint,
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: `${activeSpeaker.name} is not selected as active speaker.`
|
||||
timeoutMsg: 'Active speaker not displayed on large video.'
|
||||
});
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Muting in testActiveSpeaker');
|
||||
await activeSpeaker.log('Muting in testActiveSpeaker');
|
||||
|
||||
// Mute back again
|
||||
await activeSpeaker.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// just a debug print to go in logs
|
||||
otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
await otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
await otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
|
||||
await otherParticipant1.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Audio only', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Audio-only mode', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
@@ -7,14 +8,18 @@ import {
|
||||
} from '../../helpers/participants';
|
||||
import { unmuteAudioAndCheck, unmuteVideoAndCheck } from '../helpers/mute';
|
||||
|
||||
describe('AVModeration', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
it('check for moderators', async () => {
|
||||
// if all 3 participants are moderators, skip this test
|
||||
describe('Audio/video moderation', () => {
|
||||
|
||||
it('setup', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// if all 3 participants are moderators, skip this test
|
||||
if (!await p1.isModerator()
|
||||
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
|
||||
ctx.skipSuiteTests = `Unsupported moderator configuration: p1=${await p1.isModerator()},\
|
||||
@@ -1,9 +1,17 @@
|
||||
import { VIDEO_CODEC } from '../../../react/features/video-quality/constants';
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
const { VP8, VP9, AV1 } = VIDEO_CODEC;
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Codec selection', () => {
|
||||
it('asymmetric codecs', async () => {
|
||||
@@ -30,19 +38,17 @@ describe('Codec selection', () => {
|
||||
|
||||
// Check if p1 is sending VP9 and p2 is sending VP8 as per their codec preferences.
|
||||
// Except on Firefox because it doesn't support VP9 encode.
|
||||
if (p1.driver.isFirefox) {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
} else {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
}
|
||||
const p1ExpectedCodec = p1.driver.isFirefox ? VP8 : VP9;
|
||||
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, p1ExpectedCodec),
|
||||
waitForCodec(p2, VP8)
|
||||
]);
|
||||
});
|
||||
|
||||
it('asymmetric codecs with AV1', async () => {
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
disableTileView: true,
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
|
||||
}
|
||||
@@ -57,20 +63,14 @@ describe('Codec selection', () => {
|
||||
|
||||
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
|
||||
// Except on Firefox because it doesn't support VP9 encode.
|
||||
if (p1.driver.isFirefox) {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
} else {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
}
|
||||
const p1ExpectedCodec = p1.driver.isFirefox ? VP8 : VP9;
|
||||
const p3ExpectedCodec = (p1.driver.isFirefox && majorVersion < 136) ? VP9 : AV1;
|
||||
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
|
||||
// If there is a Firefox ep in the call, all other eps will switch to VP9.
|
||||
if (p1.driver.isFirefox && majorVersion < 136) {
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
} else {
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
|
||||
}
|
||||
await Promise.all([
|
||||
waitForCodec(p1, p1ExpectedCodec),
|
||||
waitForCodec(p2, VP8),
|
||||
waitForCodec(p3, p3ExpectedCodec)
|
||||
]);
|
||||
});
|
||||
|
||||
it('codec switch over', async () => {
|
||||
@@ -91,12 +91,13 @@ describe('Codec selection', () => {
|
||||
}
|
||||
|
||||
// Check if p1 and p2 are encoding in VP9 which is the default codec.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, VP9),
|
||||
waitForCodec(p2, VP9)
|
||||
]);
|
||||
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
disableTileView: true,
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'VP8' ]
|
||||
}
|
||||
@@ -104,27 +105,28 @@ describe('Codec selection', () => {
|
||||
});
|
||||
const { p3 } = ctx;
|
||||
|
||||
// Check if all three participants are encoding in VP8 now.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, VP8),
|
||||
waitForCodec(p2, VP8),
|
||||
waitForCodec(p3, VP8)
|
||||
]);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Check of p1 and p2 have switched to VP9.
|
||||
await p1.driver.waitUntil(
|
||||
() => p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'p1 did not switch back to VP9'
|
||||
}
|
||||
);
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'p1 did not switch back to VP9'
|
||||
}
|
||||
);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, VP9),
|
||||
waitForCodec(p2, VP9)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForCodec(p: Participant, codec: string) {
|
||||
await p.driver.waitUntil(
|
||||
() => p.execute(c => JitsiMeetJS.app.testing.getLocalCameraEncoding() === c, codec),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: `${p.name} failed to use VP8`
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SET_AUDIO_ONLY } from '../../../react/features/base/audio-only/actionTypes';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureFourParticipants,
|
||||
@@ -8,17 +9,23 @@ import {
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
|
||||
});
|
||||
|
||||
describe('Desktop sharing', () => {
|
||||
it('start', async () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Check if a remote screen share tile is created on p1.
|
||||
@@ -48,7 +55,13 @@ describe('Desktop sharing', () => {
|
||||
it('p2p to jvb switch', async () => {
|
||||
await ctx.p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Check if a remote screen share tile is created on all participants.
|
||||
@@ -67,6 +80,10 @@ describe('Desktop sharing', () => {
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Wait for p1 and p2 to switch back to p2p.
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.waitForP2PIceConnected();
|
||||
|
||||
// Check if a remote screen share tile is created on p1 and p2 after switching back to p2p.
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
await checkForScreensharingTile(p2, p2);
|
||||
@@ -81,7 +98,14 @@ describe('Desktop sharing', () => {
|
||||
await checkForScreensharingTile(p1, p1);
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
await checkForScreensharingTile(p2, p3);
|
||||
@@ -103,6 +127,10 @@ describe('Desktop sharing', () => {
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Wait for p1 and p2 to switch back to p2p.
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.waitForP2PIceConnected();
|
||||
|
||||
// Start share on both p1 and p2.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
@@ -112,7 +140,14 @@ describe('Desktop sharing', () => {
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
|
||||
// Add p3 back to the conference and check if p1 and p2's shares are visible on p3.
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
await checkForScreensharingTile(p2, p3);
|
||||
@@ -132,6 +167,7 @@ describe('Desktop sharing', () => {
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
@@ -146,7 +182,14 @@ describe('Desktop sharing', () => {
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
// Call switches to jvb.
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p2, p3 } = ctx;
|
||||
|
||||
// p1 starts share again when call switches to jvb.
|
||||
@@ -162,6 +205,10 @@ describe('Desktop sharing', () => {
|
||||
// p3 leaves the call.
|
||||
await p3.hangup();
|
||||
|
||||
// Wait for p1 and p2 to switch back to p2p.
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.waitForP2PIceConnected();
|
||||
|
||||
// Make sure p2 see's p1's share after the call switches back to p2p.
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureFourParticipants, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('lastN', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
|
||||
});
|
||||
|
||||
describe('LastN', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureOneParticipant,
|
||||
@@ -11,6 +12,10 @@ import {
|
||||
unmuteVideoAndCheck
|
||||
} from '../helpers/mute';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Mute', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('SwitchVideo', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Pinning', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('p1 click on local', () => ctx.p1.getFilmstrip().pinParticipant(ctx.p1));
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureOneParticipant,
|
||||
@@ -8,7 +9,11 @@ import {
|
||||
} from '../../helpers/participants';
|
||||
import { unmuteVideoAndCheck } from '../helpers/mute';
|
||||
|
||||
describe('StartMuted', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Start muted', () => {
|
||||
it('checkboxes test', async () => {
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
@@ -122,7 +127,7 @@ describe('StartMuted', () => {
|
||||
|
||||
const p2ID = await p2.getEndpointId();
|
||||
|
||||
p1.log(`Start configOptionsTest, second participant: ${p2ID}`);
|
||||
await p1.log(`Start configOptionsTest, second participant: ${p2ID}`);
|
||||
|
||||
// Participant 3 should be muted, 1 and 2 unmuted.
|
||||
await p3.getFilmstrip().assertAudioMuteIconIsDisplayed(p3);
|
||||
@@ -143,7 +148,7 @@ describe('StartMuted', () => {
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
p1.log('configOptionsTest, unmuted third participant');
|
||||
await p1.log('configOptionsTest, unmuted third participant');
|
||||
await p1.waitForAudioMuted(p3, false /* unmuted */);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
import { muteVideoAndCheck, unmuteVideoAndCheck } from '../helpers/mute';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Stop video', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
|
||||
import { unmuteVideoAndCheck } from '../helpers/mute';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
const EMAIL = 'support@jitsi.org';
|
||||
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChainablePromiseElement } from 'webdriverio';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
checkSubject,
|
||||
ensureThreeParticipants,
|
||||
@@ -8,11 +9,15 @@ import {
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
const MAIN_ROOM_NAME = 'Main room';
|
||||
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
|
||||
const LIST_ITEM_CONTAINER = 'list-item-container';
|
||||
|
||||
describe('BreakoutRooms', () => {
|
||||
describe('Breakout rooms', () => {
|
||||
it('check support', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
@@ -34,7 +39,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'No breakout room added for p1'
|
||||
});
|
||||
|
||||
@@ -42,7 +47,7 @@ describe('BreakoutRooms', () => {
|
||||
// second participant should also see one breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'No breakout room seen by p2'
|
||||
});
|
||||
});
|
||||
@@ -54,7 +59,7 @@ describe('BreakoutRooms', () => {
|
||||
// there should be one breakout room
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'No breakout room seen by p1'
|
||||
});
|
||||
|
||||
@@ -95,7 +100,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
|
||||
});
|
||||
});
|
||||
@@ -137,7 +142,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].participantCount === 0;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P2 is seeing p1 in the breakout room'
|
||||
});
|
||||
});
|
||||
@@ -152,14 +157,14 @@ describe('BreakoutRooms', () => {
|
||||
// there should be no breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Breakout room was not removed for p1'
|
||||
});
|
||||
|
||||
// the second participant should also see no breakout rooms
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Breakout room was not removed for p2'
|
||||
});
|
||||
});
|
||||
@@ -176,7 +181,7 @@ describe('BreakoutRooms', () => {
|
||||
// there should be two breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Breakout room was not created by p1'
|
||||
});
|
||||
|
||||
@@ -220,7 +225,7 @@ describe('BreakoutRooms', () => {
|
||||
return list[0].participantCount === 1 && list[1].participantCount === 1
|
||||
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P2 is not seeing p1 in the main room'
|
||||
});
|
||||
});
|
||||
@@ -244,7 +249,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].participantCount === 1 && list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 is not seeing two breakout rooms'
|
||||
});
|
||||
|
||||
@@ -305,7 +310,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].participantCount + list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
|
||||
});
|
||||
|
||||
@@ -336,7 +341,7 @@ describe('BreakoutRooms', () => {
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1
|
||||
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'No breakout room added for p1'
|
||||
});
|
||||
|
||||
@@ -374,7 +379,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
});
|
||||
|
||||
@@ -420,7 +425,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].name === myNewRoomName;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'The breakout room was not renamed for p1'
|
||||
});
|
||||
|
||||
@@ -442,7 +447,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list[0].participantCount === 0;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'The breakout room not found or not empty for p1'
|
||||
});
|
||||
|
||||
@@ -452,7 +457,7 @@ describe('BreakoutRooms', () => {
|
||||
|
||||
return list?.length === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'The breakout room not seen by p2'
|
||||
});
|
||||
|
||||
54
tests/specs/misc/connectivity.spec.ts
Normal file
54
tests/specs/misc/connectivity.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { joinMuc, waitForMedia } from '../../helpers/joinMuc';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
description: 'This test asserts that the connection to JVB is over UDP and using the same remote port. ',
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Connectivity', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
|
||||
it('setup', async () => {
|
||||
p1 = await joinMuc({ name: 'p1', token: testsConfig.jwt.preconfiguredToken });
|
||||
p2 = await joinMuc({ name: 'p2', token: testsConfig.jwt.preconfiguredToken });
|
||||
await waitForMedia([ p1, p2 ]);
|
||||
});
|
||||
|
||||
it('protocol', async () => {
|
||||
expect(await getProtocol(p1)).toBe('udp');
|
||||
expect(await getProtocol(p2)).toBe('udp');
|
||||
});
|
||||
|
||||
it('port', async () => {
|
||||
const port1 = await getRemotePort(p1);
|
||||
const port2 = await getRemotePort(p2);
|
||||
|
||||
expect(Number.isInteger(port1)).toBe(true);
|
||||
expect(Number.isInteger(port2)).toBe(true);
|
||||
expect(port1).toBe(port2);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the remote port of the participant.
|
||||
* @param participant
|
||||
*/
|
||||
async function getRemotePort(participant: Participant) {
|
||||
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.ip);
|
||||
const parts = data.split(':');
|
||||
|
||||
return parts.length > 1 ? parseInt(parts[1], 10) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the remote port of the participant.
|
||||
* @param participant
|
||||
*/
|
||||
async function getProtocol(participant: Participant) {
|
||||
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.type);
|
||||
|
||||
return data.toLowerCase();
|
||||
}
|
||||
120
tests/specs/misc/dialIn.spec.ts
Normal file
120
tests/specs/misc/dialIn.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import {
|
||||
assertDialInDisplayed,
|
||||
assertUrlDisplayed,
|
||||
cleanup,
|
||||
dialIn,
|
||||
isDialInEnabled, verifyMoreNumbersPage,
|
||||
waitForAudioFromDialInParticipant
|
||||
} from '../helpers/DialIn';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1' ]
|
||||
});
|
||||
|
||||
/**
|
||||
* This test is configured with two options:
|
||||
* 1. The dialIn.enabled expectation. If set to true we assert the config.js settings for dial-in are set, the dial-in
|
||||
* panel (including the PIN number) is displayed in the UI, and the "more numbers" page is displayed. If it's set to
|
||||
* false we assert the config.js settings are not set, and the PIN is not displayed.
|
||||
* 2. The DIAL_IN_REST_URL environment variable. If this is set and the environment supports dial-in, we invite a
|
||||
* dial-in participant via this URL and assert that it joins the conference and sends media.
|
||||
*/
|
||||
describe('Dial-in', () => {
|
||||
it('join participant', async () => {
|
||||
// The same cases are covered for JaaS in jaas/dial/dialin.spec.ts.
|
||||
if (testsConfig.jaas.enabled) {
|
||||
ctx.skipSuiteTests = 'JaaS is configured.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
expect(await ctx.p1.isInMuc()).toBe(true);
|
||||
});
|
||||
|
||||
it('dial in config.js values', async function() {
|
||||
if (expectations.dialIn.enabled === true) {
|
||||
expect(await isDialInEnabled(ctx.p1)).toBe(expectations.dialIn.enabled);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-this
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('open/close invite dialog', async () => {
|
||||
await ctx.p1.getInviteDialog().open();
|
||||
await ctx.p1.getInviteDialog().clickCloseButton();
|
||||
await ctx.p1.getInviteDialog().waitTillOpen(true);
|
||||
});
|
||||
|
||||
it('dial-in displayed', async function() {
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
await assertDialInDisplayed(ctx.p1, expectations.dialIn.enabled);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-this
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('skip the rest if dial-in is not expected', async () => {
|
||||
if (!expectations.dialIn.enabled) {
|
||||
ctx.skipSuiteTests = 'Dial-in is not expected';
|
||||
}
|
||||
});
|
||||
|
||||
it('url displayed', () => assertUrlDisplayed(ctx.p1));
|
||||
|
||||
|
||||
it('view more numbers page', async () => {
|
||||
await verifyMoreNumbersPage(ctx.p1);
|
||||
});
|
||||
|
||||
it('retrieve pin', async () => {
|
||||
let dialInPin: string;
|
||||
|
||||
try {
|
||||
dialInPin = await ctx.p1.getDialInPin();
|
||||
} catch (e) {
|
||||
console.error('dial-in.test.no-pin');
|
||||
ctx.skipSuiteTests = 'No dial-in pin is available.';
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (dialInPin.length === 0) {
|
||||
console.error('dial-in.test.no-pin');
|
||||
ctx.skipSuiteTests = 'The dial-in pin is empty.';
|
||||
throw new Error('no pin');
|
||||
}
|
||||
|
||||
expect(dialInPin.length >= 8).toBe(true);
|
||||
});
|
||||
|
||||
it('skip the rest if a dial-in URL is not configured', async () => {
|
||||
if (!process.env.DIAL_IN_REST_URL) {
|
||||
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
|
||||
}
|
||||
});
|
||||
|
||||
it('invite dial-in participant', async () => {
|
||||
await dialIn(await ctx.p1.getDialInPin());
|
||||
});
|
||||
|
||||
it('wait for audio from dial-in participant', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (!await p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await cleanup(p1);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Follow Me', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Follow me', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Polls', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants();
|
||||
37
tests/specs/misc/subject.spec.ts
Normal file
37
tests/specs/misc/subject.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
const MY_TEST_SUBJECT = 'My Test Subject';
|
||||
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Subject', () => {
|
||||
it('setup', async () => {
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
subject: MY_TEST_SUBJECT
|
||||
}
|
||||
});
|
||||
await ensureTwoParticipants();
|
||||
});
|
||||
it('subject set locally', async () => await checkSubject(ctx.p1, MY_TEST_SUBJECT));
|
||||
it('subject set remotely', async () => await checkSubject(ctx.p2, MY_TEST_SUBJECT));
|
||||
});
|
||||
|
||||
/**
|
||||
* Check was subject set.
|
||||
*
|
||||
* @param participant
|
||||
* @param subject
|
||||
*/
|
||||
async function checkSubject(participant: Participant, subject: string) {
|
||||
const localTile = participant.driver.$(SUBJECT_XPATH);
|
||||
|
||||
await localTile.moveTo();
|
||||
|
||||
expect(await localTile.getText()).toBe(subject);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinMuc } from '../../helpers/joinMuc';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1' ]
|
||||
});
|
||||
|
||||
describe('URL Normalisation', () => {
|
||||
describe('URL normalisation', () => {
|
||||
// If we're not able to create conferences with a custom tenant, we'll only test the room name.
|
||||
const useTenant = expectations.useTenant;
|
||||
|
||||
const tests = [
|
||||
{
|
||||
hint: '@ sign and .',
|
||||
@@ -47,12 +51,13 @@ describe('URL Normalisation', () => {
|
||||
it(test.hint, async () => {
|
||||
const fullRoom = `${test.room}${ctx.roomName}`;
|
||||
const fullRoomUrl = `${test.roomUrl}${ctx.roomName}`;
|
||||
const tenant = useTenant ? test.tenant : undefined;
|
||||
|
||||
const p = await joinMuc({
|
||||
name: 'p1',
|
||||
token: testsConfig.jwt.preconfiguredToken,
|
||||
}, {
|
||||
tenant: test.tenant,
|
||||
tenant: tenant,
|
||||
roomName: fullRoom
|
||||
});
|
||||
|
||||
@@ -61,7 +66,9 @@ describe('URL Normalisation', () => {
|
||||
const path = currentUrl.pathname;
|
||||
const parts = path.split('/');
|
||||
|
||||
expect(parts[1]).toBe(test.tenantUrl);
|
||||
if (useTenant) {
|
||||
expect(parts[1]).toBe(test.tenantUrl);
|
||||
}
|
||||
expect(parts[2]).toBe(fullRoomUrl);
|
||||
|
||||
const mucJid = (await p.execute(() => APP.conference._room.room.roomjid)).split('@');
|
||||
@@ -69,7 +76,9 @@ describe('URL Normalisation', () => {
|
||||
const domain = mucJid[1];
|
||||
|
||||
expect(roomJid).toBe(`${test.roomJid}${ctx.roomName}`);
|
||||
expect(domain.startsWith(`conference.${test.tenantJid}.`)).toBe(true);
|
||||
if (useTenant) {
|
||||
expect(domain.startsWith(`conference.${test.tenantJid}.`)).toBe(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { P1, P3, Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
@@ -9,6 +10,10 @@ import {
|
||||
import type { IJoinOptions } from '../../helpers/types';
|
||||
import type PreMeetingScreen from '../../pageobjects/PreMeetingScreen';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Lobby', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureOneParticipant();
|
||||
@@ -1,41 +1,56 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
|
||||
import type SecurityDialog from '../../pageobjects/SecurityDialog';
|
||||
|
||||
let roomKey: string;
|
||||
setTestProperties(__filename, {
|
||||
description: '1. Set a room password (assert the image changes to locked). \
|
||||
2. Join with a second participant. \
|
||||
3. Assert password is required (and padlock is locked). \
|
||||
4. Assert wrong password fails. \
|
||||
5. Unlock the room (assert the padlock is unlocked) \
|
||||
6. Assert room is unlocked and the padlock is unlocked.',
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
/**
|
||||
* 1. Lock the room (make sure the image changes to locked)
|
||||
* 2. Join with a second browser/tab
|
||||
* 3. Make sure we are required to enter a password.
|
||||
* (Also make sure the padlock is locked)
|
||||
* 4. Enter wrong password, make sure we are not joined in the room
|
||||
* 5. Unlock the room (Make sure the padlock is unlocked)
|
||||
* 6. Join again and make sure we are not asked for a password and that
|
||||
* the padlock is unlocked.
|
||||
*/
|
||||
describe('Lock Room', () => {
|
||||
it('joining the meeting', () => ensureOneParticipant());
|
||||
|
||||
it('locks the room', () => participant1LockRoom());
|
||||
describe('Lock room', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let roomKey: string;
|
||||
|
||||
it('setup', async () => {
|
||||
if (!expectations.moderation.setPasswordAvailable) {
|
||||
ctx.skipSuiteTests = 'setPasswordAvailable is not expected to be available';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
p1 = ctx.p1;
|
||||
|
||||
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
await setPassword(p1, roomKey);
|
||||
});
|
||||
it('enter participant in locked room', async () => {
|
||||
// first enter wrong pin then correct one
|
||||
await joinSecondParticipant({
|
||||
skipWaitToJoin: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
p2 = ctx.p2;
|
||||
|
||||
// wait for password prompt
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
// Submit a wrong password
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
|
||||
|
||||
// give sometime to the password prompt to disappear and send the password
|
||||
// give some time to the password prompt to disappear and send the password
|
||||
// TODO: wait until the dialog is not displayed? Assert the room is not joined?
|
||||
await p2.driver.pause(500);
|
||||
|
||||
// wait for password prompt
|
||||
// Submit the correct password
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(roomKey);
|
||||
|
||||
@@ -50,57 +65,51 @@ describe('Lock Room', () => {
|
||||
});
|
||||
|
||||
it('unlock room', async () => {
|
||||
// Unlock room. Check whether room is still locked. Click remove and check whether it is unlocked.
|
||||
await ctx.p2.hangup();
|
||||
await p2.hangup();
|
||||
|
||||
await participant1UnlockRoom();
|
||||
await removePassword(p1);
|
||||
});
|
||||
|
||||
it('enter participant in unlocked room', async () => {
|
||||
it('join the unlocked room', async () => {
|
||||
// Just enter the room and check that is not locked.
|
||||
// if we fail to unlock the room this one will detect it
|
||||
// as participant will fail joining
|
||||
await ensureTwoParticipants();
|
||||
p2 = ctx.p2;
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
|
||||
await p2SecurityDialog.clickCloseButton();
|
||||
});
|
||||
|
||||
it('update locked state while participants in room', async () => {
|
||||
it('set password while participants are in the room', async () => {
|
||||
// Both participants are in unlocked room, lock it and see whether the
|
||||
// change is reflected on the second participant icon.
|
||||
await participant1LockRoom();
|
||||
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
await setPassword(p1, roomKey);
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, true);
|
||||
|
||||
await participant1UnlockRoom();
|
||||
|
||||
await removePassword(p1);
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
});
|
||||
it('unlock after participant enter wrong password', async () => {
|
||||
// P1 locks the room. Participant tries to enter using wrong password.
|
||||
// P1 unlocks the room and Participant submits the password prompt with no password entered and
|
||||
// should enter of unlocked room.
|
||||
await ctx.p2.hangup();
|
||||
await participant1LockRoom();
|
||||
await p2.hangup();
|
||||
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
await setPassword(p1, roomKey);
|
||||
await joinSecondParticipant({
|
||||
skipWaitToJoin: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
p2 = ctx.p2;
|
||||
|
||||
// wait for password prompt
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
@@ -114,7 +123,7 @@ describe('Lock Room', () => {
|
||||
// wait for password prompt
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
|
||||
await participant1UnlockRoom();
|
||||
await removePassword(p1);
|
||||
|
||||
await p2PasswordDialog.clickOkButton();
|
||||
await p2.waitToJoinMUC();
|
||||
@@ -129,46 +138,33 @@ describe('Lock Room', () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Participant1 locks the room.
|
||||
* Set a room password via the UI.
|
||||
*/
|
||||
async function participant1LockRoom() {
|
||||
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
async function setPassword(p: Participant, password: string) {
|
||||
const securityDialog = p.getSecurityDialog();
|
||||
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.addPassword(roomKey);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, true);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
await p.getToolbar().clickSecurityButton();
|
||||
await securityDialog.waitForDisplay();
|
||||
await waitForRoomLockState(securityDialog, false);
|
||||
await securityDialog.addPassword(password);
|
||||
await securityDialog.clickCloseButton();
|
||||
await p.getToolbar().clickSecurityButton();
|
||||
await securityDialog.waitForDisplay();
|
||||
await waitForRoomLockState(securityDialog, true);
|
||||
await securityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Participant1 unlocks the room.
|
||||
* Remove the room password via the UI.
|
||||
*/
|
||||
async function participant1UnlockRoom() {
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
async function removePassword(p: Participant) {
|
||||
const securityDialog = p.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await p1SecurityDialog.removePassword();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
await p.getToolbar().clickSecurityButton();
|
||||
await securityDialog.waitForDisplay();
|
||||
await securityDialog.removePassword();
|
||||
await waitForRoomLockState(securityDialog, false);
|
||||
await securityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
describe('Chat Panel', () => {
|
||||
describe('Chat panel', () => {
|
||||
it('join participant', () => ensureOneParticipant());
|
||||
|
||||
it('start closed', async () => {
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('DisplayName', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Display name', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants({ skipDisplayName: true }));
|
||||
|
||||
it('check change', async () => {
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('End Conference', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Hangup', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('hangup call and check', async () => {
|
||||
63
tests/specs/ui/lockRoomDigitsOnly.spec.ts
Normal file
63
tests/specs/ui/lockRoomDigitsOnly.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinMuc } from '../../helpers/joinMuc';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
description: ' Tests that the digits only password feature works. When the roomPasswordNumberOfDigits config \
|
||||
option is set, the UI should only allow setting the password to a string of digits (with the given length).'
|
||||
});
|
||||
describe('Lock room with digits only', () => {
|
||||
let p: Participant;
|
||||
|
||||
it('setup', async () => {
|
||||
if (!expectations.moderation.setPasswordAvailable) {
|
||||
ctx.skipSuiteTests = 'setPasswordAvailable is not expected to be available';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
p = await joinMuc({
|
||||
name: 'p1',
|
||||
token: testsConfig.jwt.preconfiguredToken
|
||||
}, {
|
||||
configOverwrite: {
|
||||
roomPasswordNumberOfDigits: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('config value', async () => {
|
||||
expect(await p.execute(
|
||||
() => APP.store.getState()['features/base/config'].roomPasswordNumberOfDigits)).toBe(5);
|
||||
});
|
||||
it('set an invalid password', async () => {
|
||||
const securityDialog = p.getSecurityDialog();
|
||||
|
||||
await p.getToolbar().clickSecurityButton();
|
||||
await securityDialog.waitForDisplay();
|
||||
|
||||
expect(await securityDialog.isLocked()).toBe(false);
|
||||
|
||||
// Set a non-numeric password.
|
||||
await securityDialog.addPassword('AAAAA');
|
||||
|
||||
expect(await securityDialog.isLocked()).toBe(false);
|
||||
await securityDialog.clickCloseButton();
|
||||
});
|
||||
it('set a valid password', async () => {
|
||||
const securityDialog = p.getSecurityDialog();
|
||||
|
||||
await p.getToolbar().clickSecurityButton();
|
||||
await securityDialog.waitForDisplay();
|
||||
|
||||
await securityDialog.addPassword('12345');
|
||||
await securityDialog.clickCloseButton();
|
||||
|
||||
await p.getToolbar().clickSecurityButton();
|
||||
await securityDialog.waitForDisplay();
|
||||
|
||||
expect(await securityDialog.isLocked()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
const ONE_ON_ONE_CONFIG_OVERRIDES = {
|
||||
configOverwrite: {
|
||||
disable1On1Mode: false,
|
||||
@@ -14,7 +19,7 @@ const ONE_ON_ONE_CONFIG_OVERRIDES = {
|
||||
}
|
||||
};
|
||||
|
||||
describe('OneOnOne', () => {
|
||||
describe('One-on-one (1on1) mode', () => {
|
||||
it('filmstrip hidden in 1on1', async () => {
|
||||
await ensureTwoParticipants(ONE_ON_ONE_CONFIG_OVERRIDES);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant, joinFirstParticipant, joinSecondParticipant } from '../../helpers/participants';
|
||||
|
||||
describe('PreJoin', () => {
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Pre-join screen', () => {
|
||||
it('display name required', async () => {
|
||||
await joinFirstParticipant({
|
||||
configOverwrite: {
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Self view', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
import { setTestProperties } from '../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../helpers/TestsConfig';
|
||||
import { joinMuc } from '../helpers/joinMuc';
|
||||
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
|
||||
@@ -20,7 +20,7 @@ setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('TileView', () => {
|
||||
describe('Tile view', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
|
||||
before('join the meeting', async () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
describe('Video Layout', () => {
|
||||
describe('Video layout', () => {
|
||||
it('join participant', () => ensureOneParticipant());
|
||||
|
||||
it('check', async () => {
|
||||
@@ -9,7 +9,7 @@ import pretty from 'pretty';
|
||||
import { getTestProperties, loadTestFiles } from './helpers/TestProperties';
|
||||
import { config as testsConfig } from './helpers/TestsConfig';
|
||||
import WebhookProxy from './helpers/WebhookProxy';
|
||||
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
|
||||
import { getLogs, initLogger, logInfo, saveLogs } from './helpers/browserLogger';
|
||||
import { IContext } from './helpers/types';
|
||||
import { generateRoomName } from './helpers/utils';
|
||||
|
||||
@@ -162,6 +162,18 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
webdriver: 'info'
|
||||
},
|
||||
|
||||
// Can be used to debug chromedriver, depends on chromedriver and wdio-chromedriver-service
|
||||
// services: [
|
||||
// [ 'chromedriver', {
|
||||
// // Pass the --verbose flag to Chromedriver
|
||||
// args: [ '--verbose' ],
|
||||
// // Optionally, define a file to store the logs instead of stdout
|
||||
// logFileName: 'wdio-chromedriver.log',
|
||||
// // Optionally, define a directory for the log file
|
||||
// outputDir: 'test-results',
|
||||
// } ]
|
||||
// ],
|
||||
|
||||
// Set directory to store all logs into
|
||||
outputDir: TEST_RESULTS_DIR,
|
||||
|
||||
@@ -261,9 +273,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
globalAny.ctx.webhooksProxy.connect();
|
||||
}
|
||||
|
||||
if (testProperties.useWebhookProxy && !globalAny.ctx.webhooksProxy) {
|
||||
console.warn(`WebhookProxy is not available, skipping ${testName}`);
|
||||
globalAny.ctx.skipSuiteTests = 'WebhooksProxy is not required but not available';
|
||||
if (testProperties.requireWebhookProxy && !globalAny.ctx.webhooksProxy) {
|
||||
throw new Error('The test requires WebhookProxy, but it is not available.');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -359,6 +370,16 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.title} ===---`));
|
||||
|
||||
if (error) {
|
||||
|
||||
// skip all remaining tests in the suite
|
||||
ctx.skipSuiteTests = `Test "${test.title}" has failed.`;
|
||||
|
||||
// make sure all browsers are at the main app in iframe (if used), so we collect debug info
|
||||
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
|
||||
// @ts-ignore
|
||||
await ctx[instance]?.switchToIFrame();
|
||||
}));
|
||||
|
||||
const allProcessing: Promise<any>[] = [];
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
@@ -380,7 +401,14 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
'text/plain'))
|
||||
.catch(e => console.error('Failed grabbing debug logs', e)));
|
||||
|
||||
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
|
||||
allProcessing.push(
|
||||
bInstance.execute(() => window.APP?.debugLogs?.logs?.join('\n')).then(res => {
|
||||
if (res) {
|
||||
saveLogs(bInstance, res);
|
||||
}
|
||||
|
||||
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
|
||||
}));
|
||||
|
||||
allProcessing.push(bInstance.getPageSource().then(source => {
|
||||
AllureReporter.addAttachment(`html-source-${instance}`, pretty(source), 'text/plain');
|
||||
|
||||
@@ -24,15 +24,18 @@ const mergedConfig = merge(defaultConfig, {
|
||||
'specs/iframe/*.spec.ts', // FF does not support uploading files (uploadFile)
|
||||
|
||||
// FF does not support setting a file as mic input, no dominant speaker events
|
||||
'specs/3way/activeSpeaker.spec.ts',
|
||||
'specs/3way/startMuted.spec.ts', // bad audio levels
|
||||
'specs/4way/desktopSharing.spec.ts',
|
||||
'specs/4way/lastN.spec.ts',
|
||||
'specs/media/activeSpeaker.spec.ts',
|
||||
'specs/media/startMuted.spec.ts', // bad audio levels
|
||||
'specs/media/desktopSharing.spec.ts',
|
||||
'specs/media/lastN.spec.ts',
|
||||
|
||||
// fails randomly for failed downloading asset and page stays in incomplete state
|
||||
'specs/misc/urlNormalisation.spec.ts',
|
||||
|
||||
// when unmuting a participant, we see the presence in debug logs immediately,
|
||||
// but for 15 seconds it is not received/processed by the client
|
||||
// (also the menu disappears after clicking one of the moderation options, does not happen manually)
|
||||
'specs/3way/audioVideoModeration.spec.ts'
|
||||
'specs/media/audioVideoModeration.spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
p1: {
|
||||
|
||||
Reference in New Issue
Block a user