Compare commits

...

32 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
1b5b1d01b1 Update README.md 2025-02-03 15:20:32 +01:00
Johan Bernhardsson
0d109eaf00 fix(lang) update Swedish translation 2025-02-03 14:58:27 +01:00
Calinteodor
75c43fc8f2 feat(toolbox): Fixed background color prop custom buttons (#15529)
* feat(toolbox): fixed backgroundColor prop for custom menu buttons
2025-02-03 13:34:40 +02:00
damencho
532b5858c5 fix: Adds nil check in some util methods. 2025-01-31 17:55:35 -06:00
Дамян Минков
edc2328fd6 chore(deps) lib-jitsi-meet@latest (#15547)
* chore(deps) lib-jitsi-meet@latest

https://github.com/jitsi/lib-jitsi-meet/compare/v1907.0.0+0d3304b7...v1908.0.0+2a5d7fcc

* squash: Adds docs in config.js.
2025-01-31 14:48:58 -06:00
Saúl Ibarra Corretgé
a603a8df06 fix(web) fix properly applying polyfill 2025-01-31 10:19:09 -06:00
Saúl Ibarra Corretgé
703a4d0f34 feat(build) show webpack progress 2025-01-31 06:21:59 -06:00
Saúl Ibarra Corretgé
44c1624c61 fix(web) polyfill Promise.withResolvers
webpack + babel-loader + core-js should've take care of it, but they
somehow don't and I couldn't finesse the webpack config enough.

This is a stop-gap.

Fixes: https://github.com/jitsi/jitsi-meet/issues/15539
2025-01-31 06:21:59 -06:00
damencho
5b5e233734 feat(api): Adds a print about overwritten config via iframeAPI. 2025-01-30 16:54:07 -06:00
Jaya Allamsetty
5607e50b8e fix(config) Remove deprecated codec settings from config.js 2025-01-29 11:51:54 -05:00
damencho
39c942d585 fix(tests): Adds missing audio file. 2025-01-28 17:35:34 -06:00
damencho
1c04f974d8 fix(prosody): Fixes jibri util check to support occupant and jid. 2025-01-28 17:07:53 -06:00
Saúl Ibarra Corretgé
dc5142cb6b chore(deps) npm audit fix 2025-01-28 14:34:14 -06:00
Дамян Минков
d563913499 feat: Reads region from http headers and set it in presence. (#15531)
* feat: Reads region from http headers and set it in presence.

* chore(deps) lib-jitsi-meet@latest

https://github.com/jitsi/lib-jitsi-meet/compare/v1906.0.0+dfc23df4...v1907.0.0+0d3304b7
2025-01-28 09:54:47 -06:00
Jaya Allamsetty
4d0642d1a7 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1903.0.0+54592e60...v1906.0.0+dfc23df4
2025-01-27 15:23:21 -05:00
damencho
8d8e83cdfd fix(visitors): Destroy visitors room earlier on main=0.
Sometimes jicofo may leave before the disconnect iq reaches the visitor prosody, that will cause a reload for all visitors instead of a dialog for conference ended.
2025-01-27 09:41:36 -06:00
Saúl Ibarra Corretgé
6d0271338e fix(virtual-backgrounds) fix handling empty file list
The returned object is not an array but array-like. That is, it checks
truthy, while having a length of 0.
2025-01-27 12:14:11 +01:00
damencho
1401eb245e fix(lobby): Fixes wrong password going back to knocking.
Fixes the case when someone enters a wrong password to access the meeting and then clicks back to continue knocking and not showing the knocking state, while still in the Lobby room.
The problem was introduced in 721bb4e, on access denied we are being kicked out of lobby room and then knocking state should be cleared.
2025-01-25 10:43:55 -06:00
damencho
27eec7b19e fix(polls): Fixes send/receive polls and processing answers.
Fixes #15509.
2025-01-24 15:44:06 -06:00
damencho
b0d0b3ac11 fix(tests): Fixes AVModeration test hovering over more menu in thumbnail. 2025-01-24 15:04:09 -06:00
damencho
2c11b1b945 fix(tests): Fix dial-in check for pin, can be 8 digits. 2025-01-24 15:04:09 -06:00
damencho
2c92ea57f0 fix(tests): Simplifies await async. 2025-01-24 15:04:09 -06:00
damencho
09b696e95a fix(pre-join): Remove from dom when lobby is shown. 2025-01-24 15:04:09 -06:00
damencho
d67054a550 feat(tests): Adds lobby test.
feat(dialog): Adds a print when opening and hiding dialogs.
2025-01-24 15:04:09 -06:00
damencho
05374e292b fix(tests): Simplify suite names.
squash: fix lint.
2025-01-24 15:04:09 -06:00
damencho
f038099e61 fix(tests): Fixes breakout room wait for room update. 2025-01-24 15:04:09 -06:00
damencho
da5f2d00d4 fix(tests): Fixes element not interceptable in AV moderation tests. 2025-01-24 15:04:09 -06:00
damencho
c8a7877fdd feat(tests): Adds lastN test. 2025-01-24 15:04:09 -06:00
damencho
d8da660070 feat(tests): Adds kick test. 2025-01-24 15:04:09 -06:00
Saúl Ibarra Corretgé
4e033fff4b fix(virtual-background) fix image upload cancelling
Fixes: https://github.com/jitsi/jitsi-meet/issues/15520
2025-01-24 13:27:13 +01:00
Jaya Allamsetty
4861f95368 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1901.0.0+22daa357...v1903.0.0+54592e60
2025-01-23 16:48:00 -05:00
Saúl Ibarra Corretgé
b3ee8fe127 feat(rn) implement startSilent
Technically, on Android, the audio mode is configured but no audio is
played. Since the configured audio mode matches what we expect from a
calling app (what we support to coexist with) this is enough to not
create audio disruptions.
2025-01-23 14:45:22 +01:00
64 changed files with 1889 additions and 428 deletions

View File

@@ -17,11 +17,11 @@ STYLES_BUNDLE = css/all.bundle.css
STYLES_DESTINATION = css/all.css
STYLES_MAIN = css/main.scss
ifeq ($(OS),Windows_NT)
WEBPACK = .\node_modules\.bin\webpack
WEBPACK_DEV_SERVER = .\node_modules\.bin\webpack serve --mode development
WEBPACK = .\node_modules\.bin\webpack --progress
WEBPACK_DEV_SERVER = .\node_modules\.bin\webpack serve --mode development --progress
else
WEBPACK = ./node_modules/.bin/webpack
WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack serve --mode development
WEBPACK = ./node_modules/.bin/webpack --progress
WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack serve --mode development --progress
endif
all: compile deploy

View File

@@ -1,6 +1,6 @@
# <p align="center">Jitsi Meet</p>
Jitsi Meet is a set of Open Source projects which empower users to use and deploy
TEST. Jitsi Meet is a set of Open Source projects which empower users to use and deploy
video conferencing platforms with state-of-the-art video quality and features.
<hr />

View File

@@ -136,6 +136,11 @@ class AudioModeModule extends ReactContextBaseJavaModule {
*/
private String userSelectedDevice;
/**
* Whether or not audio is disabled.
*/
private boolean audioDisabled;
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the application.
@@ -236,6 +241,12 @@ class AudioModeModule extends ReactContextBaseJavaModule {
audioDeviceHandler.stop();
}
audioDeviceHandler = null;
if (audioDisabled) {
return;
}
if (useConnectionService()) {
audioDeviceHandler = new AudioDeviceHandlerConnectionService(audioManager);
} else {
@@ -278,6 +289,27 @@ class AudioModeModule extends ReactContextBaseJavaModule {
});
}
@ReactMethod
public void setDisabled(final boolean disabled, final Promise promise) {
if (audioDisabled == disabled) {
promise.resolve(null);
return;
}
JitsiMeetLogger.i(TAG + " audio disabled: " + disabled);
audioDisabled = disabled;
setAudioDeviceHandler();
if (disabled) {
mode = -1;
availableDevices.clear();
resetSelectedDevice();
}
promise.resolve(null);
}
/**
* Public method to set the current audio mode.
*
@@ -287,7 +319,12 @@ class AudioModeModule extends ReactContextBaseJavaModule {
*/
@ReactMethod
public void setMode(final int mode, final Promise promise) {
if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) {
if (audioDisabled) {
promise.resolve(null);
return;
}
if (mode < DEFAULT || mode > VIDEO_CALL) {
promise.reject("setMode", "Invalid audio mode " + mode);
return;
}

6
app.js
View File

@@ -1,4 +1,8 @@
/* application specific logic */
/* Jitsi Meet app main entrypoint. */
// Polyfill Promise.withReolvers.
// FIXME(saghul) webpack + core-js v3 should polyfill this.
import 'promise.withresolvers/auto';
// Re-export jQuery
// FIXME: Remove this requirement from torture tests.

View File

@@ -84,6 +84,9 @@ var config = {
// Allows the setting of a custom bandwidth value from the UI.
// assumeBandwidth: true,
// Enables use of getDisplayMedia in electron
// electronUseGetDisplayMedia: false,
// Enables the use of the codec selection API supported by the browsers .
// enableCodecSelectionAPI: false,
@@ -573,30 +576,6 @@ var config = {
// useKSVC: true
// },
//
// DEPRECATED! Use `codec specific settings` instead.
// // Provides a way to configure the maximum bitrates that will be enforced on the simulcast streams for
// // video tracks. The keys in the object represent the type of the stream (LD, SD or HD) and the values
// // are the max.bitrates to be set on that particular type of stream. The actual send may vary based on
// // the available bandwidth calculated by the browser, but it will be capped by the values specified here.
// // This is currently not implemented on app based clients on mobile.
// maxBitratesVideo: {
// H264: {
// low: 200000,
// standard: 500000,
// high: 1500000,
// },
// VP8 : {
// low: 200000,
// standard: 500000,
// high: 1500000,
// },
// VP9: {
// low: 100000,
// standard: 300000,
// high: 1200000,
// },
// },
//
// // The options can be used to override default thresholds of video thumbnail heights corresponding to
// // the video quality levels used in the application. At the time of this writing the allowed levels are:
// // 'low' - for the low quality level (180p at the time of this writing)
@@ -614,22 +593,6 @@ var config = {
//
// // Provides a way to set the codec preference on mobile devices, both on RN and mobile browser based endpoint
// mobileCodecPreferenceOrder: [ 'VP8', 'VP9', 'H264' ],
//
// // DEPRECATED! Use `codecPreferenceOrder/mobileCodecPreferenceOrder` instead.
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified
// // here will be removed from the list of codecs present in the SDP answer generated by the client. If the
// // same codec is specified for both the disabled and preferred option, the disable settings will prevail.
// // Note that 'VP8' cannot be disabled since it's a mandatory codec, the setting will be ignored in this case.
// disabledCodec: 'H264',
//
// // DEPRECATED! Use `codecPreferenceOrder/mobileCodecPreferenceOrder` instead.
// // Provides a way to set a preferred video codec for the JVB connection. If 'H264' is specified here,
// // simulcast will be automatically disabled since JVB doesn't support H264 simulcast yet. This will only
// // rearrange the the preference order of the codecs in the SDP answer generated by the browser only if the
// // preferred codec specified here is present. Please ensure that the JVB offers the specified codec for this
// // to take effect.
// preferredCodec: 'VP8',
//
// },
// Notification timeouts
@@ -1122,15 +1085,6 @@ var config = {
// { urls: 'stun:jitsi-meet.example.com:3478' },
{ urls: 'stun:meet-jit-si-turnrelay.jitsi.net:443' },
],
// DEPRECATED! Use `codecPreferenceOrder/mobileCodecPreferenceOrder` instead.
// Provides a way to set the video codec preference on the p2p connection. Acceptable
// codec values are 'VP8', 'VP9' and 'H264'.
// preferredCodec: 'H264',
// DEPRECATED! Use `codecPreferenceOrder/mobileCodecPreferenceOrder` instead.
// Provides a way to prevent a video codec from being negotiated on the p2p connection.
// disabledCodec: '',
},
analytics: {

View File

@@ -21,6 +21,7 @@
#import <WebRTC/WebRTC.h>
#import "JitsiAudioSession+Private.h"
#import "callkit/JMCallKitProxy.h"
// Audio mode
@@ -54,6 +55,7 @@ static NSString * const kDeviceTypeUnknown = @"UNKNOWN";
RTCAudioSessionConfiguration *audioCallConfig;
RTCAudioSessionConfiguration *videoCallConfig;
RTCAudioSessionConfiguration *earpieceConfig;
BOOL audioDisabled;
BOOL forceSpeaker;
BOOL forceEarpiece;
BOOL isSpeakerOn;
@@ -146,9 +148,36 @@ RCT_EXPORT_MODULE();
#pragma mark - Exported methods
RCT_EXPORT_METHOD(setDisabled:(BOOL)disabled
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
if (audioDisabled == disabled) {
resolve(nil);
return;
}
RCTLogInfo(@"[AudioMode] audio disabled: %d", disabled);
audioDisabled = disabled;
JMCallKitProxy.enabled = !disabled;
RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
if (disabled) {
[session removeDelegate:self];
} else {
[session addDelegate:self];
}
session.useManualAudio = disabled;
}
RCT_EXPORT_METHOD(setMode:(int)mode
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
if (audioDisabled) {
resolve(nil);
return;
}
RTCAudioSessionConfiguration *config = [self configForMode:mode];
NSError *error;
@@ -177,6 +206,11 @@ RCT_EXPORT_METHOD(setMode:(int)mode
RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
if (audioDisabled) {
resolve(nil);
return;
}
RCTLogInfo(@"[AudioMode] Selected device: %@", device);
RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
@@ -239,6 +273,10 @@ RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
}
RCT_EXPORT_METHOD(updateDeviceList) {
if (audioDisabled) {
return;
}
[self notifyDevicesChanged];
}

View File

@@ -24,11 +24,15 @@
}
+ (void)activateWithAudioSession:(AVAudioSession *)session {
[self.rtcAudioSession audioSessionDidActivate:session];
if (!self.rtcAudioSession.useManualAudio) {
[self.rtcAudioSession audioSessionDidActivate:session];
}
}
+ (void)deactivateWithAudioSession:(AVAudioSession *)session {
[self.rtcAudioSession audioSessionDidDeactivate:session];
if (!self.rtcAudioSession.useManualAudio) {
[self.rtcAudioSession audioSessionDidDeactivate:session];
}
}
@end

View File

@@ -1,5 +1,8 @@
{
"addPeople": {
"accessibilityLabel": {
"meetingLink": "Mötes länk: {{url}}"
},
"add": "Bjud in",
"addContacts": "Bjud in dina kontakter",
"contacts": "Kontakter",
@@ -39,6 +42,18 @@
"audioOnly": {
"audioOnly": "Enbart ljud"
},
"bandwidthSettings": {
"assumedBandwidthBps": "t.ex. 10000000 för 10 Mbps",
"assumedBandwidthBpsWarning": "Högre värden kan skapa nätverksproblem.",
"customValue": "anpassat värde",
"customValueEffect": "för att ställa in det faktiska bps-värdet",
"leaveEmpty": "lämna tomt",
"leaveEmptyEffect": "för att uppskattningar ska äga rum",
"possibleValues": "Möjliga värden",
"setAssumedBandwidthBps": "Uppskattat värde (bps)",
"title": "Bandbreddsinställningar",
"zeroEffect": "för att slå av video"
},
"breakoutRooms": {
"actions": {
"add": "Lägg till grupprum",
@@ -48,15 +63,22 @@
"leaveBreakoutRoom": "Lämna grupprum",
"more": "Mer",
"remove": "Ta bort",
"rename": "Döp om",
"renameBreakoutRoom": "Döp om grupprum",
"sendToBreakoutRoom": "Skicka deltagare till grupprum:"
},
"breakoutList": "Grupprums lista",
"buttonLabel": "Grupprum",
"defaultName": "Grupprum #{{index}}",
"hideParticipantList": "Dölj deltagarlista",
"mainRoom": "Huvudrum",
"notifications": {
"joined": "Gick med i \"{{name}}\"",
"joined": "Gick med i grupprum \"{{name}}\"",
"joinedMainRoom": "Gick med i huvudrummet",
"joinedTitle": "Grupprum"
}
},
"showParticipantList": "Visa deltagarlista",
"title": "Grupprum"
},
"calendarSync": {
"addMeetingURL": "Lägg till en möteslänk",
@@ -106,6 +128,7 @@
"privateNotice": "Privat meddelande till {{recipient}}",
"sendButton": "Skicka",
"smileysPanel": "Emoji panel",
"systemDisplayName": "",
"tabs": {
"chat": "Chatt",
"polls": "Omröstningar"
@@ -197,7 +220,9 @@
"joinInBrowser": "Delta på webben",
"launchMeetingLabel": "Hur vill du delta i detta möte?",
"launchWebButton": "Starta på webben",
"noDesktopApp": "",
"noMobileApp": "Har du inte appen?",
"or": "",
"termsAndConditions": "Genom att fortsätta godkänner du våra <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>villkor.</a>",
"title": "Startar ditt möte i {{app}}…",
"titleNew": "Startar ditt möte…",
@@ -239,9 +264,13 @@
"Share": "Dela",
"Submit": "Skicka",
"WaitForHostMsg": "Konferensen har inte börjat än. Autentisera konferensen om du är värd. Vänta annars på att värden startar konferensen.",
"WaitForHostNoAuthMsg": "Konferensen har ännu inte startat eftersom ingen värd har anlänt ännu. Vänligen vänta.",
"WaitingForHostButton": "Vänta på värd",
"WaitingForHostTitle": "Väntar på värden…",
"Yes": "Ja",
"accessibilityLabel": {
"Cancel": "Avbryt",
"Ok": "Ok",
"close": "Stäng",
"liveStreaming": "Livesändning",
"sharingTabs": "Delningsalternativ"
@@ -250,6 +279,8 @@
"addMeetingNote": "Mötesinformation",
"addOptionalNote": "Ytterligare information",
"allow": "Tillåt",
"allowToggleCameraDialog": "Tillåter du att {{initiatorName}} får växla kamera till ansiktsläge?",
"allowToggleCameraTitle": "Tillåt att växla kamera",
"alreadySharedVideoMsg": "En annan deltagare delar redan en video. Konferensen tillåter bara en video-delning åt gången.",
"alreadySharedVideoTitle": "Endast en delad video åt gången tillåts",
"applicationWindow": "Applikationsfönster",
@@ -276,6 +307,8 @@
"contactSupport": "Kontakta kundtjänst",
"copied": "Kopierad",
"copy": "Kopiera",
"demoteParticipantDialog": "Are you sure you want to move this participant to viewer? Är du säker på att du vill flytta denna deltagaren till tittare",
"demoteParticipantTitle": "Flytta till tittare",
"dismiss": "Förkasta",
"displayNameRequired": "Hej, vad heter du?",
"done": "Klar",
@@ -287,6 +320,7 @@
"embedMeeting": "Bädda in möte",
"enterDisplayName": "Ange namn",
"error": "Fel",
"errorRoomCreationRestriction": "Du försökte gå med för snabbt, kom tillbaka om en stund.",
"gracefulShutdown": "Vår tjänst är för tillfället nedstängd för underhåll. Vänligen försök senare.",
"grantModeratorDialog": "Är du säker du vill göra denna deltagare till en moderator?",
"grantModeratorTitle": "Godkänn moderator",
@@ -300,6 +334,7 @@
"kickParticipantButton": "Sparka ut",
"kickParticipantDialog": "Vill du sparka ut den här deltagaren?",
"kickParticipantTitle": "Tysta deltagaren?",
"kickSystemTitle": "Aj! du blev utsparkad ur mötet",
"kickTitle": "Aj! {{participantDisplayName}} sparkade ut dig ur mötet",
"linkMeeting": "Länka möte",
"linkMeetingTitle": "Länka möte till Salesforce",
@@ -310,7 +345,8 @@
"lockRoom": "Lägg till möte $t(lockRoomPasswordUppercase)",
"lockTitle": "Låsning misslyckades",
"login": "Logga in",
"logoutQuestion": "Är du säker på att du vill logga ut och stoppa konferensen?",
"loginQuestion": "Är du säker på att du vill logga in och lämna mötet",
"logoutQuestion": "Är du säker på att du vill logga ut och lämna konferensen?",
"logoutTitle": "Logga ut",
"maxUsersLimitReached": "Gränsen för maximalt antal deltagare har nåtts. Konferensen är full. Kontakta mötesägaren eller försök igen senare!",
"maxUsersLimitReachedTitle": "Maximal deltagarantal uppnått",
@@ -353,8 +389,6 @@
"permissionCameraRequiredError": "Tillåtelse krävs för att delta med kamera i denna möte. Var god skaffa detta i \"inställningar\".",
"permissionErrorTitle": "Tillåtelse krävs",
"permissionMicRequiredError": "Tillåtelse krävs för att delta med mikrofon i denna möte. Var god skaffa detta i \"inställningar\".",
"popupError": "Din webbläsare blockerar pop-up-fönster från sajten. Tillåt pop-up-fönster från den här sajten i inställningarna och försök igen.",
"popupErrorTitle": "Pop-up blockerad",
"readMore": "Mer",
"recentlyUsedObjects": "Dina senaste använda objekt",
"recording": "Inspelning",
@@ -371,6 +405,8 @@
"removePassword": "Ta bort $t(lockRoomPassword)",
"removeSharedVideoMsg": "Är du säker på att du vill ta bort din delade video?",
"removeSharedVideoTitle": "Ta bort den delade videon",
"renameBreakoutRoomLabel": "Rums namn",
"renameBreakoutRoomTitle": "Döp om grupprum",
"reservationError": "Fel i reservationssystemet",
"reservationErrorMsg": "Felkod: {{code}}, meddelande: {{msg}}",
"retry": "Försök igen",
@@ -390,8 +426,10 @@
"sendPrivateMessageTitle": "Skicka privat?",
"serviceUnavailable": "Tjänsten otillgänglig",
"sessTerminated": "Konferensen avslutades",
"sessTerminatedReason": "",
"sessionRestarted": "Samtal återstartat av bryggan",
"shareAudio": "Fortsätt",
"shareAudioAltText": "För att dela önskat innehåll, navigera till \"Webläsarfliken\", välj innehållet, aktivera \"dela ljud\"-kryssmärket och klicka sedan på \"dela\" knappen",
"shareAudioTitle": "Hur man delar ljud",
"shareAudioWarningD1": "Du måste avsluta din skärmdelning innan du kan dela ditt ljud",
"shareAudioWarningD2": "Du måste starta om din skärmdelning och därefter klicka på \"ljuddelning\"",
@@ -402,7 +440,10 @@
"shareScreenWarningD2": "du måste avsluta ljuddelning, starta skärmdelning och sen aktivera \"ljuddelning\"",
"shareScreenWarningH1": "Om du bara vill dela din skärm:",
"shareScreenWarningTitle": "Du måste avsluta ljuddelning innan du kan dela in skärm",
"shareVideoConfirmPlay": "Du är på väg att öppna en extern webbplats. Vill du fortsätta?",
"shareVideoConfirmPlayTitle": "{{name}} har delat en video med dig.",
"shareVideoLinkError": "Skriv in en fungerande länk.",
"shareVideoLinkStopped": "Videon från {{name}} har avslutats.",
"shareVideoTitle": "Ta bort en delad video",
"shareYourScreen": "Dela din skärm",
"shareYourScreenDisabled": "Skärmdelning har inaktiverats.",
@@ -421,7 +462,25 @@
"thankYou": "Tack för att du använder {{appName}}!",
"token": "token",
"tokenAuthFailed": "Du är inte behörig att delta i det här samtalet.",
"tokenAuthFailedReason": {
"audInvalid": "Ogiltigt `aud`-värde. Det ska vara `jitsi`.",
"contextNotFound": "Objektet `context` saknas från payload.",
"expInvalid": "Ogilting `exp` värde.",
"featureInvalid": "Ogiltig feature: {{feature}}, mest troligt inte implementerat än.",
"featureValueInvalid": "Felaktigt värde för feature: {{feature}}.",
"featuresNotFound": "`Features` objektet saknas i payload.",
"headerNotFound": "Saknar header.",
"issInvalid": "Ogiltigt `iss`-värde. Det ska vara `chat`.",
"kidMismatch": "Nyckel ID (kid) matchat inte sub.",
"kidNotFound": "Nyckel ID saknas (kid).",
"nbfFuture": "Värdet i `nbf` är i framtiden.",
"nbfInvalid": "Ogilitigt `nbf`värde.",
"payloadNotFound": "Payload saknas.",
"tokenExpired": "Token har gått ut."
},
"tokenAuthFailedTitle": "Autentisering misslyckades",
"tokenAuthFailedWithReasons": "Förlåt, du har inte tillåtelse att gå med i det här samtalet. Troliga anledingar: {{reason}}",
"tokenAuthUnsupported": "Token URL är inte tillåten",
"transcribing": "Transkriberar",
"unlockRoom": "Ta bort möte $t(lockRoomPassword)",
"user": "Användare",
@@ -435,6 +494,10 @@
"viewUpgradeOptions": "Se uppgraderings alternativ",
"viewUpgradeOptionsContent": "För att få obegränsad tillgång till premiumfunktioner som inspelning, transkriptioner, RTMP-streaming och mer måste du uppgradera din plan.",
"viewUpgradeOptionsTitle": "Du upptäckte en premiumfunktion!",
"whiteboardLimitContent": "Förlåt, max antal samtidiga deltagare i whiteboard har uppnåts.",
"whiteboardLimitReference": "För mer information besök",
"whiteboardLimitReferenceUrl": "vår webplats",
"whiteboardLimitTitle": "Användande av whiteboard har begränsats",
"yourEntireScreen": "Helskärm"
},
"documentSharing": {
@@ -447,6 +510,9 @@
"title": "Bädda in möte"
},
"feedback": {
"accessibilityLabel": {
"yourChoice": "Ditt val: {{rating}}"
},
"average": "Medel",
"bad": "Dåligt",
"detailsLabel": "Berätta mer.",
@@ -503,13 +569,16 @@
"noNumbers": "Inga inringningsnummer.",
"noPassword": "Inget lösenord",
"noRoom": "Inget rum specificerades för inringning.",
"noWhiteboard": "Kan inte ladda whiteboard.",
"numbers": "Inringningsnummer",
"password": "$t(lockRoomPasswordUppercase):",
"reachedLimit": "Du har nått gränsen för din prenumeration.",
"sip": "SIP adress",
"sipAudioOnly": "SIP endast ljud address",
"title": "Dela",
"tooltip": "Dela länk och information om inringning för mötet",
"upgradeOptions": "Vänligen kontrollera om uppgraderingsalternativen är på"
"upgradeOptions": "Vänligen kontrollera om uppgraderingsalternativen är på",
"whiteboardError": "Problem att ladda whiteboard, var god försök senare."
},
"inlineDialogFailure": {
"msg": "Vi slirade lite.",
@@ -542,8 +611,7 @@
"toggleParticipantsPane": "Visa eller dölj deltagarfönstret",
"toggleScreensharing": "Växla mellan kamera och skärmdelning",
"toggleShortcuts": "Visa eller dölj kortkommandon",
"videoMute": "Aktivera / inaktivera din kamera",
"whiteboard": "Visa / dölj whiteboardtavlan"
"videoMute": "Aktivera / inaktivera din kamera"
},
"largeVideo": {
"screenIsShared": "Du delar din skärm",
@@ -574,6 +642,7 @@
"on": "Strömma",
"onBy": "{{name}} startade direktströmningen",
"pending": "Börja strömma…",
"policyError": "",
"serviceName": "Livesändningstjänst",
"sessionAlreadyActive": "Den här sessionen spelas redan in eller livestreamas.",
"signIn": "Logga in med Google",
@@ -612,13 +681,13 @@
"knockingParticipantList": "Väntande deltagare",
"lobbyChatStartedNotification": "{{moderator}} startade en lobbychatt med {{attendee}}",
"lobbyChatStartedTitle": "{{moderator}} har startat en lobbychatt med dig.",
"lobbyClosed": "Lobbyn har stängts.",
"nameField": "Ange ditt namn",
"notificationLobbyAccessDenied": "{{targetParticipantName}} har nekats att delta av {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} har godkänts att delta av {{originParticipantName}}",
"notificationLobbyDisabled": "Väntrumsläge har inaktiverats av {{originParticipantName}}",
"notificationLobbyEnabled": "Väntrumsläge har aktiverats av {{originParticipantName}}",
"notificationTitle": "Väntrum",
"passwordField": "Ange möteslösenord",
"passwordJoinButton": "Anslut",
"title": "Lobby",
"toggleLabel": "Aktivera väntrum"
@@ -651,6 +720,8 @@
"sessionToken": "Sessionstoken",
"start": "Starta inspelning",
"stop": "Avsluta inspelning",
"stopping": "Avslutar inspelning",
"wait": "Var god vänta medans vi sparar din inspelning",
"yes": "Ja"
},
"lockRoomPassword": "lösenord",
@@ -670,9 +741,13 @@
"connectedOneMember": "{{name}} har gått med i mötet",
"connectedThreePlusMembers": "{{name}} och {{count}} andra har gått med i mötet",
"connectedTwoMembers": "{{first}} och {{second}} har gått med i mötet",
"connectionFailed": "Anslutning misslyckades. Var god försök senare!",
"dataChannelClosed": "Försämrad videokvalitet",
"dataChannelClosedDescription": "Bryggkanalen har kopplats bort och därmed är videokvaliteten begränsad till sin lägsta inställning",
"dataChannelClosedDescriptionWithAudio": "Bryggkanalen har kopplats bort och därmed kan abvrott i ljud och bild uppstå.",
"dataChannelClosedWithAudio": "Ljud och bild kvalite kan vara försämrad.",
"disabledIframe": "Inbäddning är endast avsedd för demonstrationsändamål, så det här samtalet kommer att kopplas ner om {{timeout}} minuter.",
"disabledIframeSecondary": "Bädda in {{domain}} är bara till för demo, så detta samtal kommer att kopplas bort inom {{timeout}} minuter. Var god använd <a href='{{jaasDomain}}' rel='nooper noreferrer' target='_blank'>Jitsi som tjänst</a> för att bädda in i produktion.",
"disconnected": "frånkopplad",
"displayNotifications": "Visa aviseringar för",
"dontRemindMe": "Påminn mig inte",
@@ -681,6 +756,9 @@
"gifsMenu": "GIPHY",
"groupTitle": "Notifieringar",
"hostAskedUnmute": "Värden vill att du ska stänga av ljudet",
"invalidTenant": "Ogiltig tenant",
"invalidTenantHyphenDescription": "Tenant du använder har ogiltiga tecken (startar eller slutar med '-').",
"invalidTenantLengthDescription": "Tenant du använder är för lång",
"invitedOneMember": "{{name}} har bjudits in",
"invitedThreePlusMembers": "{{name}} och {{count}} andra har bjudits in",
"invitedTwoMembers": "{{first}} och {{second}} har bjudits in",
@@ -717,9 +795,9 @@
"newDeviceAction": "Använd",
"newDeviceAudioTitle": "Ny ljudenhet hittad",
"newDeviceCameraTitle": "Ny kamera hittad",
"nextToSpeak": "Du är näst i kö för att prata",
"noiseSuppressionDesktopAudioDescription": "Brusreducering kan inte aktiveras när du delar skrivbordsljud, vänligen inaktivera det och försök igen.",
"noiseSuppressionFailedTitle": "Det gick inte att starta brusreducering",
"noiseSuppressionNoTrackDescription": "Slå på mikrofonen först.",
"noiseSuppressionStereoDescription": "Brusreducering i stereoljud stöds för närvarande inte.",
"oldElectronClientDescription1": "Den version av Jitsi meet som används är gammal och har säkerhetsluckor. Var god uppdatera till den senaste versionen.",
"oldElectronClientDescription2": "senast build",
@@ -743,13 +821,22 @@
"startSilentTitle": "Du gick med utan ljud aktiverat!",
"suboptimalBrowserWarning": "Din mötesupplevelse kommer tyvärr inte att bli så bra. Vi letar efter sätt att förbättra detta, men fram till dess kan du försöka använda en av de <a href='{{recommendedBrowserPageLink}}' target='_blank'> fullt stödda webbläsarna </a>.",
"suboptimalExperienceTitle": "Webbläsarvarning",
"suggestRecordingAction": "Starta",
"suggestRecordingDescription": "Vill du starta en inspelning?",
"suggestRecordingTitle": "Spela in detta mötet",
"unmute": "Slå på mikrofonen",
"videoMutedRemotelyDescription": "Du kan alltid slå på den igen.",
"videoMutedRemotelyTitle": "Din kamera har inaktiverats av {{participantDisplayName}}!",
"videoUnmuteBlockedDescription": "Aktivering av kameran och delning av skrivbord har tillfälligt blockerats på grund av systembegränsningar.",
"videoUnmuteBlockedTitle": "Aktivering av kameran och skrivbordsdelning blockerad!",
"viewLobby": "Visa lobby",
"waitingParticipants": "{{waitingParticipants}} personer"
"viewParticipants": "Visa deltagare",
"viewVisitors": "Visa besökare",
"waitingParticipants": "{{waitingParticipants}} personer",
"waitingVisitors": "Väntande besökare i kö: {{waitingVisitors}}",
"waitingVisitorsTitle": "Mötet är inte live än!",
"whiteboardLimitDescription": "Var god spara, max antal användare har snart uppnåtts och whiteboard kommer att stängas av.",
"whiteboardLimitTitle": "Användande whiteboard"
},
"participantsPane": {
"actions": {
@@ -760,7 +847,11 @@
"askUnmute": "Be om att aktivera ljud",
"audioModeration": "Slå på ljudet för sig själva",
"blockEveryoneMicCamera": "Inaktivera allas mikrofon och kamera",
"breakoutRooms": "Grupprum",
"goLive": "Gå live",
"invite": "Bjud in någon",
"lowerAllHands": "Ta ner allas händer",
"lowerHand": "Ta ner handen",
"moreModerationActions": "Fler modereringsalternativ",
"moreModerationControls": "Fler modereringskontroller",
"moreParticipantOptions": "Fler deltagaralternativ",
@@ -777,6 +868,8 @@
"headings": {
"lobby": "Väntrum ({{count}})",
"participantsList": "Mötesdeltagare ({{count}})",
"visitorInQueue": "(väntande {{count}})",
"visitorRequests": "(förfrågningar {{count}})",
"visitors": "Gäster ({{count}})",
"waitingLobby": "Väntar i väntrum ({{count}})"
},
@@ -789,10 +882,13 @@
"pinnedParticipant": "Deltagaren är fäst",
"polls": {
"answer": {
"edit": "Redigera",
"send": "Skicka",
"skip": "Avstå",
"submit": "Skicka"
},
"by": "Av {{ name }}",
"closeButton": "Stäng omröstning",
"create": {
"addOption": "Lägg till alternativ",
"answerPlaceholder": "Alternativ",
@@ -802,6 +898,7 @@
"pollQuestion": "Fråga för omröstning",
"questionPlaceholder": "Ställ en fråga",
"removeOption": "Ta bort alternativ",
"save": "Spara",
"send": "Skicka"
},
"errors": {
@@ -831,9 +928,11 @@
"configuringDevices": "Konfigurerar enheter…",
"connectedWithAudioQ": "Är din mikrofon ansluten?",
"connection": {
"failed": "Anslutningstest misslyckades!",
"good": "Din internetanslutning fungerar bra!",
"nonOptimal": "Din internetanslutning är inte optimal",
"poor": "Din internetanslutning fungerar dåligt"
"poor": "Din internetanslutning fungerar dåligt",
"running": "Kör anslutningstest..."
},
"connectionDetails": {
"audioClipping": "Ditt ljud kan upplevas hackigt.",
@@ -842,6 +941,7 @@
"goodQuality": "Grymt bra! Din mediekvalitet kommer att bli bra.",
"noMediaConnectivity": "Vi kunde inte hitta ett sätt att upprätta mediaanslutning för detta test. Detta orsakas vanligtvis av en brandvägg eller NAT.",
"noVideo": "Vi förväntar oss att din video kommer ha låg kvalitet eller inte fungera.",
"testFailed": "Anslutningstestet stötte på oväntade problem, men det behöver inte påverka din upplevelse.",
"undetectable": "Om du fortfarande inte kan ringa i webbläsaren rekommenderar vi att du ser till att dina högtalare, mikrofon och kamera är korrekt inställda, att du har beviljat din webbläsare rättigheter att använda din mikrofon och kamera och att din webbläsarversion är uppdaterad.",
"veryPoorConnection": "Vi förväntar oss att din samtalskvalitet är riktigt dålig.",
"videoFreezing": "Vi förväntar oss att din video fryser, blir svart och pixeleras.",
@@ -874,6 +974,7 @@
"or": "eller",
"premeeting": "Förmöte",
"proceedAnyway": "Fortsätt ändå",
"recordingWarning": "",
"screenSharingError": "Skärmdelningsfel:",
"showScreen": "Aktivera skärmen före mötet",
"startWithPhone": "Börja med telefonljud",
@@ -940,7 +1041,6 @@
"limitNotificationDescriptionNative": "På grund av stor efterfrågan är din inspelning begränsad till {{limit}} min. För obegränsade inspelningar, försök <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "På grund av stor efterfrågan är din inspelning begränsad till {{limit}} min. För obegränsade inspelningar, prova <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "Vi har genererat en länk till din inspelning.",
"live": "LIVE",
"localRecordingNoNotificationWarning": "Inspelningen kommer inte att meddelas till andra deltagare. Du måste meddela dem att mötet inspelas.",
"localRecordingNoVideo": "Video spelas inte in",
"localRecordingStartWarning": "Se till att du stoppar inspelningen innan du avslutar mötet för att spara det.",
@@ -957,13 +1057,16 @@
"onBy": "{{name}} påbörjade inspelningen",
"onlyRecordSelf": "Spela bara in mina ljud- och videoströmmar",
"pending": "Förbereder inspelning av mötet…",
"rec": "REC",
"policyError": "Du försökte starta inspelningen för fort. Var god försök senare!",
"recordAudioAndVideo": "Spela in ljud och bild",
"recordTranscription": "Spela in transkribiering",
"saveLocalRecording": "Spara inspelningsfil lokalt (beta)",
"serviceDescription": "Din inspelning kommer att sparas av inspelningstjänsten",
"serviceDescriptionCloud": "Molninspelning",
"serviceDescriptionCloudInfo": "Inspelade möten rensas automatiskt 24 timmar efter inspelningstiden.",
"serviceName": "Inspelningstjänst",
"sessionAlreadyActive": "Den här sessionen spelas redan in eller livestreamas.",
"showAdvancedOptions": "Visa avancerade inställningar",
"signIn": "Logga in",
"signOut": "Logga ut",
"surfaceError": "Välj aktuell flik.",
@@ -1003,6 +1106,7 @@
"desktopShareWarning": "Du måste starta om skärmdelningen för att de nya inställningarna ska träda i kraft.",
"devices": "Enheter",
"followMe": "Alla följer mig",
"followMeRecorder": "Den som spelar in följer mig",
"framesPerSecond": "bildrutor per sekund",
"incomingMessage": "Inkommande meddelande",
"language": "Språk",
@@ -1040,6 +1144,7 @@
"alertOk": "OK",
"alertTitle": "Varning",
"alertURLText": "Den angivna serverlänken är felaktig",
"apply": "Tillämpa",
"buildInfoSection": "Versionsinformation",
"conferenceSection": "Konferens",
"disableCallIntegration": "Tillåt inte deltagande via telefon",
@@ -1050,12 +1155,14 @@
"displayNamePlaceholderText": "Exempel: John Doe",
"email": "E-post",
"emailPlaceholderText": "mejl@exempel.se",
"gavatarMessage": "Om din e-post är associerad med ett Gravatar konto, så kommer vi använda det för att visa din profilbild.",
"goTo": "Gå till",
"header": "Inställningar",
"help": "Hjälp",
"links": "Länkar",
"privacy": "Sekretess",
"profileSection": "Profil",
"sdkVersion": "",
"serverURL": "Serverlänk",
"showAdvanced": "Visa avancerade inställningar",
"startCarModeInLowBandwidthMode": "Starta billäget i lågbandbreddsläge",
@@ -1076,6 +1183,7 @@
"fearful": "Rädd",
"happy": "Glad",
"hours": "{{count}} h",
"labelTooltip": "",
"minutes": "{{count}} m",
"name": "Namn",
"neutral": "Neutral",
@@ -1108,7 +1216,7 @@
"audioOnly": "Slå av eller på ljudet",
"audioRoute": "Välj ljudenhet",
"boo": "Bua",
"breakoutRoom": "Anslut eller lämna grupprum",
"breakoutRooms": "Grupprum",
"callQuality": "Hantera videokvalitet",
"carmode": "Billäge",
"cc": "Slå av eller på undertexter",
@@ -1146,6 +1254,7 @@
"lobbyButton": "Aktivera / inaktivera lobbyläge",
"localRecording": "Växla lokala inspelningskontroller",
"lockRoom": "Växla möteslösenord",
"love": "Kärlek",
"lowerHand": "Sänk din hand",
"moreActions": "Fler åtgärder",
"moreActionsMenu": "Menyn Fler åtgärder",
@@ -1155,6 +1264,7 @@
"muteEveryoneElse": "Stäng av ljudet för alla andra",
"muteEveryoneElsesVideoStream": "Stoppa alla andras video",
"muteEveryonesVideoStream": "Stoppa allas video",
"muteGUMPending": "Ansluter din mikrofon",
"noiseSuppression": "Brusdämpning",
"openChat": "Öppna chatt",
"participants": "Öppna deltagarfönstret",
@@ -1162,6 +1272,7 @@
"privateMessage": "Skicka privat meddelande",
"profile": "Redigera din profil",
"raiseHand": "Räck upp handen",
"react": "Reaktioner på meddelanden",
"reactions": "Reaktioner",
"reactionsMenu": "Reaktionsmeny",
"recording": "Växla inspelning",
@@ -1188,6 +1299,7 @@
"unmute": "Slå på ljudet",
"videoblur": "Växla videooskärpa",
"videomute": "Stoppa kamera",
"videomuteGUMPending": "Ansluter din kamera",
"videounmute": "Starta kameran"
},
"addPeople": "Lägg till personer i samtal",
@@ -1232,12 +1344,14 @@
"lobbyButtonEnable": "Aktivera väntrumsläge",
"login": "Logga in",
"logout": "Logga ut",
"love": "Kärlek",
"lowerYourHand": "Ta ner handen",
"moreActions": "Fler handlingar",
"moreOptions": "Visa fler alternativ",
"mute": "Slå av/på ljud",
"muteEveryone": "Tysta alla",
"muteEveryonesVideo": "Inaktivera allas kamera",
"muteGUMPending": "Ansluter din mikrofon",
"noAudioSignalDesc": "Om du inte avsiktligt stängde av det från systeminställningar eller hårdvara, överväg att byta enhet.",
"noAudioSignalDescSuggestion": "Om du inte avsiktligt tystade det från systeminställningar eller hårdvara, överväg att byta till den föreslagna enheten.",
"noAudioSignalDialInDesc": "Du kan också ringa in med:",
@@ -1254,8 +1368,9 @@
"profile": "Redigera din profil",
"raiseHand": "Räck upp / ta ner din hand",
"raiseYourHand": "Räck upp handen",
"reactionBoo": "Bua",
"reactionClap": "Klappa",
"reactionBoo": "Skicka Bu",
"reactionClap": "Klappa i händerna",
"reactionHeart": "Skicka kärlek",
"reactionLaugh": "Skratta",
"reactionLike": "Skicka tummen upp",
"reactionSilence": "Skicka tyst reaktion",
@@ -1283,16 +1398,14 @@
"unmute": "Slå på ljud",
"videoSettings": "Videoinställningar",
"videomute": "Inaktivera kameran",
"videomuteGUMPending": "Ansluter din kamera",
"videounmute": "Aktivera kameran"
},
"transcribing": {
"ccButtonTooltip": "Aktivera / Inaktivera undertexter",
"error": "Transkriberingen misslyckades. Försök igen.",
"expandedLabel": "Transkribering är aktiverad",
"failedToStart": "Det gick inte att starta transkribering",
"failed": "Transkribiering misslyckades",
"labelToolTip": "Mötet transkriberas",
"off": "Transkribering avslutades",
"pending": "Förbereder transkribering av mötet…",
"sourceLanguageDesc": "För närvarande är mötesspråket inställt på <b>{{sourceLanguage}}</b>. <br/> Du kan ändra det från ",
"sourceLanguageHere": "här",
"start": "Börja visa undertexter",
@@ -1339,6 +1452,7 @@
},
"videothumbnail": {
"connectionInfo": "Anslutningsinformation",
"demote": "Gör till besökare",
"domute": "Tysta",
"domuteOthers": "Inaktivera ljud för alla andra",
"domuteVideo": "Inaktivera kamera",
@@ -1362,6 +1476,10 @@
"videomute": "Deltagaren har stängt av kameran"
},
"virtualBackground": {
"accessibilityLabel": {
"currentBackground": "Nuvarande bakgrund: {{background}}",
"selectBackground": "Välj bakgrund"
},
"addBackground": "Lägg till bakgrund",
"apply": "Tillämpa",
"backgroundEffectError": "Det gick inte att tillämpa bakgrundseffekt.",
@@ -1387,11 +1505,22 @@
},
"visitors": {
"chatIndicator": "(besökare)",
"joinMeeting": {
"description": "Du är just nu en beskökare i konferensen",
"raiseHand": "Räck upp handen",
"title": "Gå med i mötet",
"wishToSpeak": "Om du vill prata, räck upp handen och vänta på att en moderator ska godkänna."
},
"labelTooltip": "Antal besökare: {{count}}",
"notification": {
"description": "Räck upp handen för att delta",
"demoteDescription": "Skickad hit av {{actor}}, räck upphanden för att delta",
"noMainParticipantsDescription": "En deltagare behöver starta mötet. Var god försök igen senare.",
"noMainParticipantsTitle": "Det är mötet har inte startat än",
"noVisitorLobby": "Du kan inte gå med medans lobby är påslagen för mötet.",
"notAllowedPromotion": "En deltagare behöver godkänna din förfrågan först.",
"title": "Du är en besökare i mötet"
}
},
"waitingMessage": "Du kommer att komma in i mötet när det är live!"
},
"volumeSlider": "Volymreglage",
"welcomepage": {
@@ -1449,6 +1578,7 @@
"whiteboard": {
"accessibilityLabel": {
"heading": "Whiteboard"
}
},
"screenTitle": "Whiteboard"
}
}

View File

@@ -851,6 +851,8 @@ function initCommands() {
'overwrite-config': config => {
const whitelistedConfig = getWhitelistedJSON('config', config);
logger.info(`Overwriting config with: ${JSON.stringify(whitelistedConfig)}`);
APP.store.dispatch(overwriteConfig(whitelistedConfig));
},
'toggle-virtual-background': () => {

71
package-lock.json generated
View File

@@ -62,7 +62,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1901.0.0+22daa357/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -13324,10 +13324,11 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -13348,7 +13349,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -13363,6 +13364,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/array-flatten": {
@@ -16954,8 +16959,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1901.0.0+22daa357/lib-jitsi-meet.tgz",
"integrity": "sha512-GWx9NSfUYoxfM9J+Le0w9Nu2+K0nAITqT9puNwqrSXwncE5PKf1uS/DZlMSK4yBKJ+EDvP+YBhDOa6wtxcXODw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"integrity": "sha512-POz5n9QppExZNX4ZguKoQOOLL0Gev2RL49ZtwiWs7QBHTat4gzBA9kYAeviRrmW4kVn7xcX0xeAxInbHDWs4lg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -18788,15 +18793,16 @@
}
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -19772,10 +19778,11 @@
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"dev": true
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -24383,9 +24390,9 @@
}
},
"node_modules/undici": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz",
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==",
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -35147,9 +35154,9 @@
}
},
"express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"requires": {
"accepts": "~1.3.8",
@@ -35171,7 +35178,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -37739,8 +37746,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1901.0.0+22daa357/lib-jitsi-meet.tgz",
"integrity": "sha512-GWx9NSfUYoxfM9J+Le0w9Nu2+K0nAITqT9puNwqrSXwncE5PKf1uS/DZlMSK4yBKJ+EDvP+YBhDOa6wtxcXODw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"integrity": "sha512-POz5n9QppExZNX4ZguKoQOOLL0Gev2RL49ZtwiWs7QBHTat4gzBA9kYAeviRrmW4kVn7xcX0xeAxInbHDWs4lg==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
@@ -39114,9 +39121,9 @@
}
},
"nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="
},
"natural-compare": {
"version": "1.4.0",
@@ -39781,9 +39788,9 @@
}
},
"path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true
},
"path-type": {
@@ -42980,9 +42987,9 @@
}
},
"undici": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz",
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==",
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"dev": true
},
"undici-types": {

View File

@@ -68,7 +68,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1901.0.0+22daa357/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -86,12 +86,16 @@ export function createDesiredLocalTracks(...desiredTypes: any) {
dispatch(destroyLocalDesktopTrackIfExists());
if (desiredTypes.length === 0) {
const { startSilent } = state['features/base/config'];
const { video } = state['features/base/media'];
// XXX: Always create the audio track early, even if it will be muted.
// This fixes a timing issue when adding the track to the conference which
// manifests primarily on iOS 15.
desiredTypes.push(MEDIA_TYPE.AUDIO);
if (!startSilent) {
// Always create the audio track early, even if it will be muted.
// This fixes a timing issue when adding the track to the conference which
// manifests primarily on iOS 15.
// Unless we are silent, of course.
desiredTypes.push(MEDIA_TYPE.AUDIO);
}
// XXX When the app is coming into the foreground from the
// background in order to handle a URL, it may realize the new

View File

@@ -118,8 +118,8 @@ interface IProps extends AbstractProps, WithTranslation {
* @param {IProps} props - The props object.
* @returns {boolean} - True if the prejoin screen should be displayed and false otherwise.
*/
function shouldShowPrejoin({ _showPrejoin, _showVisitorsQueue }: IProps) {
return _showPrejoin && !_showVisitorsQueue;
function shouldShowPrejoin({ _showLobby, _showPrejoin, _showVisitorsQueue }: IProps) {
return _showPrejoin && !_showVisitorsQueue && !_showLobby;
}
/**

View File

@@ -58,12 +58,14 @@ ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE,
...state,
isDisplayNameRequiredError: true
};
} else if (action.error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
return {
...state,
knocking: false
};
}
return {
...state,
knocking: false
};
return state;
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:

View File

@@ -10,9 +10,11 @@ import {
CONFERENCE_LEFT
} from '../../base/conference/actionTypes';
import { getCurrentConference } from '../../base/conference/functions';
import { SET_CONFIG } from '../../base/config/actionTypes';
import { AUDIO_FOCUS_DISABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import { parseURIString } from '../../base/util/uri';
import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
import logger from './logger';
@@ -44,7 +46,7 @@ MiddlewareRegistry.register(store => next => action => {
}
case APP_WILL_MOUNT:
_appWillMount(store);
case CONFERENCE_FAILED: // eslint-disable-line no-fallthrough
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
/*
@@ -60,6 +62,23 @@ MiddlewareRegistry.register(store => next => action => {
case SET_AUDIO_ONLY:
return _updateAudioMode(store, next, action);
case SET_CONFIG: {
const { locationURL } = store.getState()['features/base/connection'];
const location = parseURIString(locationURL?.href ?? '');
/**
* Don't touch the current value if there is no room in the URL. This
* avoids audio cutting off for a moment right after the user leaves
* a meeting. The next meeting join will set it to the right value.
*/
if (location.room) {
const { startSilent } = action.config;
AudioMode.setDisabled?.(Boolean(startSilent));
}
break;
}
}
/* eslint-enable no-fallthrough */

View File

@@ -130,7 +130,8 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
const tmp = {
id: pollId,
answers,
question
question,
senderId
};
// Check integrity of the poll data.
@@ -151,7 +152,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
name: answer,
voters: []
};
}).slice(MAX_ANSWERS),
}).slice(0, MAX_ANSWERS),
saved: false,
editing: false
};
@@ -172,7 +173,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
const receivedAnswer: IAnswer = {
voterId,
pollId,
answers: answers.slice(MAX_ANSWERS)
answers: answers.slice(0, MAX_ANSWERS)
};
dispatch(receiveAnswer(pollId, receivedAnswer));

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { Image } from 'react-native';
import { Image, View, ViewStyle } from 'react-native';
import { SvgCssUri } from 'react-native-svg';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import styles from './styles';
@@ -13,6 +14,7 @@ interface IProps extends AbstractButtonProps {
backgroundColor?: string;
icon: any;
id?: string;
isToolboxButton?: boolean;
text?: string;
}
@@ -40,20 +42,30 @@ class CustomOptionButton extends AbstractButton<IProps> {
}
if (this.iconSrc?.includes('svg')) {
iconComponent
= (
<SvgCssUri
style = { styles.iconImageStyles }
uri = { this.iconSrc } />);
iconComponent = (
<SvgCssUri
height = { BaseTheme.spacing[4] }
uri = { this.iconSrc }
width = { BaseTheme.spacing[4] } />
);
} else {
iconComponent
= (
<Image
source = {{ uri: this.iconSrc }}
style = { styles.iconImageStyles } />);
iconComponent = (
<Image
height = { BaseTheme.spacing[4] }
resizeMode = { 'contain' }
source = {{ uri: this.iconSrc }}
width = { BaseTheme.spacing[4] } />
);
}
return iconComponent;
return (
<View
style = { this.props.isToolboxButton && [
styles.toolboxButtonIconContainer,
{ backgroundColor: this.backgroundColor } ] as ViewStyle }>
{ iconComponent }
</View>
);
};
label = this.text || '';

