Compare commits

...

18 Commits

Author SHA1 Message Date
Mihaela Dumitru
c0fc232a0f feat(visitors): add showJoinMeetingDialog config option (#16540) 2025-11-17 16:05:41 -06:00
Hristo Terezov
0fbadd716a Revert "fix(large-video): Prevents unnecessary updates when container is hidden"
This reverts commit 6deb0a6385.
2025-11-11 08:39:53 -06:00
damencho
dbe927666e fix(tests): Give some time for recording events to be received. 2025-11-07 11:18:14 -06:00
damencho
d6f5f2fec0 fix: Disable urlNormalisation on FF. 2025-11-07 11:07:09 -06:00
Jaya Allamsetty
5c1b191dc4 chore(deps) Update lib-jitsi-meet branch. 2025-11-06 11:49:14 -05:00
damencho
45d4039b53 feat(prosody): Adds a module to cleanup room with just service components in it. 2025-11-04 16:47:36 -06:00
damencho
93871d4ec9 fix(persistent_lobby): Avoids calling destroy twice. 2025-11-04 16:47:28 -06:00
Jaya Allamsetty
c5acef3824 fix(video-layout) Possibly fixes auto-pinning of SS in a large call.
When a user joins a very large call with SS, sometime SS is not auto-pinned to stage. This may happen when lot of participant joins are processed at the same time and therefore the state for remoteScreenShares may not get updated in time. Added extra logging to help debug if this issue reproduces.
2025-11-04 13:46:10 -05:00
damencho
615fcad3a3 fix(lobby): Hide login button if authenticated(jwt is available). 2025-11-03 14:30:28 -06:00
Дамян Минков
2e201ede28 * fix(lobby): Updates metadata on destroy lobby room and let in participants on empty main.
* fix(lobby): Updates metadata on destroy lobby room.

* fix(visitors): Let people join lobby when main room is empty but with lobby.
2025-10-30 16:08:57 -05:00
bgrozev
e1c99d3220 feat: Filter transcription results. (#16606) 2025-10-30 15:40:09 -05:00
bgrozev
7a8bd1b348 feat(tests): Do not require WebhooksProxy for jaas dial-in test (#16595)
* Add a requireWebhooksProxy test property.

* test: Make the jaas dial-in test use but not require WH proxy.
2025-10-28 14:15:27 -05:00
Дамян Минков
8d1a2694a6 feat(metadata): Adds lobbyEnabled and visitorsEnabled to metadata. (#16583) 2025-10-28 12:16:45 -05:00
Kerinlin
f93509b460 fix(i18n): Fix Chinese language issues and hyphenated locale persistence
- Fix missing Chinese translations in main-zh-CN.json and main-zh-TW.json
- Fix language selection not persisting for hyphenated locales (zh-CN, zh-TW, es-US, fr-CA, pt-BR)
- Update normalizeCurrentLanguage to check exact match before normalization
2025-10-28 12:16:38 -05:00
damencho
307fd0d9e1 fix(tests): More tests fixes.
fix(tests): Fix hangup by avoiding not defined error.

ReferenceError: APP is not defined.

fix(tests): Fix reference to APP in debug logs retrieval

Avoids `ReferenceError: APP is not defined` the failure may happen after hangup where APP is not defined.

Minor test fixes (#16577)

* test: Increase password dialog timeout.
* test: Try to fix hangup().

feat(tests): Drop more aria selectors.

fix(tests): Send logs to browser to keep correct order.

feat(tests): Uses memory logs on failure and stops logging during conference.

feat(tests): Avoids using aria selector for breakout rooms.

It is reported to be slow at times.

fix(tests) Increase backToP2PDelay to 3 secs.
Setting it to 1 sec was causing p2p connections to be created when it was not needed.

feat(tests): Fail early and gather debug logs for iframe tests.
2025-10-28 12:16:06 -05:00
damencho
ad028e3361 fix(visitors): Fix room token verification.
When allowUnauthenticatedAccess is enabled we want to allow main prosody participants without verifying their token.
2025-10-16 11:29:05 -05:00
bgrozev
8df5a4a519 feat(tests): Various tests fixes, expectations.
feat(tests): Make sure dial-in user hangups.

Avoid closing browser and leaving it to the timeout of the connection.

feat(tests): Increase the randomness of the room name.

fix(tests): Wait dialog elements to be clickable.

To avoid trying to click too quickly while animations are still rendered. Avoids: Can't call elementClick on element with selector "//input[@name="lockKey"]" because element wasn't found

fix(tests): Visitors tests to avoid reordered webhook events.

fix(tests) Wait for p2p switch before checking for SS

fix(tests): Fix p2p enable flag in desktop sharing.

test: Fix jaas chat test again (typo). (#16548)

test: Do not assert message order (they can race). (#16544)

test: Add a test for joining a MUC without a conference request. (#16537)

test: Use timeout for expected codec changes. (#16539)

I suspect some intermittent test failures are caused by not waiting for
the codec change to complete. Might be exacerbated by
ensureThreeParticipants only waiting for 1 remote stream, which means
it the "ensureTwo(); ensureThree()" call may return before p2 sees p3.

fix: Fix dial-in test (wait until the dialog is closed). (#16538)

feat: Whitelist the disableFocus config option. (#16526)

Reorganize tests by feature, minor test updates (#16518)

* test: Move lockRoom under moderation/.

* ref: Cleanup lockRoom test.

* test: Move lockRoomDigitsOnly to ui/.

* test: Add a setPasswordAvailable expectation.

* ref: Move the lobby test to moderation/.

* test: Move tests to media/.

* test: Add a useTenant expectation.

* test: Move mute to media/.

* test: Move audioOnly to media/.

* test: Move startMuted to media/.

* test: Move codecSelection to media/.

* ref: Simplify, log the "actual" codec value.

* test: Move stopVideo to media/.

* test: Move videoLayout to ui/.

* test: Move chatPanel to ui/.

* test: Move switchVideo to media/pinning.spec.ts.

* test: Move audioVideoModeration to media/.

* test: Move displayName to ui/.

* test: Move preJoin to ui/.

* test: Move endConference to ui/.

* test: Move selfView to ui/.

* test: Move oneOnOne to ui/.

* test: Move tileView to ui/.

* test: Move singlePort and udp to misc/connectivity.spec.ts.

* test: Move avatars to misc/.

* test: Move polls to misc/.

* test: Move breakoutRooms to misc/.

* test: Move followMe to misc/.

* test: Move invite to dial/dialInUi.spec.ts.

* test: Move dialInAudio to dial/dialIn.spec.ts.

* test: Only log expectations in the main wdio process.

* test: Move fakeDialInAudio to dial/.

* test: Move subject to misc/.

* test: Check for subject set remotely.

* test: Remove references to "2way", "3way".

* test: Consolidate all dial-in tests in one file.

* test: Move dialIn to misc/.

* test: Adjust test titles.

* Remove waitForAudioFromDialInParticipant test.
2025-10-16 11:28:24 -05:00
damencho
b6110a5120 feat(token_verification): Pass through recorder and transcriber into meetings. 2025-10-14 15:53:05 -05:00
99 changed files with 1168 additions and 820 deletions

View File

@@ -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);
});

View File

@@ -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": "会议开始后将自动加入"

View File

@@ -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": "會議開始後您將自動加入!"

View File

@@ -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
View File

@@ -66,7 +66,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/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",

View File

@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/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",

View File

@@ -654,6 +654,7 @@ export interface IConfig {
video?: boolean;
};
queueService: string;
showJoinMeetingDialog?: boolean;
};
watchRTCConfigParams?: IWatchRTCConfiguration;
webhookProxyUrl?: string;

View File

@@ -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',

View File

@@ -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

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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));
});
}
}

View File

@@ -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();
}
/**

View File

@@ -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)
};
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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
});
}
}
);

View File

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

View File

@@ -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.

View File

@@ -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),

View File

@@ -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 ?? ''}`;

View File

@@ -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));
}
}
}

View File

@@ -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;

View File

@@ -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.');
}
}
});

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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 })

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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())
));
}

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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();
}
/**

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;
}
/**

View File

@@ -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');
}
/**

View File

@@ -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);
});
});

View File

@@ -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) : '';
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
});
});

View File

@@ -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);
}
});
});

View File

@@ -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);
});
});

View File

@@ -9,7 +9,7 @@ setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('iFrame API for Chat', () => {
describe('Chat', () => {
let p1: Participant, p2: Participant;
it('setup', async () => {

View File

@@ -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');
});
});
});

View File

@@ -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();
});
});

View File

@@ -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', () => {

View File

@@ -7,8 +7,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import { waitForMedia } from './util';
setTestProperties(__filename, {
requireWebhookProxy: true,
useJaas: true,
useWebhookProxy: true
});

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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' ]
});

View File

@@ -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'

View File

@@ -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' ]
});

View File

@@ -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);

View File

@@ -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;

View File

@@ -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' ]
});

View File

@@ -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);
});

View File

@@ -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 () => {

View File

@@ -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);
}

View File

@@ -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());
/**

View File

@@ -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()},\

View File

@@ -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`
}
);
}

View File

@@ -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);

View File

@@ -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: {

View File

@@ -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());

View File

@@ -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));

View File

@@ -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 */);
});

View File

@@ -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());

View File

@@ -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';

View File

@@ -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'
});

View 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();
}

View 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);
});
});

View File

@@ -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();

View File

@@ -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();

View 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);
}

View File

@@ -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);
}
});
}
});

View File

@@ -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();

View File

@@ -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();
}
/**

View File

@@ -1,6 +1,6 @@
import { ensureOneParticipant } from '../../helpers/participants';
describe('Chat Panel', () => {
describe('Chat panel', () => {
it('join participant', () => ensureOneParticipant());
it('start closed', async () => {

View File

@@ -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 () => {

View File

@@ -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 () => {

View 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);
});
});

View File

@@ -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);

View File

@@ -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: {

View File

@@ -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());

View File

@@ -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 () => {

View File

@@ -1,6 +1,6 @@
import { ensureOneParticipant } from '../../helpers/participants';
describe('Video Layout', () => {
describe('Video layout', () => {
it('join participant', () => ensureOneParticipant());
it('check', async () => {

View File

@@ -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');

View File

@@ -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: {