View File

@@ -237,6 +237,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
dispatch(customButtonPressed(id, text))
}
icon = { icon }
isToolboxButton = { false }
key = { id }
text = { text } />
))

View File

@@ -126,6 +126,7 @@ function Toolbox(props: IProps) {
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(id, text)) }
icon = { icon }
isToolboxButton = { true }
key = { id } />
))
}
@@ -145,7 +146,12 @@ function Toolbox(props: IProps) {
style = { style as ViewStyle }>
{
_customToolbarButtons
? renderCustomToolboxButtons()
? <>
{ renderCustomToolboxButtons() }
{ !_iAmVisitor && <OverflowMenuButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } /> }
</>
: <>
{!_iAmVisitor && <AudioMuteButton
styles = { buttonStylesBorderless }

View File

@@ -101,13 +101,16 @@ const styles = {
maxWidth: 580,
marginHorizontal: 'auto',
marginVertical: BaseTheme.spacing[0],
paddingHorizontal: BaseTheme.spacing[1],
paddingHorizontal: BaseTheme.spacing[2],
width: '100%'
},
iconImageStyles: {
height: BaseTheme.spacing[4],
width: BaseTheme.spacing[4]
toolboxButtonIconContainer: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
justifyContent: 'center',
width: BaseTheme.spacing[7]
}
};

View File

@@ -89,9 +89,14 @@ function UploadImageButton({
const uploadImage = useCallback(async e => {
const reader = new FileReader();
const imageFile = e.target.files;
if (imageFile.length === 0) {
return;
}
const reader = new FileReader();
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const url = await resizeImage(reader.result);

View File

@@ -211,6 +211,20 @@ local function cancel_destroy_timer(room)
end
end
local function destroy_with_conference_ended(room)
-- if the room is being destroyed, ignore
if room.destroying then
return;
end
cancel_destroy_timer(room);
local main_count, visitors_count = get_occupant_counts(room);
module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
room:destroy(nil, 'Conference ended.');
return true;
end
-- schedules a new destroy timer which will destroy the room if there are no visitors after the timeout
local function schedule_destroy_timer(room)
cancel_destroy_timer(room);
@@ -271,6 +285,10 @@ module:hook('muc-occupant-left', function (event)
if visitors_count == 0 then
schedule_destroy_timer(room);
end
if main_count == 0 then
destroy_with_conference_ended(room);
end
end);
-- forward visitor presences to jicofo
@@ -407,7 +425,6 @@ local function stanza_handler(event)
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
if not room then
module:log('warn', 'No room found %s in stanza_handler', room_jid);
return;
end
@@ -619,12 +636,7 @@ local function iq_from_main_handler(event)
respond_iq_result(origin, stanza);
if process_disconnect then
cancel_destroy_timer(room);
local main_count, visitors_count = get_occupant_counts(room);
module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
room:destroy(nil, 'Conference ended.');
return true;
return destroy_with_conference_ended(room);
end
-- if there is password supplied use it

View File

@@ -3,6 +3,7 @@
module:set_global();
local formdecode = require "util.http".formdecode;
local region_header_name = module:get_option_string('region_header_name', 'x_proxy_region');
-- Extract the following parameters from the URL and set them in the session:
-- * previd: for session resumption
@@ -24,6 +25,8 @@ function init_session(event)
session.jitsi_web_query_room = params.room;
session.jitsi_web_query_prefix = params.prefix or "";
end
session.user_region = request.headers[region_header_name];
end
module:hook_global("bosh-session", init_session, 1);

View File

@@ -75,17 +75,35 @@ module:hook('muc-broadcast-presence', function (event)
end
end);
--- Avoids any participant joining the room in the interval between creating the room
--- and jicofo entering the room
module:hook('muc-occupant-pre-join', function (event)
local room, stanza = event.room, event.stanza;
-- we skip processing only if jicofo_lock is set to false
if room._data.jicofo_lock == false or is_healthcheck_room(room.jid) then
local function process_region(session, stanza)
if not session.user_region then
return;
end
local region = stanza:get_child_text('jitsi_participant_region');
if region then
return;
end
stanza:tag('jitsi_participant_region'):text(session.user_region):up();
end
--- Avoids any participant joining the room in the interval between creating the room
--- and jicofo entering the room
module:hook('muc-occupant-pre-join', function (event)
local occupant, room, stanza = event.occupant, event.room, event.stanza;
local is_health_room = is_healthcheck_room(room.jid);
-- check for region
if not is_admin(occupant.bare_jid) and not is_health_room then
process_region(event.origin, stanza);
end
-- we skip processing only if jicofo_lock is set to false
if room._data.jicofo_lock == false or is_health_room then
return;
end
local occupant = event.occupant;
if ends_with(occupant.nick, '/focus') then
module:fire_event('jicofo-unlock-room', { room = room; });
else

View File

@@ -474,12 +474,20 @@ end
-- Returns the initiator extension if the stanza is coming from a sip jigasi
function is_sip_jigasi(stanza)
if not stanza then
return false;
end
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
end
function is_transcriber_jigasi(stanza)
if not stanza then
return false;
end
local features = stanza:get_child('features');
if not features then
if not features then
return false;
end
@@ -532,7 +540,7 @@ function is_sip_jibri_join(stanza)
end
function is_jibri(occupant)
return starts_with_one_of(occupant.jid, RECORDER_PREFIXES)
return starts_with_one_of(type(occupant) == "string" and occupant or occupant.jid, RECORDER_PREFIXES)
end
-- process a host module directly if loaded or hooks to wait for its load

View File

@@ -10,8 +10,11 @@ import ChatPanel from '../pageobjects/ChatPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import InviteDialog from '../pageobjects/InviteDialog';
import LobbyScreen from '../pageobjects/LobbyScreen';
import Notifications from '../pageobjects/Notifications';
import ParticipantsPane from '../pageobjects/ParticipantsPane';
import PreJoinScreen from '../pageobjects/PreJoinScreen';
import SecurityDialog from '../pageobjects/SecurityDialog';
import SettingsDialog from '../pageobjects/SettingsDialog';
import Toolbar from '../pageobjects/Toolbar';
import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
@@ -19,6 +22,11 @@ import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IContext, IJoinOptions } from './types';
export const P1_DISPLAY_NAME = 'p1';
export const P2_DISPLAY_NAME = 'p2';
export const P3_DISPLAY_NAME = 'p3';
export const P4_DISPLAY_NAME = 'p4';
/**
* Participant.
*/
@@ -29,6 +37,7 @@ export class Participant {
* @private
*/
private _name: string;
private _displayName: string;
private _endpointId: string;
private _jwt?: string;
@@ -103,6 +112,13 @@ export class Participant {
return this._name;
}
/**
* The name.
*/
get displayName() {
return this._displayName || this.name;
}
/**
* Adds a log to the participants log file.
*
@@ -135,7 +151,7 @@ export class Participant {
if (!options.skipDisplayName) {
// @ts-ignore
config.userInfo = {
displayName: options.displayName || this._name
displayName: this._displayName = options.displayName || this._name
};
}
@@ -173,7 +189,9 @@ export class Participant {
await this.driver.switchFrame(mainFrame);
}
await this.waitToJoinMUC();
if (!options.skipWaitToJoin) {
await this.waitToJoinMUC();
}
await this.postLoadProcess(options.skipInMeetingChecks);
}
@@ -234,7 +252,7 @@ export class Participant {
*/
async waitForPageToLoad(): Promise<void> {
return this.driver.waitUntil(
async () => await this.driver.execute(() => document.readyState === 'complete'),
() => this.driver.execute(() => document.readyState === 'complete'),
{
timeout: 30_000, // 30 seconds
timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
@@ -307,8 +325,8 @@ export class Participant {
async waitForIceConnected(): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
await driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
return driver.waitUntil(() =>
driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
});
@@ -323,17 +341,39 @@ export class Participant {
timeout = 15_000, msg = `expected to receive/send data in 15s for ${this.name}`): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
await driver.execute(() => {
const stats = APP.conference.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
uploadBitrate: bitrateMap.upload || 0,
downloadBitrate: bitrateMap.download || 0
};
return driver.waitUntil(() => driver.execute(() => {
const stats = APP.conference.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
uploadBitrate: bitrateMap.upload || 0,
downloadBitrate: bitrateMap.download || 0
};
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
}), {
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
}), {
timeout,
timeoutMsg: msg
});
}
/**
* Waits for send and receive data.
*
* @returns {Promise<void>}
*/
async waitForSendData(
timeout = 15_000, msg = `expected to send data in 15s for ${this.name}`): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() => driver.execute(() => {
const stats = APP.conference.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
uploadBitrate: bitrateMap.upload || 0
};
return rtpStats.uploadBitrate > 0;
}), {
timeout,
timeoutMsg: msg
});
@@ -348,10 +388,10 @@ export class Participant {
waitForRemoteStreams(number: number): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
return driver.waitUntil(() =>
driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
timeout: 15_000,
timeoutMsg: `expected remote streams in 15s for ${this.name}`
timeoutMsg: `expected number of remote streams:${number} in 15s for ${this.name}`
});
}
@@ -365,8 +405,7 @@ export class Participant {
waitForParticipants(number: number, msg?: string): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
await driver.execute(count => APP.conference.listMembers().length === count, number), {
return driver.waitUntil(() => driver.execute(count => APP.conference.listMembers().length === count, number), {
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
});
@@ -440,6 +479,15 @@ export class Participant {
return new VideoQualityDialog(this);
}
/**
* Returns the security Dialog.
*
* @returns {SecurityDialog}
*/
getSecurityDialog(): SecurityDialog {
return new SecurityDialog(this);
}
/**
* Returns the settings Dialog.
*
@@ -449,6 +497,20 @@ export class Participant {
return new SettingsDialog(this);
}
/**
* Returns the prejoin screen.
*/
getPreJoinScreen(): PreJoinScreen {
return new PreJoinScreen(this);
}
/**
* Returns the lobby screen.
*/
getLobbyScreen(): LobbyScreen {
return new LobbyScreen(this);
}
/**
* Switches to the iframe API context
*/
@@ -476,6 +538,26 @@ 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();
// already hangup
if (current.endsWith('/base.html')) {
return;
}
// do a hangup, to make sure unavailable presence is sent
await this.driver.execute(() => typeof APP !== 'undefined' && 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
await this.driver.waitUntil(
async () => current !== await this.driver.getUrl(),
{
timeout: 5000,
timeoutMsg: `${this.name} did not leave the muc in 5s`
}
);
await this.driver.url('/base.html');
}
@@ -495,7 +577,7 @@ export class Participant {
* Returns the local display name.
*/
async getLocalDisplayName() {
return await (await this.getLocalDisplayNameElement()).getText();
return (await this.getLocalDisplayNameElement()).getText();
}
/**
@@ -546,7 +628,7 @@ export class Participant {
* Returns the source of the large video currently shown.
*/
async getLargeVideoId() {
return await this.driver.execute('return document.getElementById("largeVideo").srcObject.id');
return this.driver.execute('return document.getElementById("largeVideo").srcObject.id');
}
/**
@@ -610,4 +692,37 @@ export class Participant {
expect(await displayNameEl.isDisplayed()).toBe(true);
expect(await displayNameEl.getText()).toBe(value);
}
/**
* Checks if the leave reason dialog is open.
*/
async isLeaveReasonDialogOpen() {
return this.driver.$('div[data-testid="dialog.leaveReason"]').isDisplayed();
}
/**
* Waits for remote video state - receiving and displayed.
* @param endpointId
*/
async waitForRemoteVideo(endpointId: string) {
await this.driver.waitUntil(async () =>
await this.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
});
}
/**
* Waits for ninja icon to be displayed.
* @param endpointId
*/
async waitForNinjaIcon(endpointId: string) {
await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000,
timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
});
}
}

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import process from 'node:process';
import { v4 as uuidv4 } from 'uuid';
import { Participant } from './Participant';
import { P1_DISPLAY_NAME, P2_DISPLAY_NAME, P3_DISPLAY_NAME, P4_DISPLAY_NAME, Participant } from './Participant';
import { IContext, IJoinOptions } from './types';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
@@ -34,13 +34,13 @@ export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptio
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
displayName: P2_DISPLAY_NAME,
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: 'p3',
displayName: P3_DISPLAY_NAME,
...options
})
]);
@@ -68,19 +68,19 @@ export async function ensureFourParticipants(ctx: IContext, options: IJoinOption
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
displayName: P2_DISPLAY_NAME,
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: 'p3',
displayName: P3_DISPLAY_NAME,
...options
}),
_joinParticipant('participant4', ctx.p4, p => {
ctx.p4 = p;
}, {
displayName: 'p4',
displayName: P4_DISPLAY_NAME,
...options
})
]);
@@ -102,7 +102,7 @@ export async function ensureFourParticipants(ctx: IContext, options: IJoinOption
* @returns {Promise<void>}
*/
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
const p1DisplayName = 'p1';
const p1DisplayName = P1_DISPLAY_NAME;
let token;
// if it is jaas create the first one to be moderator and second not moderator
@@ -135,7 +135,7 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: 'p2',
displayName: P2_DISPLAY_NAME,
...options
}),
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),

View File

@@ -45,4 +45,10 @@ export type IJoinOptions = {
* Whether to skip in meeting checks like ice connected and send receive data. For single in meeting participant.
*/
skipInMeetingChecks?: boolean;
/**
* Whether to skip waiting for the participant to join the room. Cases like lobby where we do not succeed to join
* based on the logic of the test.
*/
skipWaitToJoin?: boolean;
};

View File

@@ -12,29 +12,29 @@ export default class AVModerationMenu extends BasePageObject {
/**
* Clicks the start audio moderation menu item.
*/
async clickStartAudioModeration() {
await this.clickButton(START_AUDIO_MODERATION);
clickStartAudioModeration() {
return this.clickButton(START_AUDIO_MODERATION);
}
/**
* Clicks the stop audio moderation menu item.
*/
async clickStopAudioModeration() {
await this.clickButton(STOP_AUDIO_MODERATION);
clickStopAudioModeration() {
return this.clickButton(STOP_AUDIO_MODERATION);
}
/**
* Clicks the start video moderation menu item.
*/
async clickStartVideoModeration() {
await this.clickButton(START_VIDEO_MODERATION);
clickStartVideoModeration() {
return this.clickButton(START_VIDEO_MODERATION);
}
/**
* Clicks the stop audio moderation menu item.
*/
async clickStopVideoModeration() {
await this.clickButton(STOP_VIDEO_MODERATION);
clickStopVideoModeration() {
return this.clickButton(STOP_VIDEO_MODERATION);
}
/**

View File

@@ -10,14 +10,14 @@ export default class BaseDialog extends BasePageObject {
/**
* Clicks on the X (close) button.
*/
async clickCloseButton(): Promise<void> {
await this.participant.driver.$(`#${CLOSE_BUTTON}`).click();
clickCloseButton(): Promise<void> {
return this.participant.driver.$(`#${CLOSE_BUTTON}`).click();
}
/**
* Clicks on the ok button.
*/
async clickOkButton(): Promise<void> {
await this.participant.driver.$(`#${OK_BUTTON}`).click();
clickOkButton(): Promise<void> {
return this.participant.driver.$(`#${OK_BUTTON}`).click();
}
}

View File

@@ -50,11 +50,11 @@ class BreakoutRoom extends BasePageObject {
/**
* Collapses the breakout room.
*/
async collapse() {
collapse() {
const collapseElem = this.participant.driver.$(
`div[data-testid="${this.id}"]`);
await collapseElem.click();
return collapseElem.click();
}
/**
@@ -209,21 +209,4 @@ export default class BreakoutRooms extends BasePageObject {
await sendButton.waitForClickable();
await sendButton.click();
}
// /**
// * Open context menu for given participant.
// */
// async openParticipantContextMenu(participant: Participant) {
// const listItem = this.participant.driver.$(
// `div[@id="participant-item-${await participant.getEndpointId()}"]`);
//
// await listItem.waitForDisplayed();
// await listItem.moveTo();
//
// const button = listItem.$(`aria/${PARTICIPANT_MORE_LABEL}`);
//
// await button.waitForClickable();
// await button.click();
// }
}

View File

@@ -7,8 +7,8 @@ export default class ChatPanel extends BasePageObject {
/**
* Is chat panel open.
*/
async isOpen() {
return await this.participant.driver.$('#sideToolbarContainer').isExisting();
isOpen() {
return this.participant.driver.$('#sideToolbarContainer').isExisting();
}
/**

View File

@@ -70,8 +70,8 @@ export default class Filmstrip extends BasePageObject {
/**
* Returns the local video id.
*/
async getLocalVideoId() {
return await this.participant.driver.execute(
getLocalVideoId() {
return this.participant.driver.execute(
'return document.getElementById("localVideo_container").srcObject.id');
}
@@ -94,7 +94,7 @@ export default class Filmstrip extends BasePageObject {
const elem = this.participant.driver.$(
`//span[@id='participant_${endpointId}']//img[contains(@class,'userAvatar')]`);
return await elem.isExisting() ? elem.getAttribute('src') : null;
return await elem.isExisting() ? await elem.getAttribute('src') : null;
}
/**
@@ -113,10 +113,11 @@ export default class Filmstrip extends BasePageObject {
* @private
*/
private async clickOnRemoteMenuLink(participantId: string, linkClassname: string, dialogConfirm: boolean) {
const thumbnail = this.participant.driver.$(
`//span[@id='participant_${participantId}']//span[@id='remotevideomenu']`);
await this.participant.driver.$(`//span[@id='participant_${participantId}']`).moveTo();
await thumbnail.moveTo();
await this.participant.driver.$(
`//span[@id='participant_${participantId
}']//span[@id='remotevideomenu']//div[@id='remote-video-menu-trigger']`).moveTo();
const popoverElement = this.participant.driver.$(
`//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
@@ -154,8 +155,8 @@ export default class Filmstrip extends BasePageObject {
* Kicks a participant.
* @param participantId
*/
async kickParticipant(participantId: string) {
await this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
kickParticipant(participantId: string) {
return this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
}
/**
@@ -177,8 +178,8 @@ export default class Filmstrip extends BasePageObject {
/**
* Checks whether the local self view is displayed or not.
*/
async assertSelfViewIsHidden(hidden: boolean) {
await this.participant.driver.$(LOCAL_VIDEO_XPATH).waitForDisplayed({
assertSelfViewIsHidden(hidden: boolean) {
return this.participant.driver.$(LOCAL_VIDEO_XPATH).waitForDisplayed({
reverse: hidden,
timeout: 5000,
timeoutMsg: `Local video thumbnail is${hidden ? '' : ' not'} displayed for ${this.participant.name}`
@@ -200,8 +201,8 @@ export default class Filmstrip extends BasePageObject {
* Asserts that the remote videos are hidden or not.
* @param reverse
*/
async assertRemoteVideosHidden(reverse = false) {
await this.participant.driver.waitUntil(
assertRemoteVideosHidden(reverse = false) {
return this.participant.driver.waitUntil(
async () =>
await this.participant.driver.$$('//div[@id="remoteVideos" and contains(@class, "hidden")]').length > 0,
{
@@ -210,4 +211,12 @@ export default class Filmstrip extends BasePageObject {
}
);
}
/**
* Counts the displayed remote video thumbnails.
*/
async countVisibleThumbnails() {
return (await this.participant.driver.$$('//div[@id="remoteVideos"]//span[contains(@class,"videocontainer")]')
.filter(thumbnail => thumbnail.isDisplayed())).length;
}
}

View File

@@ -10,8 +10,8 @@ export default class IframeAPI extends BasePageObject {
* Returns the json object from the iframeAPI helper.
* @param event
*/
async getEventResult(event: string): Promise<any> {
return await this.participant.driver.execute(
getEventResult(event: string): Promise<any> {
return this.participant.driver.execute(
eventName => {
const result = window.jitsiAPI.test[eventName];
@@ -27,8 +27,8 @@ export default class IframeAPI extends BasePageObject {
* Adds an event listener to the iframeAPI.
* @param eventName The event name.
*/
async addEventListener(eventName: string) {
return await this.participant.driver.execute(
addEventListener(eventName: string) {
return this.participant.driver.execute(
(event, prefix) => {
console.log(`${new Date().toISOString()} ${prefix} Adding listener for event: ${event}`);
window.jitsiAPI.addListener(event, evt => {
@@ -42,15 +42,15 @@ export default class IframeAPI extends BasePageObject {
/**
* Returns an array of available rooms and details of it.
*/
async getRoomsInfo() {
return await this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo());
getRoomsInfo() {
return this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo());
}
/**
* Returns the number of participants in the conference.
*/
async getNumberOfParticipants() {
return await this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants());
getNumberOfParticipants() {
return this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants());
}
/**
@@ -58,8 +58,8 @@ export default class IframeAPI extends BasePageObject {
* @param command The command.
* @param args The arguments.
*/
async executeCommand(command: string, ...args: any[]) {
return await this.participant.driver.execute(
executeCommand(command: string, ...args: any[]) {
return this.participant.driver.execute(
(commandName, commandArgs) =>
window.jitsiAPI.executeCommand(commandName, ...commandArgs)
, command, args);
@@ -68,15 +68,14 @@ export default class IframeAPI extends BasePageObject {
/**
* Returns the current state of the participant's pane.
*/
async isParticipantsPaneOpen() {
return await this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
isParticipantsPaneOpen() {
return this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
}
/**
* Removes the embedded Jitsi Meet conference.
*/
async dispose() {
return await this.participant.driver.execute(() => window.jitsiAPI.dispose());
dispose() {
return this.participant.driver.execute(() => window.jitsiAPI.dispose());
}
}

View File

@@ -13,7 +13,7 @@ export default class InviteDialog extends BaseDialog {
/**
* Checks if the dialog is open.
*/
async isOpen() {
isOpen() {
return this.participant.driver.$(`.${DIALOG_CONTAINER}`).isExisting();
}
@@ -85,8 +85,8 @@ export default class InviteDialog extends BaseDialog {
/**
* Gets the string that contains the dial in number for the current conference.
*/
async getDialInNumber() {
return await this.getValueAfterColon(PHONE_NUMBER);
getDialInNumber() {
return this.getValueAfterColon(PHONE_NUMBER);
}
/**

View File

@@ -0,0 +1,30 @@
import PreMeetingScreen from './PreMeetingScreen';
const DISPLAY_NAME_TEST_ID = 'lobby.nameField';
const JOIN_BUTTON_TEST_ID = 'lobby.knockButton';
/**
* Page object for the Lobby screen.
*/
export default class LobbyScreen extends PreMeetingScreen {
/**
* Returns the join button element.
*/
getJoinButton(): ChainablePromiseElement {
return this.participant.driver.$(`[data-testid="${JOIN_BUTTON_TEST_ID}"]`);
}
/**
* Returns the display name input element.
*/
getDisplayNameInput(): ChainablePromiseElement {
return this.participant.driver.$(`[data-testid="${DISPLAY_NAME_TEST_ID}"]`);
}
/**
* Waits for lobby screen to load.
*/
waitForLoading(): Promise<void> {
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 4000 });
}
}

View File

@@ -4,6 +4,15 @@ const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
const LOBBY_ACCESS_DENIED_TEST_ID = 'lobby.joinRejectedMessage';
const LOBBY_ENABLED_TEST_ID = 'lobby.notificationLobbyEnabled';
const LOBBY_KNOCKING_PARTICIPANT_NOTIFICATION_XPATH
= '//div[@data-testid="notify.participantWantsToJoin"]/div/div/span';
const LOBBY_NOTIFICATIONS_TITLE_TEST_ID = 'lobby.notificationTitle';
const LOBBY_PARTICIPANT_ACCESS_DENIED_TEST_ID = 'lobby.notificationLobbyAccessDenied';
const LOBBY_PARTICIPANT_ACCESS_GRANTED_TEST_ID = 'lobby.notificationLobbyAccessGranted';
const LOBBY_PARTICIPANT_ADMIT_TEST_ID = 'participantsPane.actions.admit';
const LOBBY_PARTICIPANT_REJECT_TEST_ID = 'participantsPane.actions.reject';
const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
const REENABLE_SELF_VIEW_NOTIFICATION_ID = 'notify.selfViewTitle';
const REENABLE_SELF_VIEW_CLOSE_NOTIFICATION = 'notify.selfViewTitle-dismiss';
@@ -38,10 +47,10 @@ export default class Notifications extends BasePageObject {
/**
* Dismisses any join notifications.
*/
async dismissAnyJoinNotification() {
await Promise.allSettled(
dismissAnyJoinNotification() {
return Promise.allSettled(
[ `${JOIN_ONE_TEST_ID}-dismiss`, `${JOIN_TWO_TEST_ID}-dismiss`, `${JOIN_MULTIPLE_TEST_ID}-dismiss` ]
.map(async id => this.participant.driver.$(`#${id}"]`).click()));
.map(id => this.participant.driver.$(`#${id}"]`).click()));
}
/**
@@ -59,7 +68,155 @@ export default class Notifications extends BasePageObject {
/**
* Closes the self view notification.
*/
async closeReEnableSelfViewNotification() {
await this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_CLOSE_NOTIFICATION}"]`).click();
closeReEnableSelfViewNotification() {
return this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_CLOSE_NOTIFICATION}"]`).click();
}
/**
* The notification on participants page when Lobby is being enabled or disabled.
*/
getLobbyEnabledText() {
return this.getNotificationText(LOBBY_ENABLED_TEST_ID);
}
/**
* Closes a specific lobby notification.
* @param testId
* @private
*/
private async closeLobbyNotification(testId: string) {
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
await notification.waitForExist();
await notification.waitForStable();
const closeButton
= this.participant.driver.$(`[data-testid="${LOBBY_NOTIFICATIONS_TITLE_TEST_ID}"]`)
.$('#close-notification');
await closeButton.moveTo();
await closeButton.click();
}
/**
* Closes the notification.
*/
closeLobbyEnabled() {
return this.closeLobbyNotification(LOBBY_ENABLED_TEST_ID);
}
/**
* Returns the name of the knocking participant (the only one) that is displayed on the notification.
*/
async getKnockingParticipantName() {
const knockingParticipantNotification
= this.participant.driver.$('//div[@data-testid="notify.participantWantsToJoin"]/div/div/span');
await knockingParticipantNotification.waitForDisplayed({
timeout: 3000,
timeoutMsg: 'Knocking participant notification not displayed'
});
return await knockingParticipantNotification.getText();
}
/**
* Admits the last knocking participant (it is the only one).
*/
async allowLobbyParticipant() {
const admitButton
= this.participant.driver.$(`[data-testid="${LOBBY_PARTICIPANT_ADMIT_TEST_ID}"]`);
await admitButton.waitForExist();
await admitButton.waitForClickable();
await admitButton.click();
}
/**
* The notification that someone's access was approved.
*/
getLobbyParticipantAccessGranted() {
return this.getNotificationText(LOBBY_PARTICIPANT_ACCESS_GRANTED_TEST_ID);
}
/**
* Closes the notification.
*/
closeLobbyParticipantAccessGranted() {
return this.closeLobbyNotification(LOBBY_PARTICIPANT_ACCESS_GRANTED_TEST_ID);
}
/**
* Returns notification text if the notification is found in the next few seconds.
* @return the notification text.
*/
private async getNotificationText(testId: string) {
const notificationElement = this.participant.driver.$(`[data-testid="${testId}"]`);
await notificationElement.waitForExist();
return await notificationElement.getText();
}
/**
* Rejects the last knocking participant (it is the only one).
*/
async rejectLobbyParticipant() {
const admitButton
= this.participant.driver.$(`[data-testid="${LOBBY_PARTICIPANT_REJECT_TEST_ID}"]`);
await admitButton.waitForExist();
await admitButton.waitForClickable();
await admitButton.click();
}
/**
* The notification test that someone's access was denied.
*/
getLobbyParticipantAccessDenied() {
return this.getNotificationText(LOBBY_PARTICIPANT_ACCESS_DENIED_TEST_ID);
}
/**
* Closes the notification.
*/
closeLobbyParticipantAccessDenied() {
return this.closeLobbyNotification(LOBBY_PARTICIPANT_ACCESS_DENIED_TEST_ID);
}
/**
* Waits for the notification for access denied for entering the lobby is shown.
*/
async waitForLobbyAccessDeniedNotification() {
const displayNameEl
= this.participant.driver.$(`div[data-testid="${LOBBY_ACCESS_DENIED_TEST_ID}"]`);
await displayNameEl.waitForExist({ timeout: 2000 });
await displayNameEl.waitForDisplayed();
}
/**
* Will wait 3 seconds for the knocking participants to disappear and return true or will return false.
* @return <tt>true</tt> if the knocking participants list was not displayed.
*/
waitForHideOfKnockingParticipants() {
return this.participant.driver.$(LOBBY_KNOCKING_PARTICIPANT_NOTIFICATION_XPATH)
.waitForDisplayed({
timeout: 3000,
reverse: true
});
}
/**
* Closes local notification, for the participant that was denied.
*/
async closeLocalLobbyAccessDenied() {
await this.participant.driver.$('[data-testid="lobby.joinRejectedMessage"').waitForExist();
const dismissButton
= this.participant.driver.$('[data-testid="lobby.joinRejectedTitle-dismiss"]');
await dismissButton.moveTo();
await dismissButton.click();
}
}

View File

@@ -1,3 +1,5 @@
import { ChainablePromiseElement } from 'webdriverio';
import { Participant } from '../helpers/Participant';
import AVModerationMenu from './AVModerationMenu';
@@ -24,7 +26,7 @@ export default class ParticipantsPane extends BasePageObject {
/**
* Checks if the pane is open.
*/
async isOpen() {
isOpen() {
return this.participant.driver.$(`.${PARTICIPANTS_PANE}`).isExisting();
}
@@ -184,4 +186,57 @@ export default class ParticipantsPane extends BasePageObject {
await inviteButton.waitForDisplayed();
await inviteButton.click();
}
/**
* Find the participant by name.
* @param name - The name to look for.
* @private
*/
private async findLobbyParticipantByName(name: string): Promise<ChainablePromiseElement> {
return this.participant.driver.$$('//div[@id="lobby-list"]//div[starts-with(@id, "participant-item-")]')
.find(async participant => (await participant.getText()).includes(name));
}
/**
* Tries to click on the approve button and fails if it cannot be clicked.
* @param participantNameToAdmit - the name of the participant to admit.
*/
async admitLobbyParticipant(participantNameToAdmit: string) {
const participantToAdmit = await this.findLobbyParticipantByName(participantNameToAdmit);
await participantToAdmit.moveTo();
const participantIdToAdmit = (await participantToAdmit.getAttribute('id'))
.substring('participant-item-'.length);
const admitButton = this.participant.driver
.$(`[data-testid="admit-${participantIdToAdmit}"]`);
await admitButton.waitForExist();
await admitButton.click();
}
/**
* Tries to click on the reject button and fails if it cannot be clicked.
* @param participantNameToReject - the name of the participant for this {@link ParticipantsPane} to reject.
*/
async rejectLobbyParticipant(participantNameToReject: string) {
const participantToReject
= await this.findLobbyParticipantByName(participantNameToReject);
await participantToReject.moveTo();
const participantIdToReject = (await participantToReject.getAttribute('id'))
.substring('participant-item-'.length);
const moreOptionsButton
= this.participant.driver.$(`aria/More moderation options ${participantNameToReject}`);
await moreOptionsButton.click();
const rejectButton = this.participant.driver
.$(`[data-testid="reject-${participantIdToReject}"]`);
await rejectButton.waitForExist();
await rejectButton.click();
}
}

View File

@@ -0,0 +1,31 @@
import PreMeetingScreen from './PreMeetingScreen';
const DISPLAY_NAME_ID = 'premeeting-name-input';
const JOIN_BUTTON_TEST_ID = 'prejoin.joinMeeting';
/**
* Page object for the PreJoin screen.
*/
export default class PreJoinScreen extends PreMeetingScreen {
/**
* Returns the join button element.
*/
getJoinButton(): ChainablePromiseElement {
return this.participant.driver.$(`[data-testid="${JOIN_BUTTON_TEST_ID}"]`);
}
/**
* Returns the display name input element.
*/
getDisplayNameInput(): ChainablePromiseElement {
return this.participant.driver.$(`#${DISPLAY_NAME_ID}`);
}
/**
* Waits for pre join screen to load.
*/
waitForLoading(): Promise<void> {
return this.participant.driver.$('[data-testid="prejoin.screen"]')
.waitForDisplayed({ timeout: 3000 });
}
}

View File

@@ -0,0 +1,90 @@
import BasePageObject from './BasePageObject';
const PASSWORD_BUTTON_TEST_ID = 'lobby.enterPasswordButton';
/**
* Page object for the PreMeeting screen, common stuff between pre-join and lobby screens.
*/
export default abstract class PreMeetingScreen extends BasePageObject {
/**
* Waits for pre join or lobby screen to load.
*/
abstract waitForLoading(): Promise<void>;
/**
* Returns the display name input element.
*/
abstract getDisplayNameInput(): WebdriverIO.Element;
/**
* Returns the join button element.
*/
abstract getJoinButton(): WebdriverIO.Element;
/**
* Interacts with the view to enter a display name.
*/
async enterDisplayName(displayName: string) {
const displayNameInput = this.getDisplayNameInput();
await displayNameInput.click();
// element.clear does not always work, make sure we delete the content
await displayNameInput.clearValue();
await this.participant.driver.keys(displayName);
}
/**
* Checks internally whether lobby room is joined.
*
* @returns {Promise<void>}
*/
waitToJoinLobby(): Promise<void> {
return this.participant.driver.waitUntil(
() => this.isLobbyRoomJoined(),
{
timeout: 3_000, // 3 seconds
timeoutMsg: `Timeout waiting to join lobby for ${this.participant.name}`
}
);
}
/**
* Checks internally whether lobby room is joined.
*/
isLobbyRoomJoined() {
return this.participant.driver.execute(
() => APP.conference._room?.room?.getLobby()?.lobbyRoom?.joined === true);
}
/**
* Returns the password button element.
*/
getPasswordButton() {
return this.participant.driver.$(`[data-testid="${PASSWORD_BUTTON_TEST_ID}"]`);
}
/**
* Interacts with the view to enter a password.
*/
async enterPassword(password: string) {
const passwordButton = this.getPasswordButton();
await passwordButton.moveTo();
await passwordButton.click();
const passwordInput = this.participant.driver.$('[data-testid="lobby.password"]');
await passwordInput.waitForDisplayed();
await passwordInput.click();
await passwordInput.clearValue();
await this.participant.driver.keys(password);
const joinButton = this.participant.driver.$('[data-testid="lobby.passwordJoinButton"]');
await joinButton.waitForDisplayed();
await joinButton.click();
}
}

View File

@@ -0,0 +1,136 @@
import { Key } from 'webdriverio';
import BaseDialog from './BaseDialog';
const ADD_PASSWORD_LINK = 'add-password';
const ADD_PASSWORD_FIELD = 'info-password-input';
const DIALOG_CONTAINER = 'security-dialog';
const LOCAL_LOCK = 'info-password-local';
const REMOTE_LOCK = 'info-password-remote';
/**
* Page object for the security dialog.
*/
export default class SecurityDialog extends BaseDialog {
/**
* Waits for the settings dialog to be visible.
*/
waitForDisplay() {
return this.participant.driver.$(`.${DIALOG_CONTAINER}`).waitForDisplayed();
}
/**
* Returns the switch that can be used to detect lobby state or change lobby state.
* @private
*/
private getLobbySwitch() {
return this.participant.driver.$('#lobby-section-switch');
}
/**
* Returns is the lobby enabled.
*/
isLobbyEnabled() {
return this.getLobbySwitch().isSelected();
}
/**
* Toggles the lobby option from the security dialog.
*/
async toggleLobby() {
const lobbySwitch = this.getLobbySwitch();
await lobbySwitch.moveTo();
await lobbySwitch.click();
}
/**
* Checks whether lobby section is present in the UI.
*/
isLobbySectionPresent() {
return this.getLobbySwitch().isExisting();
}
/**
* Waits for the lobby to be enabled or disabled.
* @param reverse
*/
waitForLobbyEnabled(reverse = false) {
const lobbySwitch = this.getLobbySwitch();
return this.participant.driver.waitUntil(
async () => await lobbySwitch.isSelected() !== reverse,
{
timeout: 5_000, // 30 seconds
timeoutMsg: `Timeout waiting for lobby being ${reverse ? 'disabled' : 'enabled'} for ${
this.participant.name}.`
}
);
}
/**
* Checks if the current conference is locked with a locally set password.
*
* @return {@code true} if the conference is displayed as locked locally in
* the security dialog, {@code false} otherwise.
*/
private isLockedLocally() {
return this.participant.driver.$(`.${LOCAL_LOCK}`).isExisting();
}
/**
* Checks if the current conference is locked with a locally set password.
*
* @return {@code true} if the conference is displayed as locked remotely
* in the security dialog, {@code false} otherwise.
*/
private isLockedRemotely() {
return this.participant.driver.$(`.${REMOTE_LOCK}`).isExisting();
}
/**
* Checks if the current conference is locked based on the security dialog's
* display state.
*
* @return {@code true} if the conference is displayed as locked in the
* security dialog, {@code false} otherwise.
*/
async isLocked() {
return await this.isLockedLocally() || await this.isLockedRemotely();
}
/**
* Sets a password on the current conference to lock it.
*
* @param password - The password to use to lock the conference.
*/
async addPassword(password: string) {
const addPasswordLink = this.participant.driver.$(`.${ADD_PASSWORD_LINK}`);
await addPasswordLink.waitForClickable();
await addPasswordLink.click();
const passwordEntry = this.participant.driver.$(`#${ADD_PASSWORD_FIELD}`);
await passwordEntry.waitForDisplayed();
await passwordEntry.click();
await this.participant.driver.keys(password);
await this.participant.driver.$('button=Add').click();
let validationMessage;
// There are two cases here, validation is enabled and the field passwordEntry maybe there
// with validation failed, or maybe successfully hidden after setting the password
// So let's give it some time to act on any of the above
if (!await passwordEntry.isExisting()) {
// validation had failed on password field as it is still on the page
validationMessage = passwordEntry.getAttribute('validationMessage');
}
if (validationMessage) {
await this.participant.driver.keys([ Key.Escape ]);
expect(validationMessage).toBe('');
}
}
}

View File

@@ -15,8 +15,8 @@ export default class SettingsDialog extends BaseDialog {
/**
* Waits for the settings dialog to be visible.
*/
async waitForDisplay() {
await this.participant.driver.$(SETTINGS_DIALOG_CONTENT).waitForDisplayed();
waitForDisplay() {
return this.participant.driver.$(SETTINGS_DIALOG_CONTENT).waitForDisplayed();
}
/**
@@ -34,22 +34,22 @@ export default class SettingsDialog extends BaseDialog {
/**
* Selects the Profile tab to be displayed.
*/
async openProfileTab() {
await this.openTab(X_PATH_PROFILE_TAB);
openProfileTab() {
return this.openTab(X_PATH_PROFILE_TAB);
}
/**
* Selects the More tab to be displayed.
*/
async openMoreTab() {
await this.openTab(X_PATH_MORE_TAB);
openMoreTab() {
return this.openTab(X_PATH_MORE_TAB);
}
/**
* Selects the moderator tab to be displayed.
*/
async openModeratorTab() {
await this.openTab(X_PATH_MODERATOR_TAB);
openModeratorTab() {
return this.openTab(X_PATH_MODERATOR_TAB);
}
/**
@@ -74,8 +74,8 @@ export default class SettingsDialog extends BaseDialog {
/**
* Clicks the OK button on the settings dialog to close the dialog and save any changes made.
*/
async submit() {
await this.clickOkButton();
submit() {
return this.clickOkButton();
}
/**

View File

@@ -12,6 +12,7 @@ const OVERFLOW = 'More actions';
const PARTICIPANTS = 'Open participants pane';
const PROFILE = 'Edit your profile';
const RAISE_HAND = 'Raise your hand';
const SECURITY = 'Security options';
const SETTINGS = 'Open settings';
const STOP_DESKTOP = 'Stop sharing your screen';
const ENTER_TILE_VIEW_BUTTON = 'Enter tile view';
@@ -54,9 +55,10 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickAudioMuteButton(): Promise<void> {
clickAudioMuteButton(): Promise<void> {
this.participant.log('Clicking on: Audio Mute Button');
await this.audioMuteBtn.click();
return this.audioMuteBtn.click();
}
/**
@@ -64,9 +66,10 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickAudioUnmuteButton(): Promise<void> {
clickAudioUnmuteButton(): Promise<void> {
this.participant.log('Clicking on: Audio Unmute Button');
await this.audioUnMuteBtn.click();
return this.audioUnMuteBtn.click();
}
/**
@@ -88,9 +91,10 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickVideoMuteButton(): Promise<void> {
clickVideoMuteButton(): Promise<void> {
this.participant.log('Clicking on: Video Mute Button');
await this.videoMuteBtn.click();
return this.videoMuteBtn.click();
}
/**
@@ -98,9 +102,10 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickVideoUnmuteButton(): Promise<void> {
clickVideoUnmuteButton(): Promise<void> {
this.participant.log('Clicking on: Video Unmute Button');
await this.videoUnMuteBtn.click();
return this.videoUnMuteBtn.click();
}
/**
@@ -108,9 +113,10 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickCloseParticipantsPaneButton(): Promise<void> {
clickCloseParticipantsPaneButton(): Promise<void> {
this.participant.log('Clicking on: Close Participants pane Button');
await this.getButton(CLOSE_PARTICIPANTS_PANE).click();
return this.getButton(CLOSE_PARTICIPANTS_PANE).click();
}
/**
@@ -118,94 +124,105 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickParticipantsPaneButton(): Promise<void> {
clickParticipantsPaneButton(): Promise<void> {
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
await this.participant.driver.$(`[aria-label^="${PARTICIPANTS}"]`).click();
return this.participant.driver.$(`[aria-label^="${PARTICIPANTS}"]`).click();
}
/**
* Clicks on the video quality toolbar button which opens the
* dialog for adjusting max-received video quality.
*/
async clickVideoQualityButton(): Promise<void> {
clickVideoQualityButton(): Promise<void> {
return this.clickButtonInOverflowMenu(VIDEO_QUALITY);
}
/**
* Clicks on the profile toolbar button which opens or closes the profile panel.
*/
async clickProfileButton(): Promise<void> {
clickProfileButton(): Promise<void> {
return this.clickButtonInOverflowMenu(PROFILE);
}
/**
* Clicks on the raise hand button that enables participants will to speak.
*/
async clickRaiseHandButton(): Promise<void> {
clickRaiseHandButton(): Promise<void> {
this.participant.log('Clicking on: Raise hand Button');
await this.getButton(RAISE_HAND).click();
return this.getButton(RAISE_HAND).click();
}
/**
* Clicks on the chat button that opens chat panel.
*/
async clickChatButton(): Promise<void> {
clickChatButton(): Promise<void> {
this.participant.log('Clicking on: Chat Button');
await this.getButton(CHAT).click();
return this.getButton(CHAT).click();
}
/**
* Clicks on the chat button that closes chat panel.
*/
async clickCloseChatButton(): Promise<void> {
clickCloseChatButton(): Promise<void> {
this.participant.log('Clicking on: Close Chat Button');
await this.getButton(CLOSE_CHAT).click();
return this.getButton(CLOSE_CHAT).click();
}
/**
* Clicks on the desktop sharing button that starts desktop sharing.
*/
async clickDesktopSharingButton() {
await this.getButton(DESKTOP).click();
clickDesktopSharingButton() {
return this.getButton(DESKTOP).click();
}
/**
* Clicks on the desktop sharing button to stop it.
*/
async clickStopDesktopSharingButton() {
await this.getButton(STOP_DESKTOP).click();
clickStopDesktopSharingButton() {
return this.getButton(STOP_DESKTOP).click();
}
/**
* Clicks on the tile view button which enables tile layout.
*/
async clickEnterTileViewButton() {
await this.getButton(ENTER_TILE_VIEW_BUTTON).click();
clickEnterTileViewButton() {
return this.getButton(ENTER_TILE_VIEW_BUTTON).click();
}
/**
* Clicks on the tile view button which exits tile layout.
*/
async clickExitTileViewButton() {
await this.getButton(EXIT_TILE_VIEW_BUTTON).click();
clickExitTileViewButton() {
return this.getButton(EXIT_TILE_VIEW_BUTTON).click();
}
/**
* Clicks on the hangup button that ends the conference.
*/
async clickHangupButton(): Promise<void> {
clickHangupButton(): Promise<void> {
this.participant.log('Clicking on: Hangup Button');
await this.getButton(HANGUP).click();
return this.getButton(HANGUP).click();
}
/**
* Clicks on the security toolbar button which opens the security panel.
*/
clickSecurityButton() {
return this.clickButtonInOverflowMenu(SECURITY);
}
/**
* Clicks on the settings toolbar button which opens or closes the settings panel.
*/
async clickSettingsButton() {
await this.clickButtonInOverflowMenu(SETTINGS);
clickSettingsButton() {
return this.clickButtonInOverflowMenu(SETTINGS);
}
/**
@@ -238,8 +255,8 @@ export default class Toolbar extends BasePageObject {
* Clicks on the overflow toolbar button which opens or closes the overflow menu.
* @private
*/
private async clickOverflowButton(): Promise<void> {
await this.getButton(OVERFLOW).click();
private clickOverflowButton(): Promise<void> {
return this.getButton(OVERFLOW).click();
}
/**
@@ -275,8 +292,8 @@ export default class Toolbar extends BasePageObject {
* @param visible
* @private
*/
private async waitForOverFlowMenu(visible: boolean) {
await this.getButton(OVERFLOW_MENU).waitForDisplayed({
private waitForOverFlowMenu(visible: boolean) {
return this.getButton(OVERFLOW_MENU).waitForDisplayed({
reverse: !visible,
timeout: 3000,
timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`

Binary file not shown.

View File

@@ -0,0 +1,9 @@
fakeAudioStream.wav created from [1].
Speaker: The Jacobin
Derivative of [2]
Licensed under Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)[3]
[1]: https://en.wikipedia.org/wiki/File:Helium.ogg
[2]: https://en.wikipedia.org/wiki/Helium?oldid=302049858
[3]: https://creativecommons.org/licenses/by-sa/3.0/deed.en

View File

@@ -1,18 +1,12 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Audio only - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx, {
skipFirstModerator: true
});
});
describe('Audio only', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx, { skipFirstModerator: true }));
/**
* Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.
*/
it('set and check', async () => {
await setAudioOnlyAndCheck(true);
});
it('set and check', () => setAudioOnlyAndCheck(true));
/**
* Verifies that participant1 sees avatars for itself and other participants.
@@ -29,9 +23,7 @@ describe('Audio only - ', () => {
/**
* Disables audio only mode and verifies that both participants see p1 as not video muted.
*/
it('disable and check', async () => {
await setAudioOnlyAndCheck(false);
});
it('disable and check', () => setAudioOnlyAndCheck(false));
/**
* Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1

View File

@@ -1,11 +1,7 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('DisplayName - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx, {
skipDisplayName: true
});
});
describe('DisplayName', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx, { skipDisplayName: true }));
it('check change', async () => {
const { p1, p2 } = ctx;

View File

@@ -1,9 +1,7 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('End Conference - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
});
describe('End Conference', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
it('hangup call and check', async () => {
const { p1 } = ctx;

View File

@@ -3,7 +3,7 @@ import process from 'node:process';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Fake Dial-In - ', () => {
describe('Fake Dial-In', () => {
it('join participant', async () => {
// we execute fake dial in only if the real dial in is not enabled

View File

@@ -1,6 +1,6 @@
import { isEqual } from 'lodash-es';
import type { Participant } from '../../helpers/Participant';
import { P1_DISPLAY_NAME, P2_DISPLAY_NAME, Participant } from '../../helpers/Participant';
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
/**
@@ -30,7 +30,7 @@ async function checkParticipantLeftHook(p: Participant, reason: string) {
}
}
describe('Participants presence - ', () => {
describe('Participants presence', () => {
it('joining the meeting', async () => {
// ensure 2 participants one moderator and one guest, we will load both with iframeAPI
await ensureTwoParticipants(ctx);
@@ -144,7 +144,7 @@ describe('Participants presence - ', () => {
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
await p2.driver.waitUntil(async () => await p2.getIframeAPI().getEventResult('isModerator'), {
await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('isModerator'), {
timeout: 3000,
timeoutMsg: 'Moderator role not granted'
});
@@ -202,13 +202,11 @@ describe('Participants presence - ', () => {
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
const eventP1 = await p1.driver.waitUntil(async () =>
await p1.getIframeAPI().getEventResult('participantKickedOut'), {
const eventP1 = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on participant1 side'
});
const eventP2 = await p2.driver.waitUntil(async () =>
await p2.getIframeAPI().getEventResult('participantKickedOut'), {
const eventP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on participant2 side'
});
@@ -243,8 +241,8 @@ describe('Participants presence - ', () => {
}
})).toBe(true);
const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
const eventConferenceLeftP2 = await p2.driver.waitUntil(() =>
p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 2000,
timeoutMsg: 'videoConferenceLeft not received'
});
@@ -289,8 +287,7 @@ describe('Participants presence - ', () => {
await p1.switchToAPI();
const event = await p1.driver.waitUntil(async () =>
await p1.getIframeAPI().getEventResult('participantJoined'), {
const event = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantJoined'), {
timeout: 2000,
timeoutMsg: 'participantJoined not received'
});
@@ -310,8 +307,8 @@ describe('Participants presence - ', () => {
const p1EpId = await p1.getEndpointId();
const p2EpId = await p2.getEndpointId();
const newP1Name = 'p1';
const newP2Name = 'p2';
const newP1Name = P1_DISPLAY_NAME;
const newP2Name = P2_DISPLAY_NAME;
const newNames: ({ id: string; name: string; })[] = [ {
id: p2EpId,
name: newP2Name
@@ -341,8 +338,8 @@ describe('Participants presence - ', () => {
await p2.getIframeAPI().executeCommand('hangup');
const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
const eventConferenceLeftP2 = await p2.driver.waitUntil(() =>
p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 2000,
timeoutMsg: 'videoConferenceLeft not received'
});
@@ -352,8 +349,7 @@ describe('Participants presence - ', () => {
await checkParticipantLeftHook(p2, 'left');
const eventReadyToCloseP2 = await p2.driver.waitUntil(async () =>
await p2.getIframeAPI().getEventResult('readyToClose'), {
const eventReadyToCloseP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('readyToClose'), {
timeout: 2000,
timeoutMsg: 'readyToClose not received'
});
@@ -371,8 +367,8 @@ describe('Participants presence - ', () => {
await p1.getIframeAPI().executeCommand('hangup');
const eventConferenceLeft = await p1.driver.waitUntil(async () =>
await p1.getIframeAPI().getEventResult('videoConferenceLeft'), {
const eventConferenceLeft = await p1.driver.waitUntil(() =>
p1.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 2000,
timeoutMsg: 'videoConferenceLeft not received'
});
@@ -397,8 +393,7 @@ describe('Participants presence - ', () => {
expect(event.data.isBreakout).toBe(false);
}
const eventReadyToClose = await p1.driver.waitUntil(async () =>
await p1.getIframeAPI().getEventResult('readyToClose'), {
const eventReadyToClose = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('readyToClose'), {
timeout: 2000,
timeoutMsg: 'readyToClose not received'
});

View File

@@ -0,0 +1,39 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Kick', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
if (!await ctx.p1.isModerator()) {
ctx.skipSuiteTests = true;
}
});
it('kick and check', () => kickParticipant2AndCheck());
it('kick p2p and check', async () => {
await ensureTwoParticipants(ctx, {
configOverwrite: {
p2p: {
enabled: true
}
}
});
await kickParticipant2AndCheck();
});
});
/**
* Kicks the second participant and checks that the participant is removed from the conference and that dialog is open.
*/
async function kickParticipant2AndCheck() {
const { p1, p2 } = ctx;
await p1.getFilmstrip().kickParticipant(await p2.getEndpointId());
await p1.waitForParticipants(0);
// check that the kicked participant sees the kick reason dialog
expect(await p2.isLeaveReasonDialogOpen()).toBe(true);
}

View File

@@ -1,10 +1,8 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Self view - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
});
describe('Self view', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
it('hide from menu', async () => {
const { p1 } = ctx;

View File

@@ -2,7 +2,7 @@
import type { Participant } from '../../helpers/Participant';
import { ensureThreeParticipants, muteAudioAndCheck } from '../../helpers/participants';
describe('ActiveSpeaker ', () => {
describe('ActiveSpeaker', () => {
it('testActiveSpeaker', async () => {
await ensureThreeParticipants(ctx);
@@ -61,8 +61,7 @@ async function testActiveSpeaker(
const otherParticipant1Driver = otherParticipant1.driver;
await otherParticipant1Driver.waitUntil(
async () => await otherParticipant1Driver.execute(
id => APP.UI.getLargeVideoID() === id, speakerEndpoint),
() => otherParticipant1Driver.execute(id => APP.UI.getLargeVideoID() === id, speakerEndpoint),
{
timeout: 30_000, // 30 seconds
timeoutMsg: 'Active speaker not displayed on large video.'

View File

@@ -6,7 +6,7 @@ import {
unmuteVideoAndCheck
} from '../../helpers/participants';
describe('AVModeration -', () => {
describe('AVModeration', () => {
it('check for moderators', async () => {
// if all 3 participants are moderators, skip this test
@@ -135,7 +135,7 @@ describe('AVModeration -', () => {
await p1.getFilmstrip().grantModerator(p3);
await p3.driver.waitUntil(
async () => await p3.isModerator(), {
() => p3.isModerator(), {
timeout: 5000,
timeoutMsg: `${p3.name} is not moderator`
});
@@ -190,6 +190,7 @@ describe('AVModeration -', () => {
await p1ParticipantsPane.clickContextMenuButton();
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
await p1ParticipantsPane.close();
// join with second participant and check
await ensureTwoParticipants(ctx, {

View File

@@ -7,16 +7,15 @@ import {
const EMAIL = 'support@jitsi.org';
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
describe('Avatar - ', () => {
it('setup the meeting', async () => {
// Start p1
await ensureTwoParticipants(ctx, {
describe('Avatar', () => {
it('setup the meeting', () =>
ensureTwoParticipants(ctx, {
skipDisplayName: true,
// no default avatar if we have used to join a token with an avatar and no option to set it
skipFirstModerator: true
});
});
})
);
it('change and check', async () => {
const { p1, p2 } = ctx;

View File

@@ -7,7 +7,7 @@ const MAIN_ROOM_NAME = 'Main room';
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
const LIST_ITEM_CONTAINER = 'list-item-container';
describe('BreakoutRooms ', () => {
describe('BreakoutRooms', () => {
it('check support', async () => {
await ensureTwoParticipants(ctx);
@@ -266,8 +266,8 @@ describe('BreakoutRooms ', () => {
});
// there should be two participants in the main room, either p2 or p3 got moved to the main room
const checkParticipants = async (p: Participant) => {
await p.driver.waitUntil(
const checkParticipants = (p: Participant) =>
p.driver.waitUntil(
async () => {
const isInBreakoutRoom = await p.isInBreakoutRoom();
const breakoutRooms = p.getBreakoutRooms();
@@ -303,7 +303,6 @@ describe('BreakoutRooms ', () => {
timeout: 2000,
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
});
};
await checkParticipants(p2);
await checkParticipants(p3);
@@ -421,8 +420,10 @@ describe('BreakoutRooms ', () => {
await checkSubject(p2, myNewRoomName);
const p2BreakoutRooms = p2.getBreakoutRooms();
// leave room
await p2.getBreakoutRooms().leaveBreakoutRoom();
await p2BreakoutRooms.leaveBreakoutRoom();
// there should be one empty room
await p1.driver.waitUntil(
@@ -436,10 +437,20 @@ describe('BreakoutRooms ', () => {
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeoutMsg: 'The breakout room was not renamed for p1'
timeoutMsg: 'The breakout room not found or not empty for p1'
});
expect((await p2.getBreakoutRooms().getRooms())[0].name).toBe(myNewRoomName);
await p2.driver.waitUntil(
async () => {
const list = await p2BreakoutRooms.getRooms();
return list?.length === 1;
}, {
timeout: 3000,
timeoutMsg: 'The breakout room not seen by p2'
});
expect((await p2BreakoutRooms.getRooms())[0].name).toBe(myNewRoomName);
// send the second participant to the first breakout room
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);

View File

@@ -1,6 +1,6 @@
import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
describe('Codec selection - ', () => {
describe('Codec selection', () => {
it('asymmetric codecs', async () => {
await ensureOneParticipant(ctx, {
configOverwrite: {
@@ -104,14 +104,14 @@ describe('Codec selection - ', () => {
// Check of p1 and p2 have switched to VP9.
await p1.driver.waitUntil(
async () => await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
() => p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'
}
);
await p2.driver.waitUntil(
async () => await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
() => p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'

View File

@@ -1,6 +1,6 @@
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
describe('Follow Me - ', () => {
describe('Follow Me', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);

View File

@@ -0,0 +1,487 @@
import { P1_DISPLAY_NAME, P3_DISPLAY_NAME, Participant } from '../../helpers/Participant';
import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
import type { IJoinOptions } from '../../helpers/types';
import type PreMeetingScreen from '../../pageobjects/PreMeetingScreen';
describe('Lobby', () => {
it('joining the meeting', async () => {
await ensureOneParticipant(ctx);
if (!await ctx.p1.driver.execute(() => APP.conference._room.isLobbySupported())) {
ctx.skipSuiteTests = true;
}
});
it('enable', async () => {
await ensureTwoParticipants(ctx);
await enableLobby();
});
it('entering in lobby and approve', async () => {
const { p1, p2 } = ctx;
await enterLobby(p1, true);
const { p3 } = ctx;
await p1.getNotifications().allowLobbyParticipant();
const notificationText = await p2.getNotifications().getLobbyParticipantAccessGranted();
expect(notificationText.includes(P1_DISPLAY_NAME)).toBe(true);
expect(notificationText.includes(P3_DISPLAY_NAME)).toBe(true);
await p2.getNotifications().closeLobbyParticipantAccessGranted();
// ensure 3 participants in the call will check for the third one that muc is joined, ice connected,
// media is being receiving and there are two remote streams
await p3.waitToJoinMUC();
await p3.waitForIceConnected();
await p3.waitForSendReceiveData();
await p3.waitForRemoteStreams(2);
// now check third one display name in the room, is the one set in the prejoin screen
const name = await p1.getFilmstrip().getRemoteDisplayName(await p3.getEndpointId());
expect(name).toBe(P3_DISPLAY_NAME);
await p3.hangup();
});
it('entering in lobby and deny', async () => {
const { p1, p2 } = ctx;
// the first time tests is executed we need to enter display name,
// for next execution that will be locally stored
await enterLobby(p1, false);
// moderator rejects access
await p1.getNotifications().rejectLobbyParticipant();
// deny notification on 2nd participant
const notificationText = await p2.getNotifications().getLobbyParticipantAccessDenied();
expect(notificationText.includes(P1_DISPLAY_NAME)).toBe(true);
expect(notificationText.includes(P3_DISPLAY_NAME)).toBe(true);
await p2.getNotifications().closeLobbyParticipantAccessDenied();
const { p3 } = ctx;
// check the denied one is out of lobby, sees the notification about it
await p3.getNotifications().waitForLobbyAccessDeniedNotification();
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
await p3.hangup();
});
it('approve from participants pane', async () => {
const { p1 } = ctx;
const knockingParticipant = await enterLobby(p1, false);
// moderator allows access
const p1ParticipantsPane = p1.getParticipantsPane();
await p1ParticipantsPane.open();
await p1ParticipantsPane.admitLobbyParticipant(knockingParticipant);
await p1ParticipantsPane.close();
const { p3 } = ctx;
// ensure 3 participants in the call will check for the third one that muc is joined, ice connected,
// media is being receiving and there are two remote streams
await p3.waitToJoinMUC();
await p3.waitForIceConnected();
await p3.waitForSendReceiveData();
await p3.waitForRemoteStreams(2);
// now check third one display name in the room, is the one set in the prejoin screen
// now check third one display name in the room, is the one set in the prejoin screen
const name = await p1.getFilmstrip().getRemoteDisplayName(await p3.getEndpointId());
expect(name).toBe(P3_DISPLAY_NAME);
await p3.hangup();
});
it('reject from participants pane', async () => {
const { p1 } = ctx;
const knockingParticipant = await enterLobby(p1, false);
// moderator rejects access
const p1ParticipantsPane = p1.getParticipantsPane();
await p1ParticipantsPane.open();
await p1ParticipantsPane.rejectLobbyParticipant(knockingParticipant);
await p1ParticipantsPane.close();
const { p3 } = ctx;
// check the denied one is out of lobby, sees the notification about it
// The third participant should see a warning that his access to the room was denied
await p3.getNotifications().waitForLobbyAccessDeniedNotification();
// check Lobby room not left
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
await p3.hangup();
});
it('lobby user leave', async () => {
const { p1 } = ctx;
await enterLobby(p1, false);
await ctx.p3.hangup();
// check that moderator (participant 1) no longer sees notification about participant in lobby
await p1.getNotifications().waitForHideOfKnockingParticipants();
});
it('conference ended in lobby', async () => {
const { p1, p2 } = ctx;
await enterLobby(p1, false);
await p1.hangup();
await p2.hangup();
const { p3 } = ctx;
await p3.driver.$('.dialog.leaveReason').isExisting();
await p3.driver.waitUntil(
async () => !await p3.getLobbyScreen().isLobbyRoomJoined(),
{
timeout: 2000,
timeoutMsg: 'p2 did not leave lobby'
}
);
await p3.hangup();
});
it('disable while participant in lobby', async () => {
await ensureTwoParticipants(ctx);
const { p1 } = ctx;
await enableLobby();
await enterLobby(p1);
// WebParticipant participant1 = getParticipant1();
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
await p1SecurityDialog.toggleLobby();
await p1SecurityDialog.waitForLobbyEnabled();
const { p3 } = ctx;
await p3.waitToJoinMUC();
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
});
it('change of moderators in lobby', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureTwoParticipants(ctx);
const { p1, p2 } = ctx;
// hanging up the first one, which is moderator and second one should be
await p1.hangup();
await p2.driver.waitUntil(
() => p2.isModerator(),
{
timeout: 3000,
timeoutMsg: 'p2 is not moderator after p1 leaves'
}
);
const p2SecurityDialog = p2.getSecurityDialog();
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
await p2SecurityDialog.toggleLobby();
await p2SecurityDialog.waitForLobbyEnabled();
// here the important check is whether the moderator sees the knocking participant
await enterLobby(p2, false);
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
});
it('shared password', async () => {
await ensureTwoParticipants(ctx);
const { p1 } = ctx;
await enableLobby();
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(false);
const roomPasscode = String(Math.random() * 1_000);
await p1SecurityDialog.addPassword(roomPasscode);
await p1.driver.waitUntil(
() => p1SecurityDialog.isLocked(),
{
timeout: 2000,
timeoutMsg: 'room did not lock for p1'
}
);
await enterLobby(p1, false);
const { p3 } = ctx;
// now fill in password
const lobbyScreen = p3.getLobbyScreen();
await lobbyScreen.enterPassword(roomPasscode);
await p3.waitToJoinMUC();
await p3.waitForIceConnected();
await p3.waitForSendReceiveData();
});
it('enable with more than two participants', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureThreeParticipants(ctx);
await enableLobby();
// we need to check remote participants as isInMuc has not changed its value as
// the bug is triggered by presence with status 322 which is not handled correctly
const { p1, p2, p3 } = ctx;
await p1.waitForRemoteStreams(2);
await p2.waitForRemoteStreams(2);
await p3.waitForRemoteStreams(2);
});
it('moderator leaves while lobby enabled', async () => {
const { p1, p2, p3 } = ctx;
await p3.hangup();
await p1.hangup();
await p2.driver.waitUntil(
() => p2.isModerator(),
{
timeout: 3000,
timeoutMsg: 'p2 is not moderator after p1 leaves'
}
);
const lobbyScreen = p2.getLobbyScreen();
expect(await lobbyScreen.isLobbyRoomJoined()).toBe(true);
});
it('reject and approve in pre-join', async () => {
await ctx.p2.hangup();
await ensureTwoParticipants(ctx);
await enableLobby();
const { p1 } = ctx;
const knockingParticipant = await enterLobby(p1, true, true);
// moderator rejects access
const p1ParticipantsPane = p1.getParticipantsPane();
await p1ParticipantsPane.open();
await p1ParticipantsPane.rejectLobbyParticipant(knockingParticipant);
await p1ParticipantsPane.close();
const { p3 } = ctx;
// check the denied one is out of lobby, sees the notification about it
// The third participant should see a warning that his access to the room was denied
await p3.getNotifications().waitForLobbyAccessDeniedNotification();
// check Lobby room left
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
// try again entering the lobby with the third one and approve it
// check that everything is fine in the meeting
await p3.getNotifications().closeLocalLobbyAccessDenied();
// let's retry to enter the lobby and approve this time
const lobbyScreen = p3.getPreJoinScreen();
// click join button
await lobbyScreen.getJoinButton().click();
await lobbyScreen.waitToJoinLobby();
// check that moderator (participant 1) sees notification about participant in lobby
const name = await p1.getNotifications().getKnockingParticipantName();
expect(name).toBe(P3_DISPLAY_NAME);
expect(await lobbyScreen.isLobbyRoomJoined()).toBe(true);
await p1ParticipantsPane.open();
await p1ParticipantsPane.admitLobbyParticipant(knockingParticipant);
await p1ParticipantsPane.close();
await p3.waitForParticipants(2);
await p3.waitForRemoteStreams(2);
expect(await p3.getFilmstrip().countVisibleThumbnails()).toBe(3);
});
});
/**
* Enable lobby and check that it is enabled.
*/
async function enableLobby() {
const { p1, p2 } = ctx;
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLobbyEnabled()).toBe(false);
await p1SecurityDialog.toggleLobby();
await p1SecurityDialog.waitForLobbyEnabled();
expect((await p2.getNotifications().getLobbyEnabledText()).includes(p1.displayName)).toBe(true);
await p2.getNotifications().closeLobbyEnabled();
const p2SecurityDialog = p2.getSecurityDialog();
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
// lobby is visible to moderators only, this depends on whether deployment is all moderators or not
if (await p2.isModerator()) {
await p2SecurityDialog.waitForLobbyEnabled();
} else {
expect(await p2SecurityDialog.isLobbySectionPresent()).toBe(false);
}
// let's close the security dialog, or we will not be able to click
// on popups for allow/deny participants
await p1SecurityDialog.clickCloseButton();
await p2SecurityDialog.clickCloseButton();
}
/**
* Expects that lobby is enabled for the room we will try to join.
* Lobby UI is shown, enter display name and join.
* Checks Lobby UI and also that when joining the moderator sees corresponding notifications.
*
* @param participant The participant that is moderator in the meeting.
* @param enterDisplayName whether to enter display name. We need to enter display name only the first time when
* a participant sees the lobby screen, next time visiting the page display name will be pre-filled
* from local storage.
* @param usePreJoin
* @return the participant name knocking.
*/
async function enterLobby(participant: Participant, enterDisplayName = false, usePreJoin = false) {
const options: IJoinOptions = {};
if (usePreJoin) {
options.configOverwrite = {
prejoinConfig: {
enabled: true
}
};
}
await ensureThreeParticipants(ctx, {
...options,
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
});
const { p3 } = ctx;
let screen: PreMeetingScreen;
// WebParticipant participant3 = getParticipant3();
// ParentPreMeetingScreen lobbyScreen;
if (usePreJoin) {
screen = p3.getPreJoinScreen();
} else {
screen = p3.getLobbyScreen();
}
// participant 3 should be now on pre-join screen
await screen.waitForLoading();
const displayNameInput = screen.getDisplayNameInput();
// check display name is visible
expect(await displayNameInput.isExisting()).toBe(true);
expect(await displayNameInput.isDisplayed()).toBe(true);
const joinButton = screen.getJoinButton();
expect(await joinButton.isExisting()).toBe(true);
if (enterDisplayName) {
let classes = await joinButton.getAttribute('class');
if (!usePreJoin) {
// check join button is disabled
expect(classes.includes('disabled')).toBe(true);
}
// TODO check that password is hidden as the room does not have password
// this check needs to be added once the functionality exists
// enter display name
await screen.enterDisplayName(P3_DISPLAY_NAME);
// check join button is enabled
classes = await joinButton.getAttribute('class');
expect(classes.includes('disabled')).toBe(false);
}
// click join button
await screen.getJoinButton().click();
await screen.waitToJoinLobby();
// check no join button
expect(!await joinButton.isExisting() || !await joinButton.isDisplayed() || !await joinButton.isEnabled())
.toBe(true);
// new screen, is password button shown
const passwordButton = screen.getPasswordButton();
expect(await passwordButton.isExisting()).toBe(true);
expect(await passwordButton.isEnabled()).toBe(true);
// check that moderator (participant 1) sees notification about participant in lobby
const name = await participant.getNotifications().getKnockingParticipantName();
expect(name).toBe(P3_DISPLAY_NAME);
expect(await screen.isLobbyRoomJoined()).toBe(true);
return name;
}

View File

@@ -7,7 +7,7 @@ import {
ensureTwoParticipants
} from '../../helpers/participants';
describe('Desktop sharing - ', () => {
describe('Desktop sharing', () => {
it('start', async () => {
await ensureTwoParticipants(ctx, {
configOverwrite: {
@@ -203,8 +203,7 @@ describe('Desktop sharing - ', () => {
await checkForScreensharingTile(p3, p2);
// the video should be playing
await p1.driver.waitUntil(async () =>
await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
await p1.driver.waitUntil(() => p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
timeout: 5_000,
timeoutMsg: 'expected remote screen share to be on large'
});
@@ -255,8 +254,7 @@ describe('Desktop sharing - ', () => {
expect(await p1.driver.execute(() => APP.UI.getLargeVideoID())).toBe(`${await p3.getEndpointId()}-v1`);
// the video should be playing
await p1.driver.waitUntil(async () =>
await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
await p1.driver.waitUntil(() => p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
timeout: 5_000,
timeoutMsg: 'expected remote screen share to be on large'
});
@@ -296,49 +294,20 @@ describe('Desktop sharing - ', () => {
expect(await p4.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
const p1EndpointId = await p1.getEndpointId();
const p2EndpointId = await p2.getEndpointId();
// p4 has lastN=2 and has selected p3. With p2 being dominant speaker p4 should eventually
// see video for [p3, p2] and p1 as ninja.
await p4.driver.$(`//span[@id='participant_${p1EndpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000
});
const p2EndpointId = await p2.getEndpointId();
await p4.driver.waitUntil(async () =>
await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
p2EndpointId) && await p4.driver.$(
`//span[@id="participant_${p2EndpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: 'expected remote video to be received 15s'
});
await p4.driver.waitUntil(async () =>
await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
p2EndpointId) && await p4.driver.$(
`//span[@id="participant_${p2EndpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: 'expected remote video to be received 15s'
});
await p4.waitForNinjaIcon(p1EndpointId);
await p4.waitForRemoteVideo(p2EndpointId);
// Let's switch and check, muting participant 2 and unmuting 1 will leave participant 1 as dominant
await p1.getToolbar().clickAudioUnmuteButton();
await p2.getToolbar().clickAudioMuteButton();
// Participant4 should eventually see video for [p3, p1] and p2 as a ninja.
await p4.driver.$(`//span[@id='participant_${p2EndpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000
});
await p4.driver.waitUntil(async () =>
await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
p1EndpointId) && await p4.driver.$(
`//span[@id="participant_${p1EndpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: 'expected remote video to be received 15s'
});
await p4.waitForNinjaIcon(p2EndpointId);
await p4.waitForRemoteVideo(p1EndpointId);
});
});

View File

@@ -0,0 +1,73 @@
import { ensureFourParticipants, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
describe('lastN', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx, {
skipInMeetingChecks: true,
configOverwrite: {
startWithAudioMuted: true,
startWithVideoMuted: true,
channelLastN: 1
}
});
await ensureThreeParticipants(ctx, {
skipInMeetingChecks: true,
configOverwrite: {
channelLastN: 1
}
});
});
it('checks', async () => {
const { p3 } = ctx;
const p3Toolbar = p3.getToolbar();
await p3.waitForSendData();
await ctx.p1.waitForRemoteVideo(await p3.getEndpointId());
// Mute audio on participant3.
await p3Toolbar.clickAudioMuteButton();
await ensureFourParticipants(ctx, {
skipInMeetingChecks: true,
configOverwrite: {
channelLastN: 1
}
});
const { p1, p2, p4 } = ctx;
await p4.waitForSendReceiveData();
// Mute audio on p4 and unmute p3.
await p4.getToolbar().clickAudioMuteButton();
await p3Toolbar.clickAudioUnmuteButton();
const p4EndpointId = await p4.getEndpointId();
const p3EndpointId = await p3.getEndpointId();
// Check if p1 starts receiving video from p3 and p4 shows up as ninja.
await p1.waitForNinjaIcon(p4EndpointId);
await p1.waitForRemoteVideo(p3EndpointId);
// At this point, mute video of p3 and others should be receiving p4's video.
// Mute p1's video
await p3Toolbar.clickVideoMuteButton();
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p3);
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p3);
await p1.waitForRemoteVideo(p4EndpointId);
// Unmute p3's video and others should switch to receiving p3's video.
await p3Toolbar.clickVideoUnmuteButton();
await p1.waitForRemoteVideo(p3EndpointId);
await p1.waitForNinjaIcon(p4EndpointId);
// Mute p3's audio and unmute p2's audio. Other endpoints should continue to receive video from p3
// even though p2 is the dominant speaker.
await p3Toolbar.clickAudioMuteButton();
await p2.getToolbar().clickAudioUnmuteButton();
await p1.waitForRemoteVideo(p3EndpointId);
});
});

View File

@@ -1,9 +1,8 @@
import { ensureOneParticipant } from '../../helpers/participants';
describe('Chat Panel - ', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);
});
describe('Chat Panel', () => {
it('join participant', () => ensureOneParticipant(ctx));
it('start closed', async () => {
expect(await ctx.p1.getChatPanel().isOpen()).toBe(false);
});

View File

@@ -4,7 +4,7 @@ import process from 'node:process';
import { ensureOneParticipant } from '../../helpers/participants';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Dial-In - ', () => {
describe('Dial-In', () => {
it('join participant', async () => {
// check rest url is configured
if (!process.env.DIAL_IN_REST_URL) {
@@ -30,7 +30,7 @@ describe('Dial-In - ', () => {
console.error('dial-in.test.no-pin');
}
expect(dialInPin.length >= 9).toBe(true);
expect(dialInPin.length >= 8).toBe(true);
ctx.dialInPin = dialInPin;
});

View File

@@ -1,10 +1,8 @@
import { ensureOneParticipant } from '../../helpers/participants';
import { isDialInEnabled } from '../helpers/DialIn';
describe('Invite - ', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);
});
describe('Invite', () => {
it('join participant', () => ensureOneParticipant(ctx));
it('url displayed', async () => {
const { p1 } = ctx;