mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-18 03:40:19 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d30489cc4 | ||
|
|
7288a6dce4 | ||
|
|
697c41bffa | ||
|
|
0361830122 | ||
|
|
6bb984acc8 | ||
|
|
938d112f6b | ||
|
|
d8cca36181 | ||
|
|
c9add0a9ef | ||
|
|
aabc50da60 | ||
|
|
4aa4382d2b | ||
|
|
6bf1b46609 | ||
|
|
9d7237dc24 | ||
|
|
3745c19857 | ||
|
|
cd6419d826 | ||
|
|
a05e8fd2ef | ||
|
|
239e5743dd | ||
|
|
01a731ab4d | ||
|
|
3d56538c69 | ||
|
|
2dc135b80f | ||
|
|
4e81d4461b | ||
|
|
68b16d7bc8 | ||
|
|
45f6550d30 | ||
|
|
5c65446ef4 | ||
|
|
eee58b2945 | ||
|
|
c7cfb5218d | ||
|
|
b9017176a8 | ||
|
|
cad37306d4 | ||
|
|
83192435e7 | ||
|
|
f85d0e6469 | ||
|
|
3834f1e99c |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -53,6 +53,8 @@ jobs:
|
||||
npm -v
|
||||
- run: npm install
|
||||
- run: make
|
||||
- name: Check config.js syntax
|
||||
run: node config.js
|
||||
android-rn-bundle-build:
|
||||
name: Build mobile bundle (Android)
|
||||
runs-on: macos-15
|
||||
|
||||
@@ -155,7 +155,6 @@ import {
|
||||
NOTIFICATION_TIMEOUT_TYPE
|
||||
} from './react/features/notifications/constants';
|
||||
import { isModerationNotificationDisplayed } from './react/features/notifications/functions';
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay/actions';
|
||||
import { suspendDetected } from './react/features/power-monitor/actions';
|
||||
import { initPrejoin, isPrejoinPageVisible } from './react/features/prejoin/functions';
|
||||
import { disableReceiver, stopReceiver } from './react/features/remote-control/actions';
|
||||
@@ -435,15 +434,6 @@ export default {
|
||||
requestedVideo = true;
|
||||
}
|
||||
|
||||
if (!config.disableInitialGUM) {
|
||||
JitsiMeetJS.mediaDevices.addEventListener(
|
||||
JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
|
||||
browserName =>
|
||||
APP.store.dispatch(
|
||||
mediaPermissionPromptVisibilityChanged(true, browserName))
|
||||
);
|
||||
}
|
||||
|
||||
let tryCreateLocalTracks = Promise.resolve([]);
|
||||
|
||||
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
|
||||
@@ -452,8 +442,7 @@ export default {
|
||||
const timeout = browser.isElectron() ? 15000 : 60000;
|
||||
const audioOptions = {
|
||||
devices: [ MEDIA_TYPE.AUDIO ],
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true
|
||||
timeout
|
||||
};
|
||||
|
||||
// Spot uses the _desktopSharingSourceDevice config option to use an external video input device label as
|
||||
@@ -488,8 +477,7 @@ export default {
|
||||
} else if (requestedAudio || requestedVideo) {
|
||||
tryCreateLocalTracks = APP.store.dispatch(createInitialAVTracks({
|
||||
devices: initialDevices,
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true
|
||||
timeout
|
||||
}, recordTimeMetrics)).then(({ tracks, errors: pErrors }) => {
|
||||
Object.assign(errors, pErrors);
|
||||
|
||||
@@ -497,15 +485,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
// Hide the permissions prompt/overlay as soon as the tracks are created. Don't wait for the connection to
|
||||
// be established, as in some cases like when auth is required, connection won't be established until the user
|
||||
// inputs their credentials, but the dialog would be overshadowed by the overlay.
|
||||
tryCreateLocalTracks.then(tracks => {
|
||||
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
|
||||
|
||||
return tracks;
|
||||
});
|
||||
|
||||
return {
|
||||
tryCreateLocalTracks,
|
||||
errors
|
||||
|
||||
@@ -509,12 +509,17 @@ var config = {
|
||||
// videoQuality: {
|
||||
//
|
||||
// // Provides a way to set the codec preference on desktop based endpoints.
|
||||
// codecPreferenceOrder: [ 'VP9', 'VP8', 'H264' ],
|
||||
// codecPreferenceOrder: [ 'VP9', 'VP8', 'H264', 'AV1' ],
|
||||
//
|
||||
// // Provides a way to set the codec for screenshare.
|
||||
// screenshareCodec: 'AV1',
|
||||
// mobileScreenshareCodec: 'VP8',
|
||||
//
|
||||
// // Enables the adaptive mode in the client that will make runtime adjustments to selected codecs and received
|
||||
// // videos for a better user experience. This mode will kick in only when CPU overuse is reported in the
|
||||
// // WebRTC statistics for the outbound video streams.
|
||||
// enableAdaptiveMode: false,
|
||||
//
|
||||
// // Codec specific settings for scalability modes and max bitrates.
|
||||
// av1: {
|
||||
// maxBitratesVideo: {
|
||||
|
||||
@@ -1726,7 +1726,7 @@ PODS:
|
||||
- React
|
||||
- RNCAsyncStorage (1.23.1):
|
||||
- React-Core
|
||||
- RNCClipboard (1.14.1):
|
||||
- RNCClipboard (1.14.3):
|
||||
- React-Core
|
||||
- RNDefaultPreference (1.4.4):
|
||||
- React-Core
|
||||
@@ -2197,7 +2197,7 @@ SPEC CHECKSUMS:
|
||||
ReactCommon: 6a952e50c2a4b694731d7682aaa6c79bc156e4ad
|
||||
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
|
||||
RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c
|
||||
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
|
||||
RNCClipboard: 2821ac938ef46f736a8de0c8814845dde2dcbdfb
|
||||
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
|
||||
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
|
||||
RNGestureHandler: 939f21fabf5d45a725c0bf175eb819dd25cf2e70
|
||||
@@ -2211,4 +2211,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 7ed908101076ca2c595b633c34648bcfc7e98614
|
||||
|
||||
COCOAPODS: 1.16.1
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1388,7 +1388,7 @@
|
||||
"tileViewToggle": "Kachelansicht ein-/ausschalten",
|
||||
"toggleCamera": "Kamera wechseln",
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"videoSettings": "Kameraeinstellungen",
|
||||
"videoSettings": "Kamera-Einstellungen",
|
||||
"videomute": "Kamera stoppen",
|
||||
"videomuteGUMPending": "Verbinde Ihre Kamera",
|
||||
"videounmute": "Kamera einschalten"
|
||||
|
||||
@@ -483,8 +483,8 @@
|
||||
"transcribing": "Notiek transkripcija",
|
||||
"unlockRoom": "Noņemt $t(lockRoomPassword)",
|
||||
"user": "Lietotājs",
|
||||
"userIdentifier": "Lietotāja identifikators",
|
||||
"userPassword": "lietotāja parole",
|
||||
"userIdentifier": "Lietotājvārds",
|
||||
"userPassword": "Parole",
|
||||
"verifyParticipantConfirm": "Sakrīt",
|
||||
"verifyParticipantDismiss": "Nesakrīt",
|
||||
"verifyParticipantQuestion": "EKSPERIMENTĀLS: pajautājiet dalībniekam {{participantName}}, vai viņš redz to pašu saturu tādā pašā secībā.",
|
||||
@@ -641,6 +641,7 @@
|
||||
"on": "Tiešraide ieslēgta",
|
||||
"onBy": "{{name}} ieslēdza tiešraidi",
|
||||
"pending": "Sākam tiešraidi…",
|
||||
"policyError": "Jūs mēģinājāt pārāk ātri sākt tiešraides straumi. Lūdzu, vēlāk mēģiniet vēlreiz!",
|
||||
"serviceName": "Tiešsaistes pakalpojums",
|
||||
"sessionAlreadyActive": "Šī sesija jau tiek ierakstīta vai straumēta tiešraidē.",
|
||||
"signIn": "Ierakstīties ar Google kontu",
|
||||
@@ -739,6 +740,7 @@
|
||||
"connectedOneMember": "{{name}} ir pievienojies sapulcei",
|
||||
"connectedThreePlusMembers": "{{name}} un {{count}} citi ir pievienojušies sapulcei",
|
||||
"connectedTwoMembers": "{{first}} un {{second}} ir pievienojušies sapulcei",
|
||||
"connectionFailed": "Savienojums neizdevās. Lūdzu, vēlāk mēģiniet vēlreiz!",
|
||||
"dataChannelClosed": "Video kvalitāte var būt traucēta",
|
||||
"dataChannelClosedDescription": "Savienojuma kanāls nedarbojas, tāpēc video kvalitāte var būt ierobežota līdz zemākajam iestatījumam.",
|
||||
"dataChannelClosedDescriptionWithAudio": "Savienojuma kanāls nedarbojas, tāpēc var rasties audio un video traucējumi.",
|
||||
@@ -753,6 +755,9 @@
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Paziņojumi",
|
||||
"hostAskedUnmute": "Moderators vēlas, lai jūs runātu",
|
||||
"invalidTenant": "Nederīgs tenants",
|
||||
"invalidTenantHyphenDescription": "Jūsu izmantotais tenants nav derīgs (sākas vai beidzas ar '-').",
|
||||
"invalidTenantLengthDescription": "Jūsu izmantotais tenants ir pārāk garš.",
|
||||
"invitedOneMember": "{{displayName}} tika uzaicināts",
|
||||
"invitedThreePlusMembers": "Uzaicināts {{name}} un {{count}} citi lietotāji",
|
||||
"invitedTwoMembers": "{{first}} un {{second}} tika uzaicināti uz sapulci",
|
||||
@@ -1051,6 +1056,7 @@
|
||||
"onBy": "{{name}} ieslēdza ierakstu",
|
||||
"onlyRecordSelf": "Ierakstīt tikai manas audio un video straumes",
|
||||
"pending": "Gatavojas ierakstīt sapulci…",
|
||||
"policyError": "Jūs mēģinājāt pārāk ātri sākt ierakstīšanu. Lūdzu, vēlāk mēģiniet vēlreiz!",
|
||||
"recordAudioAndVideo": "Ierakstīt audio un video",
|
||||
"recordTranscription": "Ierakstīt transkripciju",
|
||||
"saveLocalRecording": "Ieraksta faila saglabāšana lokāli (beta)",
|
||||
@@ -1246,6 +1252,7 @@
|
||||
"lobbyButton": "Iespējot/atspējot vestibila režīmu",
|
||||
"localRecording": "Lokālā ieraksta vadības rīki (iesl./izsl.)",
|
||||
"lockRoom": "Telpas slēgšana ar paroli (iesl./izsl.)",
|
||||
"love": "Sirds",
|
||||
"lowerHand": "Nolaist roku",
|
||||
"moreActions": "Papildus iestatījumu izvēlne (rādīt/nerādīt)",
|
||||
"moreActionsMenu": "Papildus iestatījumu izvēlne",
|
||||
@@ -1335,6 +1342,7 @@
|
||||
"lobbyButtonEnable": "Iespējot vestibila režīmu",
|
||||
"login": "Ierakstīties",
|
||||
"logout": "Izrakstīties",
|
||||
"love": "Sirds",
|
||||
"lowerYourHand": "Nolaist roku",
|
||||
"moreActions": "Vairāk rīcību",
|
||||
"moreOptions": "Vairāk opciju",
|
||||
@@ -1360,6 +1368,7 @@
|
||||
"raiseYourHand": "Pacelt roku",
|
||||
"reactionBoo": "Nosūtīt būū reakciju",
|
||||
"reactionClap": "Nosūtīt aplausu reakciju",
|
||||
"reactionHeart": "Nosūtīt sirds reakciju",
|
||||
"reactionLaugh": "Nosūtīt smieklu reakciju",
|
||||
"reactionLike": "Nosūtīt īkšķi augšup reakciju",
|
||||
"reactionSilence": "Nosūtīt klusuma reakciju",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -334,6 +334,7 @@
|
||||
"kickParticipantButton": "Kick",
|
||||
"kickParticipantDialog": "Are you sure you want to kick this participant?",
|
||||
"kickParticipantTitle": "Kick this participant?",
|
||||
"kickSystemTitle": "Ouch! You were kicked out of the meeting",
|
||||
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
|
||||
"linkMeeting": "Link meeting",
|
||||
"linkMeetingTitle": "Link meeting to Salesforce",
|
||||
|
||||
11
modules/API/external/external_api.js
vendored
11
modules/API/external/external_api.js
vendored
@@ -398,6 +398,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
this._participants = {};
|
||||
this._myUserID = undefined;
|
||||
this._onStageParticipant = undefined;
|
||||
this._iAmvisitor = undefined;
|
||||
this._setupListeners();
|
||||
id++;
|
||||
}
|
||||
@@ -619,6 +620,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
email: data.email,
|
||||
avatarURL: data.avatarURL
|
||||
};
|
||||
this._iAmvisitor = data.visitor;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
@@ -1168,6 +1170,15 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have joined as visitor in a meeting.
|
||||
*
|
||||
* @returns {boolean} - Returns true if we have joined as visitor.
|
||||
*/
|
||||
isVisitor() {
|
||||
return this._iAmvisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the avatar URL of a participant.
|
||||
*
|
||||
|
||||
2499
package-lock.json
generated
2499
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -32,7 +32,7 @@
|
||||
"@microsoft/microsoft-graph-client": "3.0.1",
|
||||
"@mui/material": "5.12.1",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-clipboard/clipboard": "1.14.1",
|
||||
"@react-native-clipboard/clipboard": "1.14.3",
|
||||
"@react-native-community/netinfo": "11.1.0",
|
||||
"@react-native-community/slider": "4.4.3",
|
||||
"@react-native-google-signin/google-signin": "10.1.0",
|
||||
@@ -139,12 +139,14 @@
|
||||
"@types/amplitude-js": "8.16.5",
|
||||
"@types/audioworklet": "0.0.29",
|
||||
"@types/dom-screen-wake-lock": "1.0.1",
|
||||
"@types/jasmine": "5.1.4",
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/moment-duration-format": "2.2.6",
|
||||
"@types/offscreencanvas": "2019.7.2",
|
||||
"@types/pixelmatch": "5.2.5",
|
||||
"@types/pretty": "2.0.3",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/react": "17.0.14",
|
||||
"@types/react-dom": "17.0.14",
|
||||
@@ -160,12 +162,12 @@
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@wdio/allure-reporter": "9.2.14",
|
||||
"@wdio/cli": "9.2.14",
|
||||
"@wdio/globals": "9.2.14",
|
||||
"@wdio/jasmine-framework": "9.2.14",
|
||||
"@wdio/junit-reporter": "9.2.14",
|
||||
"@wdio/local-runner": "9.2.15",
|
||||
"@wdio/allure-reporter": "9.4.3",
|
||||
"@wdio/cli": "9.4.3",
|
||||
"@wdio/globals": "9.4.3",
|
||||
"@wdio/junit-reporter": "9.4.3",
|
||||
"@wdio/local-runner": "9.4.3",
|
||||
"@wdio/mocha-framework": "9.4.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-optional-require": "0.3.1",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
@@ -178,16 +180,18 @@
|
||||
"eslint-plugin-react-native": "4.0.0",
|
||||
"eslint-plugin-typescript-sort-keys": "2.3.0",
|
||||
"jetifier": "1.6.4",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"metro-react-native-babel-preset": "0.77.0",
|
||||
"patch-package": "6.4.7",
|
||||
"pretty": "2.0.0",
|
||||
"process": "0.11.10",
|
||||
"sass": "1.26.8",
|
||||
"style-loader": "3.3.1",
|
||||
"traverse": "0.6.6",
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript": "5.0.4",
|
||||
"typescript": "5.7.2",
|
||||
"unorm": "1.6.0",
|
||||
"webdriverio": "9.2.14",
|
||||
"webdriverio": "9.4.3",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
@@ -197,7 +201,7 @@
|
||||
"@xmldom/xmldom": "0.8.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
@@ -215,7 +219,10 @@
|
||||
"tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native",
|
||||
"tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
|
||||
"start": "make dev",
|
||||
"test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts"
|
||||
"test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
|
||||
"test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
|
||||
"test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
|
||||
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.14",
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"peerDependencies": {
|
||||
"@amplitude/react-native": "0.0.0",
|
||||
"@giphy/react-native-sdk": "0.0.0",
|
||||
"@react-native/metro-config": "*",
|
||||
"@react-native-async-storage/async-storage": "0.0.0",
|
||||
"@react-native-clipboard/clipboard": "0.0.0",
|
||||
"@react-native-community/netinfo": "0.0.0",
|
||||
|
||||
22
react-native-sdk/update_sdk_dependencies.js
vendored
22
react-native-sdk/update_sdk_dependencies.js
vendored
@@ -1,9 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const semver = require('semver');
|
||||
|
||||
const packageJSON = require('../package.json');
|
||||
|
||||
const SDKPackageJSON = require('./package.json');
|
||||
|
||||
// Skip checking these.
|
||||
const skipDeps = [ 'react', 'react-native' ];
|
||||
|
||||
/**
|
||||
* Merges the dependency versions from the root package.json with the dependencies of the SDK package.json.
|
||||
*/
|
||||
@@ -18,15 +22,21 @@ function mergeDependencyVersions() {
|
||||
|
||||
// Updates SDK peer dependencies.
|
||||
for (const key in packageJSON.dependencies) {
|
||||
if (SDKPackageJSON.peerDependencies.hasOwnProperty(key)) {
|
||||
|
||||
// Updates all peer dependencies except react and react-native.
|
||||
if (key !== 'react' && key !== 'react-native') {
|
||||
SDKPackageJSON.peerDependencies[key] = packageJSON.dependencies[key];
|
||||
}
|
||||
if (SDKPackageJSON.peerDependencies.hasOwnProperty(key) && !skipDeps.includes(key)) {
|
||||
SDKPackageJSON.peerDependencies[key] = packageJSON.dependencies[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Set RN peer dependency.
|
||||
const rnVersion = semver.parse(packageJSON.dependencies['react-native']);
|
||||
|
||||
if (!rnVersion) {
|
||||
throw new Error('failed to parse React Native version');
|
||||
}
|
||||
|
||||
// In RN the "major" version is the Semver minor.
|
||||
SDKPackageJSON.peerDependencies['react-native'] = `~0.${rnVersion.minor}.0`;
|
||||
|
||||
// Updates SDK overrides dependencies.
|
||||
for (const key in packageJSON.overrides) {
|
||||
if (SDKPackageJSON.overrides.hasOwnProperty(key)) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import AudioMuteButton from './AudioMuteButton';
|
||||
import HangupButton from './HangupButton';
|
||||
import VideoMuteButton from './VideoMuteButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Toolbar}.
|
||||
*/
|
||||
@@ -25,12 +27,89 @@ interface IProps {
|
||||
onMouseOver: (e?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link Toolbar}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* Whether audio button to be shown or not.
|
||||
*/
|
||||
showAudioButton: boolean;
|
||||
|
||||
/**
|
||||
* Whether video button to be shown or not.
|
||||
*/
|
||||
showVideoButton: boolean;
|
||||
}
|
||||
|
||||
type Props = Partial<IProps>;
|
||||
|
||||
/**
|
||||
* Represents the toolbar in the Always On Top window.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
export default class Toolbar extends Component<IProps> {
|
||||
export default class Toolbar extends Component<Props, IState> {
|
||||
/**
|
||||
* Initializes a new {@code Toolbar} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize the new {@code Toolbar} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showAudioButton: true,
|
||||
showVideoButton: true
|
||||
};
|
||||
|
||||
this._videoConferenceJoinedListener = this._videoConferenceJoinedListener.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets listens for changing meetings while showing the toolbar.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
api.on('videoConferenceJoined', this._videoConferenceJoinedListener);
|
||||
|
||||
this._videoConferenceJoinedListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles is visitor changes.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_videoConferenceJoinedListener() {
|
||||
// for electron clients that embed the api and are not updated
|
||||
if (!api.isVisitor) {
|
||||
console.warn('external API not updated');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isNotVisitor = !api.isVisitor();
|
||||
|
||||
this.setState({
|
||||
showAudioButton: isNotVisitor,
|
||||
showVideoButton: isNotVisitor
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
api.removeListener('videoConferenceJoined', this._videoConferenceJoinedListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -49,8 +128,8 @@ export default class Toolbar extends Component<IProps> {
|
||||
className = { `toolbox-content-items always-on-top-toolbox ${className}` }
|
||||
onMouseOut = { onMouseOut }
|
||||
onMouseOver = { onMouseOver }>
|
||||
<AudioMuteButton />
|
||||
<VideoMuteButton />
|
||||
{ this.state.showAudioButton && <AudioMuteButton /> }
|
||||
{ this.state.showVideoButton && <VideoMuteButton /> }
|
||||
<HangupButton customClass = 'hangup-button' />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,6 @@ import '../jaas/reducer';
|
||||
import '../large-video/reducer';
|
||||
import '../lobby/reducer';
|
||||
import '../notifications/reducer';
|
||||
import '../overlay/reducer';
|
||||
import '../participants-pane/reducer';
|
||||
import '../polls/reducer';
|
||||
import '../polls-history/reducer';
|
||||
|
||||
@@ -57,7 +57,6 @@ import { INoAudioSignalState } from '../no-audio-signal/reducer';
|
||||
import { INoiseDetectionState } from '../noise-detection/reducer';
|
||||
import { INoiseSuppressionState } from '../noise-suppression/reducer';
|
||||
import { INotificationsState } from '../notifications/reducer';
|
||||
import { IOverlayState } from '../overlay/reducer';
|
||||
import { IParticipantsPaneState } from '../participants-pane/reducer';
|
||||
import { IPollsState } from '../polls/reducer';
|
||||
import { IPollsHistoryState } from '../polls-history/reducer';
|
||||
@@ -147,7 +146,6 @@ export interface IReduxState {
|
||||
'features/noise-detection': INoiseDetectionState;
|
||||
'features/noise-suppression': INoiseSuppressionState;
|
||||
'features/notifications': INotificationsState;
|
||||
'features/overlay': IOverlayState;
|
||||
'features/participants-pane': IParticipantsPaneState;
|
||||
'features/polls': IPollsState;
|
||||
'features/polls-history': IPollsHistoryState;
|
||||
|
||||
@@ -208,6 +208,7 @@ export function getConferenceOptions(stateful: IStateful) {
|
||||
|
||||
const config = state['features/base/config'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { defaultTranscriptionLanguage } = state['features/dynamic-branding'];
|
||||
const { tenant } = state['features/base/jwt'];
|
||||
const { email, name: nick } = getLocalParticipant(state) ?? {};
|
||||
const options: any = { ...config };
|
||||
@@ -229,7 +230,8 @@ export function getConferenceOptions(stateful: IStateful) {
|
||||
}
|
||||
|
||||
options.applicationName = getName();
|
||||
options.transcriptionLanguage = determineTranscriptionLanguage(options);
|
||||
options.transcriptionLanguage
|
||||
= defaultTranscriptionLanguage ?? determineTranscriptionLanguage(options);
|
||||
|
||||
// Disable analytics, if requested.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
|
||||
@@ -404,6 +404,7 @@ export interface IConfig {
|
||||
guestDialOutStatusUrl?: string;
|
||||
guestDialOutUrl?: string;
|
||||
helpCentreURL?: string;
|
||||
hiddenDomain?: string;
|
||||
hiddenPremeetingButtons?: Array<'microphone' | 'camera' | 'select-background' | 'invite' | 'settings'>;
|
||||
hideAddRoomButton?: boolean;
|
||||
hideConferenceSubject?: boolean;
|
||||
|
||||
@@ -131,7 +131,7 @@ export default [
|
||||
'displayJids',
|
||||
'doNotStoreRoom',
|
||||
'doNotFlipLocalVideo',
|
||||
'dropbox',
|
||||
'dropbox.appKey',
|
||||
'e2eeLabels',
|
||||
'e2ee',
|
||||
'e2eping',
|
||||
@@ -169,14 +169,12 @@ export default [
|
||||
'hideRecordingLabel',
|
||||
'hideParticipantsStats',
|
||||
'hideConferenceTimer',
|
||||
'hiddenDomain',
|
||||
'hideAddRoomButton',
|
||||
'hideEmailInSettings',
|
||||
'hideLobbyButton',
|
||||
'hosts',
|
||||
'iAmRecorder',
|
||||
'iAmSipGateway',
|
||||
'iceTransportPolicy',
|
||||
'ignoreStartMuted',
|
||||
'inviteAppName',
|
||||
'liveStreaming.enabled',
|
||||
@@ -192,7 +190,11 @@ export default [
|
||||
'notificationTimeouts',
|
||||
'openSharedDocumentOnJoin',
|
||||
'opusMaxAverageBitrate',
|
||||
'p2p',
|
||||
'p2p.backToP2PDelay',
|
||||
'p2p.codecPreferenceOrder',
|
||||
'p2p.enabled',
|
||||
'p2p.iceTransportPolicy',
|
||||
'p2p.mobileCodecPreferenceOrder',
|
||||
'participantMenuButtonsWithNotifyClick',
|
||||
'participantsPane',
|
||||
'pcStatsInterval',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
@@ -21,8 +22,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case SET_CONFIG:
|
||||
return _setConfig(store, next, action);
|
||||
|
||||
case SET_DYNAMIC_BRANDING_DATA:
|
||||
return _setDynamicBrandingData(store, next, action);
|
||||
|
||||
case OVERWRITE_CONFIG:
|
||||
return _updateSettings(store, next, action);
|
||||
|
||||
}
|
||||
|
||||
return next(action);
|
||||
@@ -94,6 +99,79 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates config based on dynamic branding data.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} in the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is being {@code dispatch}ed
|
||||
* in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {*} The return value of {@code next(action)}.
|
||||
*/
|
||||
function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const config: IConfig = {};
|
||||
const {
|
||||
downloadAppsUrl,
|
||||
liveStreamingDialogUrls = {},
|
||||
preCallTest = {},
|
||||
salesforceUrl,
|
||||
userDocumentationUrl
|
||||
} = action.value;
|
||||
|
||||
const { helpUrl, termsUrl, dataPrivacyUrl } = liveStreamingDialogUrls;
|
||||
|
||||
if (helpUrl || termsUrl || dataPrivacyUrl) {
|
||||
config.liveStreaming = {};
|
||||
if (helpUrl) {
|
||||
config.liveStreaming.helpLink = helpUrl;
|
||||
}
|
||||
|
||||
if (termsUrl) {
|
||||
config.liveStreaming.termsLink = termsUrl;
|
||||
}
|
||||
|
||||
if (dataPrivacyUrl) {
|
||||
config.liveStreaming.dataPrivacyLink = dataPrivacyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadAppsUrl || userDocumentationUrl) {
|
||||
config.deploymentUrls = {};
|
||||
|
||||
if (downloadAppsUrl) {
|
||||
config.deploymentUrls.downloadAppsUrl = downloadAppsUrl;
|
||||
}
|
||||
|
||||
if (userDocumentationUrl) {
|
||||
config.deploymentUrls.userDocumentationURL = userDocumentationUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (salesforceUrl) {
|
||||
config.salesforceUrl = salesforceUrl;
|
||||
}
|
||||
|
||||
const { enabled, iceUrl } = preCallTest;
|
||||
|
||||
if (typeof enabled === 'boolean') {
|
||||
config.prejoinConfig = {
|
||||
preCallTestEnabled: enabled
|
||||
};
|
||||
}
|
||||
|
||||
if (iceUrl) {
|
||||
config.prejoinConfig = config.prejoinConfig || {};
|
||||
config.prejoinConfig.preCallTestICEUrl = iceUrl;
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings based on some config values.
|
||||
*
|
||||
|
||||
@@ -176,12 +176,6 @@ export const PARTICIPANTS_ENABLED = 'participants.enabled';
|
||||
*/
|
||||
export const PIP_ENABLED = 'pip.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if Picture-in-Picture button should be shown while screen sharing.
|
||||
* Default: disabled (false).
|
||||
*/
|
||||
export const PIP_WHILE_SCREEN_SHARING_ENABLED = 'pip-while-screen-sharing.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the prejoin page should be enabled.
|
||||
* Default: enabled (true).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
|
||||
import { getConferenceState } from '../conference/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes';
|
||||
@@ -28,6 +29,18 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
logger.log('Error setting dynamic language bundle', err);
|
||||
});
|
||||
}
|
||||
|
||||
// Update transcription language, if applicable.
|
||||
if (action.type === SET_DYNAMIC_BRANDING_DATA) {
|
||||
const { defaultTranscriptionLanguage } = action.value;
|
||||
|
||||
if (typeof defaultTranscriptionLanguage !== 'undefined') {
|
||||
const { conference } = getConferenceState(store.getState());
|
||||
|
||||
conference?.setTranscriptionLanguage(defaultTranscriptionLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { PIP_WHILE_SCREEN_SHARING_ENABLED } from '../flags/constants';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { setScreenshareMuted } from '../media/actions';
|
||||
|
||||
import { addLocalTrack, replaceLocalTrack } from './actions.any';
|
||||
import { getLocalDesktopTrack, getTrackState, isLocalVideoTrackDesktop } from './functions.native';
|
||||
import { getLocalDesktopTrack, getTrackState } from './functions.native';
|
||||
|
||||
|
||||
export * from './actions.any';
|
||||
@@ -26,14 +23,9 @@ export function toggleScreensharing(enabled: boolean, _ignore1?: boolean, _ignor
|
||||
const state = getState();
|
||||
|
||||
if (enabled) {
|
||||
const isSharing = isLocalVideoTrackDesktop(state);
|
||||
|
||||
if (!isSharing) {
|
||||
_startScreenSharing(dispatch, state);
|
||||
}
|
||||
_startScreenSharing(dispatch, state);
|
||||
} else {
|
||||
dispatch(setScreenshareMuted(true));
|
||||
setPictureInPictureEnabled(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -47,12 +39,6 @@ export function toggleScreensharing(enabled: boolean, _ignore1?: boolean, _ignor
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _startScreenSharing(dispatch: IStore['dispatch'], state: IReduxState) {
|
||||
const pipWhileScreenSharingEnabled = getFeatureFlag(state, PIP_WHILE_SCREEN_SHARING_ENABLED, false);
|
||||
|
||||
if (!pipWhileScreenSharingEnabled) {
|
||||
setPictureInPictureEnabled(false);
|
||||
}
|
||||
|
||||
try {
|
||||
const tracks: any[] = await JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] });
|
||||
const track = tracks[0];
|
||||
@@ -78,7 +64,5 @@ async function _startScreenSharing(dispatch: IStore['dispatch'], state: IReduxSt
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('ERROR creating screen-sharing stream ', error);
|
||||
|
||||
setPictureInPictureEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,8 +359,7 @@ export function createInitialAVTracks(options: ICreateInitialTracksOptions, reco
|
||||
return (dispatch: IStore['dispatch'], _getState: IStore['getState']) => {
|
||||
const {
|
||||
devices,
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent
|
||||
timeout
|
||||
} = options;
|
||||
|
||||
dispatch(gumPending(devices, IGUMPendingState.PENDING_UNMUTE));
|
||||
@@ -400,16 +399,14 @@ export function createInitialAVTracks(options: ICreateInitialTracksOptions, reco
|
||||
if (devices.includes(MEDIA_TYPE.AUDIO)) {
|
||||
gUMPromises.push(createLocalTracksF({
|
||||
devices: [ MEDIA_TYPE.AUDIO ],
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent
|
||||
timeout
|
||||
}));
|
||||
}
|
||||
|
||||
if (devices.includes(MEDIA_TYPE.VIDEO)) {
|
||||
gUMPromises.push(createLocalTracksF({
|
||||
devices: [ MEDIA_TYPE.VIDEO ],
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent
|
||||
timeout
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@ export * from './functions.any';
|
||||
* @param {string|null} [options.micDeviceId] - Microphone device id or
|
||||
* {@code undefined} to use app's settings.
|
||||
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
|
||||
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
|
||||
* should check for a {@code getUserMedia} permission prompt and fire a
|
||||
* corresponding event.
|
||||
* @param {IStore} store - The redux store in the context of which the function
|
||||
* is to execute and from which state such as {@code config} is to be retrieved.
|
||||
* @returns {Promise<JitsiLocalTrack[]>}
|
||||
|
||||
@@ -32,9 +32,6 @@ export * from './functions.any';
|
||||
* @param {string|null} [options.micDeviceId] - Microphone device id or
|
||||
* {@code undefined} to use app's settings.
|
||||
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
|
||||
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
|
||||
* should check for a {@code getUserMedia} permission prompt and fire a
|
||||
* corresponding event.
|
||||
* @param {IStore} store - The redux store in the context of which the function
|
||||
* is to execute and from which state such as {@code config} is to be retrieved.
|
||||
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
|
||||
@@ -45,7 +42,6 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore,
|
||||
const {
|
||||
desktopSharingSourceDevice,
|
||||
desktopSharingSources,
|
||||
firePermissionPromptIsShownEvent,
|
||||
timeout
|
||||
} = options;
|
||||
|
||||
@@ -91,7 +87,6 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore,
|
||||
effects,
|
||||
facingMode: options.facingMode || getCameraFacingMode(state),
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
firePermissionPromptIsShownEvent,
|
||||
micDeviceId,
|
||||
resolution,
|
||||
timeout
|
||||
@@ -146,7 +141,6 @@ export function createPrejoinTracks() {
|
||||
if (requestedAudio || requestedVideo) {
|
||||
tryCreateLocalTracks = createLocalTracksF({
|
||||
devices: initialDevices,
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
timeout
|
||||
}, APP.store)
|
||||
.catch(async (err: Error) => {
|
||||
@@ -163,7 +157,6 @@ export function createPrejoinTracks() {
|
||||
if (requestedAudio) {
|
||||
gUMPromises.push(createLocalTracksF({
|
||||
devices: [ MEDIA_TYPE.AUDIO ],
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
timeout
|
||||
}));
|
||||
}
|
||||
@@ -171,7 +164,6 @@ export function createPrejoinTracks() {
|
||||
if (requestedVideo) {
|
||||
gUMPromises.push(createLocalTracksF({
|
||||
devices: [ MEDIA_TYPE.VIDEO ],
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
timeout
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface ITrackOptions {
|
||||
desktopSharingSources?: string[];
|
||||
devices?: string[];
|
||||
facingMode?: string;
|
||||
firePermissionPromptIsShownEvent?: boolean;
|
||||
micDeviceId?: string | null;
|
||||
timeout?: number;
|
||||
}
|
||||
@@ -76,7 +75,6 @@ export interface IShareOptions {
|
||||
|
||||
export interface ICreateInitialTracksOptions {
|
||||
devices: Array<MediaType>;
|
||||
firePermissionPromptIsShownEvent?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const IconButton: React.FC<IIconButtonProps> = ({
|
||||
underlayColor = { underlayColor }>
|
||||
<Icon
|
||||
color = { color }
|
||||
size = { 20 || size }
|
||||
size = { size ?? 20 }
|
||||
src = { src } />
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
*/
|
||||
export function notifyKickedOut(participant: any, submit?: Function) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (!participant || participant?.isReplaced?.()) {
|
||||
if (participant?.isReplaced?.()) {
|
||||
submit?.();
|
||||
|
||||
return;
|
||||
@@ -24,9 +24,9 @@ export function notifyKickedOut(participant: any, submit?: Function) {
|
||||
|
||||
dispatch(openDialog(AlertDialog, {
|
||||
contentKey: {
|
||||
key: 'dialog.kickTitle',
|
||||
key: participant ? 'dialog.kickTitle' : 'dialog.kickSystemTitle',
|
||||
params: {
|
||||
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
|
||||
participantDisplayName: participant && getParticipantDisplayName(getState, participant.getId())
|
||||
}
|
||||
},
|
||||
onSubmit: submit
|
||||
|
||||
@@ -27,12 +27,17 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch } = store;
|
||||
const { participant } = action;
|
||||
|
||||
// we need to first finish dispatching or the notification can be cleared out
|
||||
const result = next(action);
|
||||
|
||||
const participantDisplayName
|
||||
= getParticipantDisplayName(store.getState, participant.getId());
|
||||
= participant && getParticipantDisplayName(store.getState, participant.getId());
|
||||
|
||||
dispatch(hangup(true, i18next.t('dialog.kickTitle', { participantDisplayName })));
|
||||
dispatch(hangup(true,
|
||||
participantDisplayName ? i18next.t('dialog.kickTitle', { participantDisplayName })
|
||||
: i18next.t('dialog.kickSystemTitle')));
|
||||
|
||||
break;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@ const StageParticipantNameLabel = () => {
|
||||
classes.badgeContainer,
|
||||
toolboxVisible && classes.containerElevated,
|
||||
_isScreenShareParticipant && classes.screenSharing
|
||||
) }>
|
||||
) }
|
||||
data-testid = 'stage-display-name' >
|
||||
<DisplayNameBadge name = { nameToDisplay } />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
didPageUrl,
|
||||
downloadAppsUrl,
|
||||
inviteDomain,
|
||||
labels,
|
||||
liveStreamingDialogUrls,
|
||||
salesforceUrl,
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl
|
||||
supportUrl,
|
||||
userDocumentationUrl
|
||||
} = action.value;
|
||||
|
||||
action.value = {
|
||||
@@ -35,10 +39,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
didPageUrl,
|
||||
downloadAppsUrl,
|
||||
inviteDomain,
|
||||
labels,
|
||||
liveStreamingDialogUrls,
|
||||
salesforceUrl,
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl
|
||||
supportUrl,
|
||||
userDocumentationUrl
|
||||
};
|
||||
|
||||
// The backend may send an empty string, make sure we skip that.
|
||||
|
||||
@@ -150,6 +150,7 @@ export interface IDynamicBrandingState {
|
||||
customizationFailed: boolean;
|
||||
customizationReady: boolean;
|
||||
defaultBranding: boolean;
|
||||
defaultTranscriptionLanguage?: boolean;
|
||||
didPageUrl: string;
|
||||
inviteDomain: string;
|
||||
labels: Object | null;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getBaseUrl } from '../base/util/helpers';
|
||||
import { appendSuffix } from '../display-name/functions';
|
||||
import { SUBMIT_FEEDBACK_ERROR, SUBMIT_FEEDBACK_SUCCESS } from '../feedback/actionTypes';
|
||||
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip/actionTypes';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import './subscriber';
|
||||
|
||||
@@ -120,7 +121,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
),
|
||||
avatarURL: loadableAvatarUrl,
|
||||
breakoutRoom,
|
||||
email
|
||||
email,
|
||||
visitor: iAmVisitor(state)
|
||||
}
|
||||
);
|
||||
break;
|
||||
@@ -151,8 +153,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
local: true
|
||||
},
|
||||
{
|
||||
id: actor.getId(),
|
||||
name: actor.getDisplayName()
|
||||
id: actor?.getId(),
|
||||
name: actor?.getDisplayName()
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { PIP_WHILE_SCREEN_SHARING_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconArrowDown } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
|
||||
import { enterPictureInPicture } from '../actions';
|
||||
import { isPipEnabled } from '../functions';
|
||||
|
||||
@@ -58,9 +55,7 @@ class PictureInPictureButton extends AbstractButton<IProps> {
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const pipEnabled = isPipEnabled(state);
|
||||
const pipWhileScreenSharingEnabled = getFeatureFlag(state, PIP_WHILE_SCREEN_SHARING_ENABLED, false);
|
||||
const enabled = pipEnabled && (!isLocalVideoTrackDesktop(state) || pipWhileScreenSharingEnabled);
|
||||
const enabled = isPipEnabled(state);
|
||||
|
||||
return {
|
||||
_enabled: enabled
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* The type of the Redux action which signals that the prompt for media
|
||||
* permission is visible or not.
|
||||
*
|
||||
* {
|
||||
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
* isVisible: {boolean},
|
||||
* browser: {string}
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED
|
||||
= 'MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED';
|
||||
@@ -3,23 +3,6 @@ import { openDialog } from '../base/dialog/actions';
|
||||
import PageReloadDialog from '../base/dialog/components/native/PageReloadDialog';
|
||||
|
||||
|
||||
/**
|
||||
* Signals that the prompt for media permission is visible or not.
|
||||
*
|
||||
* @param {boolean} _isVisible - If the value is true - the prompt for media
|
||||
* permission is visible otherwise the value is false/undefined.
|
||||
* @param {string} _browser - The name of the current browser.
|
||||
* @public
|
||||
* @returns {{
|
||||
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
* browser: {string},
|
||||
* isVisible: {boolean}
|
||||
* }}
|
||||
*/
|
||||
export function mediaPermissionPromptVisibilityChanged(_isVisible: boolean, _browser: string) {
|
||||
// Dummy.
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@link PageReloadDialog}.
|
||||
*
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes';
|
||||
|
||||
|
||||
/**
|
||||
* Signals that the prompt for media permission is visible or not.
|
||||
*
|
||||
* @param {boolean} isVisible - If the value is true - the prompt for media
|
||||
* permission is visible otherwise the value is false/undefined.
|
||||
* @param {string} browser - The name of the current browser.
|
||||
* @public
|
||||
* @returns {{
|
||||
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
* browser: {string},
|
||||
* isVisible: {boolean}
|
||||
* }}
|
||||
*/
|
||||
export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, browser: string) {
|
||||
return {
|
||||
type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
browser,
|
||||
isVisible
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@link PageReloadDialog}.
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate, translateToHTML } from '../../../base/i18n/functions';
|
||||
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link UserMediaPermissionsOverlay}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
_premeetingBackground?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React Component for overlay with guidance how to proceed with
|
||||
* gUM prompt.
|
||||
*/
|
||||
class UserMediaPermissionsOverlay extends Component<IProps> {
|
||||
/**
|
||||
* Determines whether this overlay needs to be rendered (according to a
|
||||
* specific redux state). Called by {@link OverlayContainer}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - If this overlay needs to be rendered, {@code true};
|
||||
* {@code false}, otherwise.
|
||||
*/
|
||||
static needsRender(state: IReduxState) {
|
||||
return state['features/overlay'].isMediaPermissionPromptVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _premeetingBackground, t } = this.props;
|
||||
const style = _premeetingBackground ? {
|
||||
background: _premeetingBackground,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover'
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<OverlayFrame style = { style }>
|
||||
<div className = 'inlay'>
|
||||
<span className = 'inlay__icon icon-microphone' />
|
||||
<span className = 'inlay__icon icon-camera' />
|
||||
<h3
|
||||
aria-label = { t('startupoverlay.genericTitle') }
|
||||
className = 'inlay__title'
|
||||
role = 'alert' >
|
||||
{
|
||||
t('startupoverlay.genericTitle')
|
||||
}
|
||||
</h3>
|
||||
<span
|
||||
className = 'inlay__text'
|
||||
role = 'alert' >
|
||||
{
|
||||
t('userMedia.grantPermissions')
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className = 'policy overlay__policy'>
|
||||
<p
|
||||
className = 'policy__text'
|
||||
role = 'alert'>
|
||||
{ translateToHTML(t, 'startupoverlay.policyText') }
|
||||
</p>
|
||||
{
|
||||
this._renderPolicyLogo()
|
||||
}
|
||||
</div>
|
||||
</OverlayFrame>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the policy logo.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
_renderPolicyLogo() {
|
||||
const policyLogoSrc = interfaceConfig.POLICY_LOGO;
|
||||
|
||||
if (policyLogoSrc) {
|
||||
return (
|
||||
<div className = 'policy__logo'>
|
||||
<img
|
||||
alt = { this.props.t('welcomepage.logo.policyLogo') }
|
||||
src = { policyLogoSrc } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The props passed to the component.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { premeetingBackground } = state['features/dynamic-branding'];
|
||||
|
||||
return {
|
||||
_premeetingBackground: premeetingBackground
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(UserMediaPermissionsOverlay));
|
||||
@@ -2,7 +2,6 @@ import { IReduxState } from '../app/types';
|
||||
|
||||
import PageReloadOverlay from './components/web/PageReloadOverlay';
|
||||
import SuspendedOverlay from './components/web/SuspendedOverlay';
|
||||
import UserMediaPermissionsOverlay from './components/web/UserMediaPermissionsOverlay';
|
||||
|
||||
/**
|
||||
* Returns the overlay to be currently rendered.
|
||||
@@ -13,8 +12,7 @@ import UserMediaPermissionsOverlay from './components/web/UserMediaPermissionsOv
|
||||
export function getOverlayToRender(state: IReduxState) {
|
||||
const overlays = [
|
||||
PageReloadOverlay,
|
||||
SuspendedOverlay,
|
||||
UserMediaPermissionsOverlay
|
||||
SuspendedOverlay
|
||||
];
|
||||
|
||||
for (const overlay of overlays) {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { assign } from '../base/redux/functions';
|
||||
|
||||
import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes';
|
||||
|
||||
|
||||
export interface IOverlayState {
|
||||
browser?: string;
|
||||
isMediaPermissionPromptVisible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the redux actions of the feature overlay.
|
||||
*
|
||||
* FIXME: these pieces of state should probably be in a different place.
|
||||
*/
|
||||
ReducerRegistry.register<IOverlayState>('features/overlay', (state = {}, action): IOverlayState => {
|
||||
switch (action.type) {
|
||||
case MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED:
|
||||
return _mediaPermissionPromptVisibilityChanged(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Reduces a specific redux action MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED of
|
||||
* the feature overlay.
|
||||
*
|
||||
* @param {Object} state - The redux state of the feature overlay.
|
||||
* @param {Action} action - The redux action to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature overlay after the reduction of
|
||||
* the specified action.
|
||||
*/
|
||||
function _mediaPermissionPromptVisibilityChanged(
|
||||
state: IOverlayState,
|
||||
{ browser, isVisible }: { browser?: string; isVisible?: boolean; }) {
|
||||
return assign(state, {
|
||||
browser,
|
||||
isMediaPermissionPromptVisible: isVisible
|
||||
});
|
||||
}
|
||||
@@ -274,7 +274,6 @@ export function showStartedRecordingNotification(
|
||||
if (recordingSharingUrl
|
||||
&& isVpaasMeeting(state)
|
||||
&& iAmRecordingInitiator
|
||||
&& showRecordingLink
|
||||
&& !isSavingRecordingOnDropbox(state)) {
|
||||
const region = getMeetingRegion(state);
|
||||
const tenant = getVpaasTenant(state);
|
||||
@@ -288,15 +287,17 @@ export function showStartedRecordingNotification(
|
||||
}
|
||||
|
||||
// add the option to copy recording link
|
||||
notifyProps.dialogProps = {
|
||||
...notifyProps.dialogProps,
|
||||
customActionNameKey: [ 'recording.copyLink' ],
|
||||
customActionHandler: [ () => copyText(link) ],
|
||||
titleKey: 'recording.on',
|
||||
descriptionKey: 'recording.linkGenerated'
|
||||
};
|
||||
if (showRecordingLink) {
|
||||
notifyProps.dialogProps = {
|
||||
...notifyProps.dialogProps,
|
||||
customActionNameKey: [ 'recording.copyLink' ],
|
||||
customActionHandler: [ () => copyText(link) ],
|
||||
titleKey: 'recording.on',
|
||||
descriptionKey: 'recording.linkGenerated'
|
||||
};
|
||||
|
||||
notifyProps.type = NOTIFICATION_TIMEOUT_TYPE.STICKY;
|
||||
notifyProps.type = NOTIFICATION_TIMEOUT_TYPE.STICKY;
|
||||
}
|
||||
} catch (err) {
|
||||
dispatch(showErrorNotification({
|
||||
titleKey: 'recording.errorFetchingLink'
|
||||
|
||||
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
||||
// @ts-expect-error
|
||||
import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
|
||||
import { getToolboxHeight } from '../../../toolbox/functions.web';
|
||||
import { isSharedVideoEnabled } from '../../functions';
|
||||
@@ -39,11 +38,6 @@ interface IProps {
|
||||
*/
|
||||
isEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Is the video shared by the local user.
|
||||
*/
|
||||
isOwner: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the user is actively resizing the filmstrip.
|
||||
*/
|
||||
@@ -124,17 +118,15 @@ class SharedVideo extends Component<IProps> {
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
render() {
|
||||
const { isEnabled, isOwner, isResizing } = this.props;
|
||||
const { isEnabled, isResizing } = this.props;
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const className = !isResizing && isOwner ? '' : 'disable-pointer';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
className = { (isResizing && 'disable-pointer') || '' }
|
||||
id = 'sharedVideo'
|
||||
style = { this.getDimensions() }>
|
||||
{this.getManager()}
|
||||
@@ -152,19 +144,16 @@ class SharedVideo extends Component<IProps> {
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { ownerId, videoUrl } = state['features/shared-video'];
|
||||
const { videoUrl } = state['features/shared-video'];
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { visible, isResizing } = state['features/filmstrip'];
|
||||
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
clientHeight,
|
||||
clientWidth,
|
||||
filmstripVisible: visible,
|
||||
filmstripWidth: getVerticalViewMaxWidth(state),
|
||||
isEnabled: isSharedVideoEnabled(state),
|
||||
isOwner: ownerId === localParticipant?.id,
|
||||
isResizing,
|
||||
videoUrl
|
||||
};
|
||||
|
||||
@@ -196,7 +196,7 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
|
||||
// Regex to filter out all possible country codes after language code:
|
||||
// this should catch all notations like 'en-GB' 'en_GB' and 'enGB'
|
||||
// and be independent of the country code length
|
||||
if (json.language.replace(/[-_A-Z].*/, '') !== language) {
|
||||
if (_getPrimaryLanguageCode(json.language) !== _getPrimaryLanguageCode(language)) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -260,6 +260,17 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to extract the primary language code like 'en-GB' 'en_GB'
|
||||
* 'enGB' 'zh-CN' and 'zh-TW'.
|
||||
*
|
||||
* @param {string} language - The language to use for translation or user requested.
|
||||
* @returns {string}
|
||||
*/
|
||||
function _getPrimaryLanguageCode(language: string) {
|
||||
return language.replace(/[-_A-Z].*/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the local property 'requestingTranscription'. This will cause Jicofo
|
||||
* and Jigasi to decide whether the transcriber needs to be in the room.
|
||||
|
||||
@@ -10,6 +10,7 @@ import styles from './styles';
|
||||
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
backgroundColor?: string;
|
||||
icon: any;
|
||||
id?: string;
|
||||
text: string;
|
||||
@@ -21,6 +22,7 @@ interface IProps extends AbstractButtonProps {
|
||||
* @returns {Component}
|
||||
*/
|
||||
class CustomOptionButton extends AbstractButton<IProps> {
|
||||
backgroundColor = this.props.backgroundColor;
|
||||
iconSrc = this.props.icon;
|
||||
id = this.props.id;
|
||||
text = this.props.text;
|
||||
@@ -48,8 +50,7 @@ class CustomOptionButton extends AbstractButton<IProps> {
|
||||
= (
|
||||
<Image
|
||||
source = {{ uri: this.iconSrc }}
|
||||
style = { styles.iconImageStyles }
|
||||
tintColor = { 'white' } />);
|
||||
style = { styles.iconImageStyles } />);
|
||||
}
|
||||
|
||||
return iconComponent;
|
||||
|
||||
@@ -228,11 +228,10 @@ class OverflowMenu extends PureComponent<IProps, IState> {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
_customToolbarButtons.map(({ id, text, icon, ...rest }) => (
|
||||
_customToolbarButtons.map(({ id, text, icon, backgroundColor }) => (
|
||||
<CustomOptionButton
|
||||
{ ...rest }
|
||||
{ ...topButtonProps }
|
||||
|
||||
backgroundColor = { backgroundColor }
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
handleClick = { () =>
|
||||
dispatch(customOverflowMenuButtonPressed(id, text))
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IReduxState } from '../app/types';
|
||||
import { isJwtFeatureEnabledStateless } from '../base/jwt/functions';
|
||||
import { IGUMPendingState } from '../base/media/types';
|
||||
import { IParticipantFeatures } from '../base/participants/types';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
/**
|
||||
* Indicates if the audio mute button is disabled or not.
|
||||
@@ -13,7 +14,8 @@ export function isAudioMuteButtonDisabled(state: IReduxState) {
|
||||
const { available, muted, unmuteBlocked, gumPending } = state['features/base/media'].audio;
|
||||
const { startSilent } = state['features/base/config'];
|
||||
|
||||
return Boolean(!available || startSilent || (muted && unmuteBlocked) || gumPending !== IGUMPendingState.NONE);
|
||||
return Boolean(!available || startSilent || (muted && unmuteBlocked) || gumPending !== IGUMPendingState.NONE
|
||||
|| iAmVisitor(state));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,7 +52,7 @@ const hasCollabDetails = (state: IReduxState): boolean => Boolean(
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardEnabled = (state: IReduxState): boolean =>
|
||||
(getWhiteboardConfig(state).enabled || hasCollabDetails(state))
|
||||
(getWhiteboardConfig(state).enabled ?? hasCollabDetails(state))
|
||||
&& getWhiteboardConfig(state).collabServerBaseUrl
|
||||
&& getCurrentConference(state)?.getMetadataHandler()
|
||||
?.isSupported();
|
||||
|
||||
@@ -141,7 +141,7 @@ module:hook('muc-occupant-pre-join', function (event)
|
||||
else
|
||||
occupant.role = 'visitor';
|
||||
end
|
||||
elseif room.moderators_list:contains(resource) then
|
||||
elseif room.moderators_list and room.moderators_list:contains(resource) then
|
||||
-- remote participants, host is the main prosody
|
||||
occupant.role = 'moderator';
|
||||
end
|
||||
|
||||
@@ -115,9 +115,10 @@ function handle_kick_participant (event)
|
||||
end
|
||||
|
||||
local number = params["number"];
|
||||
local participantId = params["participantId"];
|
||||
|
||||
if not number then
|
||||
module:log("warn", "Missing number param");
|
||||
if (not number and not participantId) or (number and participantId) then
|
||||
module:log("warn", "Invalid parameters: exactly one of 'number' or 'participantId' must be provided.");
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
@@ -134,10 +135,8 @@ function handle_kick_participant (event)
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
local pr = occupant:get_presence();
|
||||
local displayName = pr:get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
|
||||
if is_sip_jigasi(pr) and displayName and starts_with(displayName, number) then
|
||||
if is_participant_match(pr, number, participantId) then
|
||||
room:set_role(true, occupant.nick, nil);
|
||||
module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid);
|
||||
return { status_code = 200; }
|
||||
@@ -148,6 +147,20 @@ function handle_kick_participant (event)
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
function is_participant_match(pr, number, participantId)
|
||||
if number then
|
||||
local displayName = pr:get_child_text('nick', 'http://jabber.org/protocol/nick');
|
||||
return is_sip_jigasi(pr) and displayName and starts_with(displayName, number);
|
||||
elseif participantId then
|
||||
local from = pr.attr.from;
|
||||
local _, _, from_resource = jid.split(from);
|
||||
if from_resource then
|
||||
return from_resource == participantId;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
module:log("info","Adding http handler for /kick-participant on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
@@ -115,7 +115,8 @@ module:hook("message/bare", function(event)
|
||||
|
||||
if room.polls.by_id[data.pollId] ~= nil then
|
||||
module:log("error", "Poll already exists: %s", data.pollId);
|
||||
return;
|
||||
event.origin.send(st.error_reply(event.stanza, 'cancel', 'not-allowed', 'Poll already exists'));
|
||||
return true;
|
||||
end
|
||||
|
||||
local answers = {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
|
||||
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
|
||||
#BASE_URL=
|
||||
|
||||
# To be able to match a domain to a specific address
|
||||
@@ -13,3 +14,19 @@
|
||||
|
||||
# The path to the browser video capture file
|
||||
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
|
||||
|
||||
# The path to the helper iframe page that will be used for the iframeAPI tests
|
||||
#IFRAME_PAGE_BASE=
|
||||
|
||||
# The grid host url (https://mygrid.com/wd/hub)
|
||||
#GRID_HOST_URL=
|
||||
|
||||
# The path to the private key used for generating JWT token (.pk)
|
||||
#JWT_PRIVATE_KEY_PATH=
|
||||
# The kid to use in the token
|
||||
#JWT_KID=
|
||||
|
||||
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
|
||||
#WEBHOOKS_PROXY_URL=
|
||||
# A shared secret to authenticate the webhook proxy connection
|
||||
#WEBHOOKS_PROXY_SHARED_SECRET=
|
||||
|
||||
5
tests/globals.d.ts
vendored
Normal file
5
tests/globals.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { IContext } from './helpers/types';
|
||||
|
||||
declare global {
|
||||
const ctx: IContext;
|
||||
}
|
||||
@@ -5,10 +5,15 @@ import { multiremotebrowser } from '@wdio/globals';
|
||||
import { IConfig } from '../../react/features/base/config/configType';
|
||||
import { urlObjectToString } from '../../react/features/base/util/uri';
|
||||
import Filmstrip from '../pageobjects/Filmstrip';
|
||||
import IframeAPI from '../pageobjects/IframeAPI';
|
||||
import Notifications from '../pageobjects/Notifications';
|
||||
import ParticipantsPane from '../pageobjects/ParticipantsPane';
|
||||
import SettingsDialog from '../pageobjects/SettingsDialog';
|
||||
import Toolbar from '../pageobjects/Toolbar';
|
||||
import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
|
||||
|
||||
import { LOG_PREFIX, logInfo } from './browserLogger';
|
||||
import { IContext } from './participants';
|
||||
import { IContext, IJoinOptions } from './types';
|
||||
|
||||
/**
|
||||
* Participant.
|
||||
@@ -19,9 +24,9 @@ export class Participant {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private context: { roomName: string; };
|
||||
private _name: string;
|
||||
private _endpointId: string;
|
||||
private _jwt?: string;
|
||||
|
||||
/**
|
||||
* The default config to use when joining.
|
||||
@@ -59,9 +64,11 @@ export class Participant {
|
||||
* Creates a participant with given name.
|
||||
*
|
||||
* @param {string} name - The name of the participant.
|
||||
* @param {string }jwt - The jwt if any.
|
||||
*/
|
||||
constructor(name: string) {
|
||||
constructor(name: string, jwt?: string) {
|
||||
this._name = name;
|
||||
this._jwt = jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +76,7 @@ export class Participant {
|
||||
*
|
||||
* @returns {Promise<string>} The endpoint ID.
|
||||
*/
|
||||
async getEndpointId() {
|
||||
async getEndpointId(): Promise<string> {
|
||||
if (!this._endpointId) {
|
||||
this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
|
||||
return APP.conference.getMyUserId();
|
||||
@@ -99,40 +106,70 @@ export class Participant {
|
||||
* @param {string} message - The message to log.
|
||||
* @returns {void}
|
||||
*/
|
||||
log(message: string) {
|
||||
log(message: string): void {
|
||||
logInfo(this.driver, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins conference.
|
||||
*
|
||||
* @param {IContext} context - The context.
|
||||
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
|
||||
* @param {IContext} ctx - The context.
|
||||
* @param {IJoinOptions} options - Options for joining.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async joinConference(context: IContext, skipInMeetingChecks = false) {
|
||||
this.context = context;
|
||||
|
||||
const url = urlObjectToString({
|
||||
room: context.roomName,
|
||||
async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
const config = {
|
||||
room: ctx.roomName,
|
||||
configOverwrite: this.config,
|
||||
interfaceConfigOverwrite: {
|
||||
SHOW_CHROME_EXTENSION_BANNER: false
|
||||
},
|
||||
userInfo: {
|
||||
displayName: this._name
|
||||
}
|
||||
}) || '';
|
||||
};
|
||||
|
||||
if (!options.skipDisplayName) {
|
||||
// @ts-ignore
|
||||
config.userInfo = {
|
||||
displayName: this._name
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
config.room = 'iframeAPITest.html';
|
||||
}
|
||||
|
||||
let url = urlObjectToString(config) || '';
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
const baseUrl = new URL(this.driver.options.baseUrl || '');
|
||||
|
||||
// @ts-ignore
|
||||
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
|
||||
|
||||
if (baseUrl.pathname.length > 1) {
|
||||
// remove leading slash
|
||||
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
|
||||
}
|
||||
}
|
||||
if (this._jwt) {
|
||||
url = `${url}&jwt="${this._jwt}"`;
|
||||
}
|
||||
|
||||
await this.driver.setTimeout({ 'pageLoad': 30000 });
|
||||
|
||||
await this.driver.url(url);
|
||||
// drop the leading '/' so we can use the tenant if any
|
||||
await this.driver.url(url.startsWith('/') ? url.substring(1) : url);
|
||||
|
||||
await this.waitForPageToLoad();
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
const mainFrame = this.driver.$('iframe');
|
||||
|
||||
await this.driver.switchFrame(mainFrame);
|
||||
}
|
||||
|
||||
await this.waitToJoinMUC();
|
||||
|
||||
await this.postLoadProcess(skipInMeetingChecks);
|
||||
await this.postLoadProcess(options.skipInMeetingChecks);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +179,7 @@ export class Participant {
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private async postLoadProcess(skipInMeetingChecks: boolean) {
|
||||
private async postLoadProcess(skipInMeetingChecks = false): Promise<void> {
|
||||
const driver = this.driver;
|
||||
|
||||
const parallel = [];
|
||||
@@ -189,9 +226,9 @@ export class Participant {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForPageToLoad() {
|
||||
async waitForPageToLoad(): Promise<void> {
|
||||
return this.driver.waitUntil(
|
||||
() => this.driver.execute(() => document.readyState === 'complete'),
|
||||
async () => await this.driver.execute(() => document.readyState === 'complete'),
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
|
||||
@@ -199,14 +236,29 @@ export class Participant {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the participant is in the meeting.
|
||||
*/
|
||||
async isInMuc() {
|
||||
return await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the participant is a moderator in the meeting.
|
||||
*/
|
||||
async isModerator() {
|
||||
return await this.driver.execute(() => typeof APP !== 'undefined'
|
||||
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits to join the muc.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitToJoinMUC() {
|
||||
async waitToJoinMUC(): Promise<void> {
|
||||
return this.driver.waitUntil(
|
||||
() => this.driver.execute(() => APP.conference.isJoined()),
|
||||
() => this.isInMuc(),
|
||||
{
|
||||
timeout: 10_000, // 10 seconds
|
||||
timeoutMsg: 'Timeout waiting to join muc.'
|
||||
@@ -219,11 +271,11 @@ export class Participant {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForIceConnected() {
|
||||
async waitForIceConnected(): Promise<void> {
|
||||
const driver = this.driver;
|
||||
|
||||
return driver.waitUntil(async () =>
|
||||
driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
|
||||
await driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: 'expected ICE to be connected for 15s'
|
||||
});
|
||||
@@ -234,11 +286,11 @@ export class Participant {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForSendReceiveData() {
|
||||
async waitForSendReceiveData(): Promise<void> {
|
||||
const driver = this.driver;
|
||||
|
||||
return driver.waitUntil(async () =>
|
||||
driver.execute(() => {
|
||||
await driver.execute(() => {
|
||||
const stats = APP.conference.getStats();
|
||||
const bitrateMap = stats?.bitrate || {};
|
||||
const rtpStats = {
|
||||
@@ -259,11 +311,11 @@ export class Participant {
|
||||
* @param {number} number - The number of remote streams o wait for.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
waitForRemoteStreams(number: number) {
|
||||
waitForRemoteStreams(number: number): Promise<void> {
|
||||
const driver = this.driver;
|
||||
|
||||
return driver.waitUntil(async () =>
|
||||
driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
|
||||
await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: 'expected remote streams in 15s'
|
||||
});
|
||||
@@ -274,7 +326,7 @@ export class Participant {
|
||||
*
|
||||
* @returns {Toolbar}
|
||||
*/
|
||||
getToolbar() {
|
||||
getToolbar(): Toolbar {
|
||||
return new Toolbar(this);
|
||||
}
|
||||
|
||||
@@ -283,7 +335,172 @@ export class Participant {
|
||||
*
|
||||
* @returns {Filmstrip}
|
||||
*/
|
||||
getFilmstrip() {
|
||||
getFilmstrip(): Filmstrip {
|
||||
return new Filmstrip(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notifications.
|
||||
*/
|
||||
getNotifications(): Notifications {
|
||||
return new Notifications(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the participants pane.
|
||||
*
|
||||
* @returns {ParticipantsPane}
|
||||
*/
|
||||
getParticipantsPane(): ParticipantsPane {
|
||||
return new ParticipantsPane(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the videoQuality Dialog.
|
||||
*
|
||||
* @returns {VideoQualityDialog}
|
||||
*/
|
||||
getVideoQualityDialog(): VideoQualityDialog {
|
||||
return new VideoQualityDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the settings Dialog.
|
||||
*
|
||||
* @returns {SettingsDialog}
|
||||
*/
|
||||
getSettingsDialog(): SettingsDialog {
|
||||
return new SettingsDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the iframe API context
|
||||
*/
|
||||
async switchToAPI() {
|
||||
await this.driver.switchFrame(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the meeting page context.
|
||||
*/
|
||||
async switchInPage() {
|
||||
const mainFrame = this.driver.$('iframe');
|
||||
|
||||
await this.driver.switchFrame(mainFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the iframe API for this participant.
|
||||
*/
|
||||
getIframeAPI() {
|
||||
return new IframeAPI(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hangups the participant by leaving the page. base.html is an empty page on all deployments.
|
||||
*/
|
||||
async hangup() {
|
||||
await this.driver.url('/base.html');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local display name.
|
||||
*/
|
||||
async getLocalDisplayName() {
|
||||
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
|
||||
|
||||
await localVideoContainer.moveTo();
|
||||
|
||||
const localDisplayName = localVideoContainer.$('span[id="localDisplayName"]');
|
||||
|
||||
return await localDisplayName.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets avatar SRC attribute for the one displayed on local video thumbnail.
|
||||
*/
|
||||
async getLocalVideoAvatar() {
|
||||
const avatar
|
||||
= this.driver.$('//span[@id="localVideoContainer"]//img[contains(@class,"userAvatar")]');
|
||||
|
||||
return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets avatar SRC attribute for the one displayed on large video.
|
||||
*/
|
||||
async getLargeVideoAvatar() {
|
||||
const avatar = this.driver.$('//img[@id="dominantSpeakerAvatar"]');
|
||||
|
||||
return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns resource part of the JID of the user who is currently displayed in the large video area.
|
||||
*/
|
||||
async getLargeVideoResource() {
|
||||
return await this.driver.execute(() => APP.UI.getLargeVideoID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
|
||||
* There are 3 options for avatar:
|
||||
* - defaultAvatar: true - the default avatar (with grey figure) is used
|
||||
* - image: true - the avatar is an image set in the settings
|
||||
* - defaultAvatar: false, image: false - the avatar is produced from the initials of the display name
|
||||
*/
|
||||
async assertThumbnailShowsAvatar(
|
||||
participant: Participant, reverse = false, defaultAvatar = false, image = false): Promise<void> {
|
||||
const id = participant === this
|
||||
? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
|
||||
|
||||
const xpath = defaultAvatar
|
||||
? `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`
|
||||
: `//span[@id="${id}"]//${image ? 'img' : 'div'}[contains(@class,"userAvatar")]`;
|
||||
|
||||
await this.driver.$(xpath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Avatar is ${reverse ? '' : 'not'} displayed in the local thumbnail for ${participant.name}`
|
||||
});
|
||||
|
||||
await this.driver.$(`//span[@id="${id}"]//video`).waitForDisplayed({
|
||||
reverse: !reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Video is ${reverse ? 'not' : ''} displayed in the local thumbnail for ${participant.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the default avatar is used.
|
||||
*/
|
||||
async assertDefaultAvatarExist(participant: Participant): Promise<void> {
|
||||
const id = participant === this
|
||||
? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
|
||||
|
||||
await this.driver.$(
|
||||
`//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`)
|
||||
.waitForExist({
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Default avatar does not exist for ${participant.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the local video is displayed in the local thumbnail and that the avatar is not displayed.
|
||||
*/
|
||||
async asserLocalThumbnailShowsVideo(): Promise<void> {
|
||||
await this.assertThumbnailShowsAvatar(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure a display name is visible on the stage.
|
||||
* @param value
|
||||
*/
|
||||
async assertDisplayNameVisibleOnStage(value: string) {
|
||||
const displayNameEl = this.driver.$('div[data-testid="stage-display-name"]');
|
||||
|
||||
expect(await displayNameEl.isDisplayed()).toBe(true);
|
||||
expect(await displayNameEl.getText()).toBe(value);
|
||||
}
|
||||
}
|
||||
|
||||
129
tests/helpers/WebhookProxy.ts
Normal file
129
tests/helpers/WebhookProxy.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import WebSocket from 'ws';
|
||||
|
||||
/**
|
||||
* Uses the webhook proxy service to proxy events to the testing clients.
|
||||
*/
|
||||
export default class WebhookProxy {
|
||||
private url;
|
||||
private secret;
|
||||
private ws: WebSocket | undefined;
|
||||
private cache = new Map();
|
||||
private listeners = new Map();
|
||||
private consumers = new Map();
|
||||
|
||||
/**
|
||||
* Initializes the webhook proxy.
|
||||
* @param url
|
||||
* @param secret
|
||||
*/
|
||||
constructor(url: string, secret: string) {
|
||||
this.url = url;
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects.
|
||||
*/
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url, {
|
||||
headers: {
|
||||
Authorization: this.secret
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('error', console.error);
|
||||
|
||||
this.ws.on('open', function open() {
|
||||
console.log('WebhookProxy connected');
|
||||
});
|
||||
|
||||
this.ws.on('message', (data: any) => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
|
||||
if (msg.eventType) {
|
||||
if (this.consumers.has(msg.eventType)) {
|
||||
this.consumers.get(msg.eventType)(msg);
|
||||
this.consumers.delete(msg.eventType);
|
||||
} else {
|
||||
this.cache.set(msg.eventType, msg);
|
||||
}
|
||||
|
||||
if (this.listeners.has(msg.eventType)) {
|
||||
this.listeners.get(msg.eventType)(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event consumer. Consumers receive the event single time and we remove them from the list of consumers.
|
||||
* @param eventType
|
||||
* @param callback
|
||||
*/
|
||||
addConsumer(eventType: string, callback: (deventata: any) => void) {
|
||||
if (this.cache.has(eventType)) {
|
||||
callback(this.cache.get(eventType));
|
||||
this.cache.delete(eventType);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.consumers.set(eventType, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any stored event.
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the event to be received.
|
||||
* @param eventType
|
||||
* @param timeout
|
||||
*/
|
||||
async waitForEvent(eventType: string, timeout = 4000): Promise<any> {
|
||||
// we create the error here so we have a meaningful stack trace
|
||||
const error = new Error(`Timeout waiting for event:${eventType}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiter = setTimeout(() => reject(error), timeout);
|
||||
|
||||
this.addConsumer(eventType, event => {
|
||||
clearTimeout(waiter);
|
||||
|
||||
resolve(event);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for the event type.
|
||||
* @param eventType
|
||||
* @param callback
|
||||
*/
|
||||
addListener(eventType: string, callback: (data: any) => void) {
|
||||
this.listeners.set(eventType, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for the event type.
|
||||
* @param eventType
|
||||
*/
|
||||
removeListener(eventType: string) {
|
||||
this.listeners.delete(eventType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the webhook proxy.
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
console.log('WebhookProxy disconnected');
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,141 @@
|
||||
import fs from 'fs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import process from 'node:process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Participant } from './Participant';
|
||||
|
||||
export type IContext = {
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
roomName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random room name.
|
||||
*
|
||||
* @returns {string} - The random room name.
|
||||
*/
|
||||
function generateRandomRoomName(): string {
|
||||
return `jitsimeettorture-${crypto.randomUUID()}}`;
|
||||
}
|
||||
import { IContext, IJoinOptions } from './types';
|
||||
|
||||
/**
|
||||
* Ensure that there is on participant.
|
||||
*
|
||||
* @param {IContext} context - The context.
|
||||
* @param {IContext} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureOneParticipant(context: IContext): Promise<void> {
|
||||
context.roomName = generateRandomRoomName();
|
||||
export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
ctx.p1 = new Participant('participant1');
|
||||
|
||||
context.p1 = new Participant('participant1');
|
||||
|
||||
await context.p1.joinConference(context, true);
|
||||
await ctx.p1.joinConference(ctx, {
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are three participants.
|
||||
*
|
||||
* @param {Object} context - The context.
|
||||
* @param {Object} ctx - The context.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureThreeParticipants(context: IContext): Promise<void> {
|
||||
context.roomName = generateRandomRoomName();
|
||||
export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx);
|
||||
|
||||
const p1 = new Participant('participant1');
|
||||
const p2 = new Participant('participant2');
|
||||
const p3 = new Participant('participant3');
|
||||
|
||||
context.p1 = p1;
|
||||
context.p2 = p2;
|
||||
context.p3 = p3;
|
||||
ctx.p2 = p2;
|
||||
ctx.p3 = p3;
|
||||
|
||||
// these need to be all, so we get the error when one fails
|
||||
await Promise.all([
|
||||
p1.joinConference(context),
|
||||
p2.joinConference(context),
|
||||
p3.joinConference(context)
|
||||
p2.joinConference(ctx),
|
||||
p3.joinConference(ctx)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
p1.waitForRemoteStreams(2),
|
||||
p2.waitForRemoteStreams(2),
|
||||
p3.waitForRemoteStreams(2)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the first participant is moderator.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to join.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
|
||||
const p1DisplayName = 'participant1';
|
||||
let token;
|
||||
|
||||
// if it is jaas create the first one to be moderator and second not moderator
|
||||
if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
|
||||
token = getModeratorToken(p1DisplayName);
|
||||
}
|
||||
|
||||
// make sure the first participant is moderator, if supported by deployment
|
||||
await _joinParticipant(p1DisplayName, ctx.p1, p => {
|
||||
ctx.p1 = p;
|
||||
}, {
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
}, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are two participants.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to join.
|
||||
*/
|
||||
export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
|
||||
const { skipInMeetingChecks } = options;
|
||||
|
||||
await Promise.all([
|
||||
_joinParticipant('participant2', ctx.p2, p => {
|
||||
ctx.p2 = p;
|
||||
}, options),
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a participant instance or prepares one for re-joining.
|
||||
* @param name - The name of the participant.
|
||||
* @param p - The participant instance to prepare or undefined if new one is needed.
|
||||
* @param setter - The setter to use for setting the new participant instance into the context if needed.
|
||||
* @param {boolean} options - Join options.
|
||||
* @param {string?} jwtToken - The token to use if any.
|
||||
*/
|
||||
async function _joinParticipant( // eslint-disable-line max-params
|
||||
name: string,
|
||||
p: Participant,
|
||||
setter: (p: Participant) => void,
|
||||
options: IJoinOptions = {},
|
||||
jwtToken?: string) {
|
||||
if (p) {
|
||||
if (ctx.iframeAPI) {
|
||||
await p.switchInPage();
|
||||
}
|
||||
|
||||
if (await p.isInMuc()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
// when loading url make sure we are on the top page context or strange errors may occur
|
||||
await p.switchToAPI();
|
||||
}
|
||||
|
||||
// Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
|
||||
await p.driver.url('/base.html');
|
||||
|
||||
// we want the participant instance re-recreated so we clear any kept state, like endpoint ID
|
||||
}
|
||||
|
||||
const newParticipant = new Participant(name, jwtToken);
|
||||
|
||||
// set the new participant instance, pass it to setter
|
||||
setter(newParticipant);
|
||||
|
||||
await newParticipant.joinConference(ctx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
|
||||
* conference participants sees a specific mute state for the former.
|
||||
@@ -72,9 +146,94 @@ export async function ensureThreeParticipants(context: IContext): Promise<void>
|
||||
* the mute state of {@code testee}.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function toggleMuteAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
export async function muteAudioAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
await testee.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
|
||||
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute audio, checks if the local UI has been updated accordingly and then does the verification from
|
||||
* the other observer participant perspective.
|
||||
* @param testee
|
||||
* @param observer
|
||||
*/
|
||||
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
|
||||
await testee.getToolbar().clickAudioUnmuteButton();
|
||||
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the video on testee and check on observer.
|
||||
* @param testee
|
||||
* @param observer
|
||||
*/
|
||||
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
await testee.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JWT token for a moderator.
|
||||
*/
|
||||
function getModeratorToken(displayName: string) {
|
||||
const keyid = process.env.JWT_KID;
|
||||
const headers = {
|
||||
algorithm: 'RS256',
|
||||
noTimestamp: true,
|
||||
expiresIn: '24h',
|
||||
keyid
|
||||
};
|
||||
|
||||
if (!keyid) {
|
||||
console.error('JWT_KID is not set');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const key = fs.readFileSync(ctx.jwtPrivateKeyPath);
|
||||
|
||||
const payload = {
|
||||
'aud': 'jitsi',
|
||||
'iss': 'chat',
|
||||
'sub': keyid.substring(0, keyid.indexOf('/')),
|
||||
'context': {
|
||||
'user': {
|
||||
'name': displayName,
|
||||
'id': uuidv4(),
|
||||
'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
|
||||
'email': 'john.doe@jitsi.org'
|
||||
}
|
||||
},
|
||||
'room': '*'
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
payload.context.user.moderator = true;
|
||||
|
||||
// @ts-ignore
|
||||
return jwt.sign(payload, key, headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a JID string.
|
||||
* @param str the string to parse.
|
||||
*/
|
||||
export function parseJid(str: string): {
|
||||
domain: string;
|
||||
node: string;
|
||||
resource: string | undefined;
|
||||
} {
|
||||
const parts = str.split('@');
|
||||
const domainParts = parts[1].split('/');
|
||||
|
||||
return {
|
||||
node: parts[0],
|
||||
domain: domainParts[0],
|
||||
resource: domainParts.length > 0 ? domainParts[1] : undefined
|
||||
};
|
||||
}
|
||||
|
||||
34
tests/helpers/types.ts
Normal file
34
tests/helpers/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Participant } from './Participant';
|
||||
import WebhookProxy from './WebhookProxy';
|
||||
|
||||
export type IContext = {
|
||||
conferenceJid: string;
|
||||
iframeAPI: boolean;
|
||||
jwtKid: string;
|
||||
jwtPrivateKeyPath: string;
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
roomName: string;
|
||||
skipSuiteTests: boolean;
|
||||
webhooksProxy: WebhookProxy;
|
||||
};
|
||||
|
||||
export type IJoinOptions = {
|
||||
|
||||
/**
|
||||
* Whether to skip setting display name.
|
||||
*/
|
||||
skipDisplayName?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip setting the moderator role for the first participant (whether to use jwt for it).
|
||||
*/
|
||||
skipFirstModerator?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip in meeting checks like ice connected and send receive data. For single in meeting participant.
|
||||
*/
|
||||
skipInMeetingChecks?: boolean;
|
||||
};
|
||||
66
tests/pageobjects/AVModerationMenu.ts
Normal file
66
tests/pageobjects/AVModerationMenu.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
const START_AUDIO_MODERATION = 'participants-pane-context-menu-start-audio-moderation';
|
||||
const STOP_AUDIO_MODERATION = 'participants-pane-context-menu-stop-audio-moderation';
|
||||
const START_VIDEO_MODERATION = 'participants-pane-context-menu-start-video-moderation';
|
||||
const STOP_VIDEO_MODERATION = 'participants-pane-context-menu-stop-video-moderation';
|
||||
|
||||
/**
|
||||
* Represents the Audio Video Moderation menu in the participants pane.
|
||||
*/
|
||||
export default class AVModerationMenu {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Represents the Audio Video Moderation menu in the participants pane.
|
||||
* @param participant
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the start audio moderation menu item.
|
||||
*/
|
||||
async clickStartAudioModeration() {
|
||||
await this.clickButton(START_AUDIO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the stop audio moderation menu item.
|
||||
*/
|
||||
async clickStopAudioModeration() {
|
||||
await this.clickButton(STOP_AUDIO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the start video moderation menu item.
|
||||
*/
|
||||
async clickStartVideoModeration() {
|
||||
await this.clickButton(START_VIDEO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the stop audio moderation menu item.
|
||||
*/
|
||||
async clickStopVideoModeration() {
|
||||
await this.clickButton(STOP_VIDEO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a context menu button.
|
||||
* @param id
|
||||
* @private
|
||||
*/
|
||||
private async clickButton(id: string) {
|
||||
const button = this.participant.driver.$(`#${id}`);
|
||||
|
||||
await button.waitForDisplayed();
|
||||
await button.click();
|
||||
|
||||
await button.moveTo({
|
||||
xOffset: -40,
|
||||
yOffset: -40
|
||||
});
|
||||
}
|
||||
}
|
||||
34
tests/pageobjects/BaseDialog.ts
Normal file
34
tests/pageobjects/BaseDialog.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
const CLOSE_BUTTON = 'modal-header-close-button';
|
||||
const OK_BUTTON = 'modal-dialog-ok-button';
|
||||
|
||||
/**
|
||||
* Base class for all dialogs.
|
||||
*/
|
||||
export default class BaseDialog {
|
||||
participant: Participant;
|
||||
|
||||
/**
|
||||
* Initializes for a participant.
|
||||
*
|
||||
* @param {Participant} participant - The participant.
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the X (close) button.
|
||||
*/
|
||||
async clickCloseButton(): Promise<void> {
|
||||
await this.participant.driver.$(`#${CLOSE_BUTTON}`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the ok button.
|
||||
*/
|
||||
async clickOkButton(): Promise<void> {
|
||||
await this.participant.driver.$(`#${OK_BUTTON}`).click();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
/**
|
||||
* Filmstrip elements.
|
||||
*/
|
||||
@@ -20,12 +22,12 @@ export default class Filmstrip {
|
||||
* mute icon for the conference participant identified by
|
||||
* {@code testee}.
|
||||
*
|
||||
* @param {Participant} testee - The {@code WebParticipant} for whom we're checking the status of audio muted icon.
|
||||
* @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
|
||||
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
|
||||
* otherwise, it will assert its presence.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false) {
|
||||
async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
|
||||
let id;
|
||||
|
||||
if (testee === this.participant) {
|
||||
@@ -40,7 +42,93 @@ export default class Filmstrip {
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Audio mute icon is not displayed for ${testee.name}`
|
||||
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remote display name for an endpoint.
|
||||
* @param endpointId The endpoint id.
|
||||
*/
|
||||
async getRemoteDisplayName(endpointId: string) {
|
||||
const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}_name"]`);
|
||||
|
||||
await remoteDisplayName.moveTo();
|
||||
|
||||
return await remoteDisplayName.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins a participant by clicking on their thumbnail.
|
||||
* @param participant The participant.
|
||||
*/
|
||||
async pinParticipant(participant: Participant) {
|
||||
const id = participant === this.participant
|
||||
? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
|
||||
|
||||
await this.participant.driver.$(`//span[@id="${id}"]`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets avatar SRC attribute for the one displayed on small video thumbnail.
|
||||
* @param endpointId
|
||||
*/
|
||||
async getAvatar(endpointId: string) {
|
||||
const elem = this.participant.driver.$(
|
||||
`//span[@id='participant_${endpointId}']//img[contains(@class,'userAvatar')]`);
|
||||
|
||||
return await elem.isExisting() ? elem.getAttribute('src') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants moderator rights to a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async grantModerator(participant: Participant) {
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'grantmoderatorlink', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the link in the remote participant actions menu.
|
||||
* @param participantId
|
||||
* @param linkClassname
|
||||
* @param dialogConfirm
|
||||
* @private
|
||||
*/
|
||||
private async clickOnRemoteMenuLink(participantId: string, linkClassname: string, dialogConfirm: boolean) {
|
||||
const thumbnail = this.participant.driver.$(
|
||||
`//span[@id='participant_${participantId}']//span[@id='remotevideomenu']`);
|
||||
|
||||
await thumbnail.moveTo();
|
||||
|
||||
const popoverElement = this.participant.driver.$(
|
||||
`//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
|
||||
|
||||
await popoverElement.waitForDisplayed();
|
||||
await popoverElement.click();
|
||||
|
||||
if (dialogConfirm) {
|
||||
await new BaseDialog(this.participant).clickOkButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the audio of a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async muteAudio(participant: Participant) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await this.participant.driver.$(`#participant-item-${participantId}`).moveTo();
|
||||
|
||||
await this.participant.driver.$(`button[data-testid="mute-audio-${participantId}"]`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the video of a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async muteVideo(participant: Participant) {
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
|
||||
}
|
||||
}
|
||||
|
||||
91
tests/pageobjects/IframeAPI.ts
Normal file
91
tests/pageobjects/IframeAPI.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
import { LOG_PREFIX } from '../helpers/browserLogger';
|
||||
|
||||
/**
|
||||
* The Iframe API and helpers from iframeAPITest.html
|
||||
*/
|
||||
export default class IframeAPI {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Initializes for a participant.
|
||||
* @param participant
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the json object from the iframeAPI helper.
|
||||
* @param event
|
||||
*/
|
||||
async getEventResult(event: string): Promise<any> {
|
||||
return await this.participant.driver.execute(
|
||||
eventName => {
|
||||
const result = window.jitsiAPI.test[eventName];
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to the iframeAPI.
|
||||
* @param eventName The event name.
|
||||
*/
|
||||
async addEventListener(eventName: string) {
|
||||
return await this.participant.driver.execute(
|
||||
(event, prefix) => {
|
||||
console.log(`${new Date().toISOString()} ${prefix} Adding listener for event: ${event}`);
|
||||
window.jitsiAPI.addListener(event, evt => {
|
||||
console.log(
|
||||
`${new Date().toISOString()} ${prefix} Received ${event} event: ${JSON.stringify(evt)}`);
|
||||
window.jitsiAPI.test[event] = evt;
|
||||
});
|
||||
}, eventName, LOG_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of available rooms and details of it.
|
||||
*/
|
||||
async getRoomsInfo() {
|
||||
return await 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes command using iframeAPI.
|
||||
* @param command The command.
|
||||
* @param args The arguments.
|
||||
*/
|
||||
async executeCommand(command: string, ...args: any[]) {
|
||||
return await this.participant.driver.execute(
|
||||
(commandName, commandArgs) =>
|
||||
window.jitsiAPI.executeCommand(commandName, ...commandArgs)
|
||||
, command, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the participant's pane.
|
||||
*/
|
||||
async isParticipantsPaneOpen() {
|
||||
return await this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the embedded Jitsi Meet conference.
|
||||
*/
|
||||
async dispose() {
|
||||
return await this.participant.driver.execute(() => window.jitsiAPI.dispose());
|
||||
}
|
||||
|
||||
}
|
||||
54
tests/pageobjects/Notifications.ts
Normal file
54
tests/pageobjects/Notifications.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
|
||||
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
|
||||
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
|
||||
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
|
||||
const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
|
||||
|
||||
/**
|
||||
* Gathers all notifications logic in the UI and obtaining those.
|
||||
*/
|
||||
export default class Notifications {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Represents the Audio Video Moderation menu in the participants pane.
|
||||
* @param participant
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the raised hand notification to be displayed.
|
||||
* The notification on moderators page when the participant tries to unmute.
|
||||
*/
|
||||
async waitForRaisedHandNotification() {
|
||||
const displayNameEl
|
||||
= this.participant.driver.$(`div[data-testid="${RAISE_HAND_NOTIFICATION_ID}"]`);
|
||||
|
||||
await displayNameEl.waitForExist({ timeout: 2000 });
|
||||
await displayNameEl.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification on participants page when the moderator asks to unmute.
|
||||
*/
|
||||
async waitForAskToUnmuteNotification() {
|
||||
const displayNameEl
|
||||
= this.participant.driver.$(`div[data-testid="${ASK_TO_UNMUTE_NOTIFICATION_ID}"]`);
|
||||
|
||||
await displayNameEl.waitForExist({ timeout: 2000 });
|
||||
await displayNameEl.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses any join notifications.
|
||||
*/
|
||||
async dismissAnyJoinNotification() {
|
||||
await 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()));
|
||||
}
|
||||
}
|
||||
165
tests/pageobjects/ParticipantsPane.ts
Normal file
165
tests/pageobjects/ParticipantsPane.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import AVModerationMenu from './AVModerationMenu';
|
||||
|
||||
/**
|
||||
* Classname of the closed/hidden participants pane
|
||||
*/
|
||||
const PARTICIPANTS_PANE = 'participants_pane';
|
||||
|
||||
/**
|
||||
* Represents the participants pane from the UI.
|
||||
*/
|
||||
export default class ParticipantsPane {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Initializes for a participant.
|
||||
*
|
||||
* @param {Participant} participant - The participant.
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio video moderation menu.
|
||||
*/
|
||||
getAVModerationMenu() {
|
||||
return new AVModerationMenu(this.participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pane is open.
|
||||
*/
|
||||
async isOpen() {
|
||||
return this.participant.driver.$(`.${PARTICIPANTS_PANE}`).isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "participants" toolbar button to open the participants pane.
|
||||
*/
|
||||
async open() {
|
||||
await this.participant.getToolbar().clickParticipantsPaneButton();
|
||||
|
||||
const pane = this.participant.driver.$(`.${PARTICIPANTS_PANE}`);
|
||||
|
||||
await pane.waitForExist();
|
||||
await pane.waitForStable();
|
||||
await pane.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "participants" toolbar button to close the participants pane.
|
||||
*/
|
||||
async close() {
|
||||
await this.participant.getToolbar().clickCloseParticipantsPaneButton();
|
||||
|
||||
await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that {@code participant} shows or doesn't show the video mute icon for the conference participant
|
||||
* identified by {@code testee}.
|
||||
*
|
||||
* @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
|
||||
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
|
||||
* otherwise, it will assert its presence.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async assertVideoMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
|
||||
const isOpen = await this.isOpen();
|
||||
|
||||
if (!isOpen) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const id = `participant-item-${await testee.getEndpointId()}`;
|
||||
const mutedIconXPath
|
||||
= `//div[@id='${id}']//div[contains(@class, 'indicators')]//*[local-name()='svg' and @id='videoMuted']`;
|
||||
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Video mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the context menu button in the participants pane.
|
||||
*/
|
||||
async clickContextMenuButton() {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const menu = this.participant.driver.$('#participants-pane-context-menu');
|
||||
|
||||
await menu.waitForDisplayed();
|
||||
await menu.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trys to click allow video button.
|
||||
* @param participantToUnmute
|
||||
*/
|
||||
async allowVideo(participantToUnmute: Participant) {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const participantId = await participantToUnmute.getEndpointId();
|
||||
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
|
||||
|
||||
await participantItem.waitForExist();
|
||||
await participantItem.moveTo();
|
||||
|
||||
const unmuteButton = this.participant.driver
|
||||
.$(`button[data-testid="unmute-video-${participantId}"]`);
|
||||
|
||||
await unmuteButton.waitForExist();
|
||||
await unmuteButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trys to click ask to unmute button.
|
||||
* @param participantToUnmute
|
||||
* @param fromContextMenu
|
||||
*/
|
||||
async askToUnmute(participantToUnmute: Participant, fromContextMenu: boolean) {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
await this.participant.getNotifications().dismissAnyJoinNotification();
|
||||
|
||||
const participantId = await participantToUnmute.getEndpointId();
|
||||
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
|
||||
|
||||
await participantItem.waitForExist();
|
||||
await participantItem.waitForStable();
|
||||
await participantItem.waitForDisplayed();
|
||||
await participantItem.moveTo();
|
||||
|
||||
if (fromContextMenu) {
|
||||
const meetingParticipantMoreOptions = this.participant.driver
|
||||
.$(`[data-testid="participant-more-options-${participantId}"]`);
|
||||
|
||||
await meetingParticipantMoreOptions.waitForExist();
|
||||
await meetingParticipantMoreOptions.waitForDisplayed();
|
||||
await meetingParticipantMoreOptions.waitForStable();
|
||||
await meetingParticipantMoreOptions.moveTo();
|
||||
await meetingParticipantMoreOptions.click();
|
||||
}
|
||||
|
||||
const unmuteButton = this.participant.driver
|
||||
.$(`[data-testid="unmute-audio-${participantId}"]`);
|
||||
|
||||
await unmuteButton.waitForExist();
|
||||
await unmuteButton.click();
|
||||
}
|
||||
}
|
||||
62
tests/pageobjects/SettingsDialog.ts
Normal file
62
tests/pageobjects/SettingsDialog.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const EMAIL_FIELD = '#setEmail';
|
||||
const SETTINGS_DIALOG_CONTENT = '.settings-pane';
|
||||
const X_PATH_PROFILE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Profile"]';
|
||||
|
||||
/**
|
||||
* The settings dialog.
|
||||
*/
|
||||
export default class SettingsDialog extends BaseDialog {
|
||||
/**
|
||||
* Waits for the settings dialog to be visible.
|
||||
*/
|
||||
async waitForDisplay() {
|
||||
await this.participant.driver.$(SETTINGS_DIALOG_CONTENT).waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a specific tab in the settings dialog.
|
||||
* @param xpath
|
||||
* @private
|
||||
*/
|
||||
private async openTab(xpath: string) {
|
||||
const elem = this.participant.driver.$(xpath);
|
||||
|
||||
await elem.waitForClickable();
|
||||
await elem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the Profile tab to be displayed.
|
||||
*/
|
||||
async openProfileTab() {
|
||||
await this.openTab(X_PATH_PROFILE_TAB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters the passed in email into the email field.
|
||||
* @param email
|
||||
*/
|
||||
async setEmail(email: string) {
|
||||
await this.openProfileTab();
|
||||
|
||||
await this.participant.driver.$(EMAIL_FIELD).setValue(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the participant's email displayed in the settings dialog.
|
||||
*/
|
||||
async getEmail() {
|
||||
await this.openProfileTab();
|
||||
|
||||
return await this.participant.driver.$(EMAIL_FIELD).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the OK button on the settings dialog to close the dialog and save any changes made.
|
||||
*/
|
||||
async submit() {
|
||||
await this.clickOkButton();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,15 @@ import { Participant } from '../helpers/Participant';
|
||||
|
||||
const AUDIO_MUTE = 'Mute microphone';
|
||||
const AUDIO_UNMUTE = 'Unmute microphone';
|
||||
const CLOSE_PARTICIPANTS_PANE = 'Close participants pane';
|
||||
const OVERFLOW_MENU = 'More actions menu';
|
||||
const OVERFLOW = 'More actions';
|
||||
const PARTICIPANTS = 'Open participants pane';
|
||||
const PROFILE = 'Edit your profile';
|
||||
const RAISE_HAND = 'Raise your hand';
|
||||
const VIDEO_QUALITY = 'Manage video quality';
|
||||
const VIDEO_MUTE = 'Stop camera';
|
||||
const VIDEO_UNMUTE = 'Start camera';
|
||||
|
||||
/**
|
||||
* The toolbar elements.
|
||||
@@ -49,8 +58,8 @@ export default class Toolbar {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickAudioMuteButton() {
|
||||
await this.participant.log('Clicking on: Audio Mute Button');
|
||||
async clickAudioMuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Audio Mute Button');
|
||||
await this.audioMuteBtn.click();
|
||||
}
|
||||
|
||||
@@ -59,8 +68,168 @@ export default class Toolbar {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickAudioUnmuteButton() {
|
||||
await this.participant.log('Clicking on: Audio Unmute Button');
|
||||
async clickAudioUnmuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Audio Unmute Button');
|
||||
await this.audioUnMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The video mute button.
|
||||
*/
|
||||
get videoMuteBtn() {
|
||||
return this.getButton(VIDEO_MUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The video unmute button.
|
||||
*/
|
||||
get videoUnMuteBtn() {
|
||||
return this.getButton(VIDEO_UNMUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks video mute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickVideoMuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Video Mute Button');
|
||||
await this.videoMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks video unmute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickVideoUnmuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Video Unmute Button');
|
||||
await this.videoUnMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks Participants pane button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickCloseParticipantsPaneButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Close Participants pane Button');
|
||||
await this.getButton(CLOSE_PARTICIPANTS_PANE).click();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clicks Participants pane button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickParticipantsPaneButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Participants pane Button');
|
||||
await this.getButton(PARTICIPANTS).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the video quality toolbar button which opens the
|
||||
* dialog for adjusting max-received video quality.
|
||||
*/
|
||||
async 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> {
|
||||
return this.clickButtonInOverflowMenu(PROFILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the raise hand button that enables participants will to speak.
|
||||
*/
|
||||
async clickRaiseHandButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Raise hand Button');
|
||||
await this.getButton(RAISE_HAND).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the overflow menu is open and clicks on a specified button.
|
||||
* @param accessibilityLabel The accessibility label of the button to be clicked.
|
||||
* @private
|
||||
*/
|
||||
private async clickButtonInOverflowMenu(accessibilityLabel: string) {
|
||||
await this.openOverflowMenu();
|
||||
|
||||
this.participant.log(`Clicking on: ${accessibilityLabel}`);
|
||||
await this.getButton(accessibilityLabel).click();
|
||||
|
||||
await this.closeOverflowMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the overflow menu is open and visible.
|
||||
* @private
|
||||
*/
|
||||
private async isOverflowMenuOpen() {
|
||||
return await this.participant.driver.$$(`[aria-label^="${OVERFLOW_MENU}"]`).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the overflow toolbar button which opens or closes the overflow menu.
|
||||
* @private
|
||||
*/
|
||||
private async clickOverflowButton(): Promise<void> {
|
||||
await this.getButton(OVERFLOW).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the overflow menu is displayed.
|
||||
* @private
|
||||
*/
|
||||
private async openOverflowMenu() {
|
||||
if (await this.isOverflowMenuOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.clickOverflowButton();
|
||||
|
||||
await this.waitForOverFlowMenu(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the overflow menu is not displayed.
|
||||
* @private
|
||||
*/
|
||||
private async closeOverflowMenu() {
|
||||
if (!await this.isOverflowMenuOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.clickOverflowButton();
|
||||
|
||||
await this.waitForOverFlowMenu(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the overflow menu to be visible or hidden.
|
||||
* @param visible
|
||||
* @private
|
||||
*/
|
||||
private async waitForOverFlowMenu(visible: boolean) {
|
||||
await this.participant.driver.$(`[aria-label^="${OVERFLOW_MENU}"]`).waitForDisplayed({
|
||||
reverse: !visible,
|
||||
timeout: 3000,
|
||||
timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the participant's avatar image element located in the toolbar.
|
||||
*/
|
||||
async getProfileImage() {
|
||||
await this.openOverflowMenu();
|
||||
|
||||
const elem = this.participant.driver.$(`[aria-label^="${PROFILE}"] img`);
|
||||
|
||||
return await elem.isExisting() ? await elem.getAttribute('src') : null;
|
||||
}
|
||||
}
|
||||
|
||||
42
tests/pageobjects/VideoQualityDialog.ts
Normal file
42
tests/pageobjects/VideoQualityDialog.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Key } from 'webdriverio';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const VIDEO_QUALITY_SLIDER_CLASS = 'custom-slider';
|
||||
|
||||
/**
|
||||
* The video quality dialog.
|
||||
*/
|
||||
export default class VideoQualityDialog extends BaseDialog {
|
||||
/**
|
||||
* Opens the video quality dialog and sets the video quality to the minimum or maximum definition.
|
||||
* @param audioOnly - Whether to set the video quality to audio only (minimum).
|
||||
* @private
|
||||
*/
|
||||
async setVideoQuality(audioOnly: boolean) {
|
||||
await this.participant.getToolbar().clickVideoQualityButton();
|
||||
|
||||
const videoQualitySlider = this.participant.driver.$(`.${VIDEO_QUALITY_SLIDER_CLASS}`);
|
||||
|
||||
const audioOnlySliderValue = parseInt(await videoQualitySlider.getAttribute('min'), 10);
|
||||
|
||||
const maxDefinitionSliderValue = parseInt(await videoQualitySlider.getAttribute('max'), 10);
|
||||
const activeValue = parseInt(await videoQualitySlider.getAttribute('value'), 10);
|
||||
|
||||
const targetValue = audioOnly ? audioOnlySliderValue : maxDefinitionSliderValue;
|
||||
const distanceToTargetValue = targetValue - activeValue;
|
||||
const keyDirection = distanceToTargetValue > 0 ? Key.ArrowRight : Key.ArrowLeft;
|
||||
|
||||
// we need to click the element to activate it so it will receive the keys
|
||||
await videoQualitySlider.click();
|
||||
|
||||
// Move the slider to the target value.
|
||||
for (let i = 0; i < Math.abs(distanceToTargetValue); i++) {
|
||||
|
||||
await this.participant.driver.keys(keyDirection);
|
||||
}
|
||||
|
||||
// Close the video quality dialog.
|
||||
await this.clickCloseButton();
|
||||
}
|
||||
}
|
||||
121
tests/resources/iframeAPITest.html
Normal file
121
tests/resources/iframeAPITest.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<title>iframe API test</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
/**
|
||||
* Ported from https://github.com/jitsi/jitsi-meet-torture/blob/master/src/test/resources/files/iframeAPITest.html
|
||||
*/
|
||||
const blacklist = [ '__proto__', 'constructor', 'prototype' ];
|
||||
const paramStr = document.location.hash;
|
||||
const params = {};
|
||||
const paramParts = paramStr?.substring(1).split('&') || [];
|
||||
|
||||
paramParts.forEach(part => {
|
||||
const param = part.split('=');
|
||||
const key = param[0];
|
||||
|
||||
if (!key || key.split('.').some(k => blacklist.includes(k))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
try {
|
||||
value = param[1];
|
||||
|
||||
|
||||
const decoded = decodeURIComponent(value).replace(/\\&/, '&')
|
||||
.replace(/[\u2018\u2019]/g, '\'')
|
||||
.replace(/[\u201C\u201D]/g, '"');
|
||||
|
||||
value = decoded === 'undefined' || decoded === '' ? undefined : JSON.parse(decoded);
|
||||
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse URL parameter value: ${String(value)}`, e);
|
||||
|
||||
return;
|
||||
}
|
||||
params[key] = value;
|
||||
});
|
||||
const json = {
|
||||
config: {},
|
||||
interfaceConfig: {}
|
||||
};
|
||||
|
||||
for (const param of Object.keys(params)) {
|
||||
let base = json;
|
||||
const names = param.split('.');
|
||||
const last = names.pop() ?? '';
|
||||
|
||||
for (const name of names) {
|
||||
base = base[name] = base[name] || {};
|
||||
}
|
||||
|
||||
base[last] = params[param];
|
||||
}
|
||||
|
||||
const { config, domain, interfaceConfig, jwt, password, room:roomName, userInfo: uInfoObj } = json;
|
||||
let tenant = json.tenant || '';
|
||||
|
||||
let userInfo;
|
||||
if (uInfoObj) {
|
||||
if (uInfoObj.length > 0) {
|
||||
userInfo = JSON.parse(uInfoObj);
|
||||
} else if (Object.keys(uInfoObj).length) {
|
||||
userInfo = uInfoObj;
|
||||
}
|
||||
}
|
||||
|
||||
if (tenant.length > 0) {
|
||||
tenant = tenant + '/';
|
||||
}
|
||||
|
||||
const options = {
|
||||
jwt,
|
||||
roomName: `${tenant}${roomName}`,
|
||||
configOverwrite: config,
|
||||
interfaceConfigOverwrite: interfaceConfig,
|
||||
userInfo,
|
||||
onload: function () {
|
||||
// we use this to save data from api to be accessible to tests
|
||||
window.jitsiAPI.test = {};
|
||||
|
||||
window.jitsiAPI.addEventListener('participantRoleChanged', function(event) {
|
||||
if (event.role === "moderator" && event.id === window.jitsiAPI.test.myEndpointId) {
|
||||
window.jitsiAPI.test.isModerator = true;
|
||||
}
|
||||
});
|
||||
window.jitsiAPI.addEventListener('audioAvailabilityChanged', function(event) {
|
||||
window.jitsiAPI.test.audioAvailabilityChanged = event;
|
||||
});
|
||||
window.jitsiAPI.addEventListener('videoAvailabilityChanged', function(event) {
|
||||
window.jitsiAPI.test.videoAvailabilityChanged = event;
|
||||
});
|
||||
window.jitsiAPI.addEventListener('videoConferenceJoined', function(event) {
|
||||
window.jitsiAPI.test.videoConferenceJoined = event;
|
||||
window.jitsiAPI.test.myEndpointId = event.id;
|
||||
});
|
||||
if (password && password.length > 0) {
|
||||
// join a protected channel with the password supplied
|
||||
window.jitsiAPI.on('passwordRequired', function ()
|
||||
{
|
||||
window.jitsiAPI.executeCommand('password', password);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const externalAPIScript = document.createElement('script');
|
||||
externalAPIScript.src = `https://${domain}/${tenant}external_api.js`;
|
||||
externalAPIScript.type = "text/javascript";
|
||||
externalAPIScript.onload = function(){
|
||||
window.jitsiAPI = new JitsiMeetExternalAPI(domain, options);
|
||||
}
|
||||
document.getElementsByTagName('head')[0].appendChild(externalAPIScript);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
94
tests/specs/2way/audioOnly.spec.ts
Normal file
94
tests/specs/2way/audioOnly.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Audio only - ', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await 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);
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that participant1 sees avatars for itself and other participants.
|
||||
*/
|
||||
it('avatars check', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.driver.$('//div[@id="dominantSpeaker"]').waitForDisplayed();
|
||||
|
||||
// Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
|
||||
await p1.assertThumbnailShowsAvatar(p1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Disables audio only mode and verifies that both participants see p1 as not video muted.
|
||||
*/
|
||||
it('disable and check', async () => {
|
||||
await setAudioOnlyAndCheck(false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
|
||||
* p2 participant sees a video mute state for the former.
|
||||
* @param enable
|
||||
*/
|
||||
async function setAudioOnlyAndCheck(enable: boolean) {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getVideoQualityDialog().setVideoQuality(enable);
|
||||
|
||||
await verifyVideoMute(enable);
|
||||
|
||||
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
|
||||
.waitForDisplayed({ reverse: !enable });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that p1 and p2 see p1 as video muted or not.
|
||||
* @param muted
|
||||
*/
|
||||
async function verifyVideoMute(muted: boolean) {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// Verify the observer sees the testee in the desired muted state.
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
|
||||
|
||||
// Verify the testee sees itself in the desired muted state.
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1
|
||||
* as video muted.
|
||||
*/
|
||||
it('mute video, set twice and check muted', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
// Mute video on participant1.
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await verifyVideoMute(true);
|
||||
|
||||
// Enable audio-only mode.
|
||||
await setAudioOnlyAndCheck(true);
|
||||
|
||||
// Disable audio-only mode.
|
||||
await p1.getVideoQualityDialog().setVideoQuality(false);
|
||||
|
||||
// p1 should stay muted since it was muted before audio-only was enabled.
|
||||
await verifyVideoMute(true);
|
||||
});
|
||||
|
||||
it('unmute video and check not muted', async () => {
|
||||
// Unmute video on participant1.
|
||||
await ctx.p1.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await verifyVideoMute(false);
|
||||
});
|
||||
});
|
||||
418
tests/specs/2way/iFrameApiParticipantsPresence.spec.ts
Normal file
418
tests/specs/2way/iFrameApiParticipantsPresence.spec.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
|
||||
|
||||
/**
|
||||
* Tests PARTICIPANT_LEFT webhook.
|
||||
*/
|
||||
async function checkParticipantLeftHook(p: Participant, reason: string) {
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_LEFT webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
disconnectReason: string;
|
||||
isBreakout: boolean;
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect('PARTICIPANT_LEFT').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(ctx.conferenceJid);
|
||||
expect(event.data.disconnectReason).toBe(reason);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.participantId).toBe(await p.getEndpointId());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
// let's populate endpoint ids
|
||||
await Promise.all([
|
||||
p1.getEndpointId(),
|
||||
p2.getEndpointId()
|
||||
]);
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p2.getIframeAPI().getEventResult('isModerator')).toBe(false);
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
|
||||
if (webhooksProxy) {
|
||||
// USAGE webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: [
|
||||
{ participantId: string; }
|
||||
];
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('USAGE');
|
||||
|
||||
expect('USAGE').toBe(event.eventType);
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
expect(event.data.filter(d => d.participantId === p1EpId
|
||||
|| d.participantId === p2EpId).length).toBe(2);
|
||||
}
|
||||
|
||||
// we will use it later
|
||||
// TODO figure out why adding those just before grantModerator and we miss the events
|
||||
await p1.getIframeAPI().addEventListener('participantRoleChanged');
|
||||
await p2.getIframeAPI().addEventListener('participantRoleChanged');
|
||||
});
|
||||
|
||||
it('participants info',
|
||||
async () => {
|
||||
const { p1, roomName, webhooksProxy } = ctx;
|
||||
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
|
||||
|
||||
expect(roomsInfo).toBeDefined();
|
||||
expect(roomsInfo.isMainRoom).toBe(true);
|
||||
|
||||
expect(roomsInfo.id).toBeDefined();
|
||||
const { node: roomNode } = parseJid(roomsInfo.id);
|
||||
|
||||
expect(roomNode).toBe(roomName);
|
||||
|
||||
const { node, resource } = parseJid(roomsInfo.jid);
|
||||
|
||||
ctx.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
|
||||
expect(node).toBe(roomName);
|
||||
expect(resource).toBe(p1EpId);
|
||||
|
||||
expect(roomsInfo.participants.length).toBe(2);
|
||||
expect(await p1.getIframeAPI().getNumberOfParticipants()).toBe(2);
|
||||
|
||||
if (webhooksProxy) {
|
||||
// ROOM_CREATED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
|
||||
|
||||
expect('ROOM_CREATED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(ctx.conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('participants pane', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantsPaneToggled');
|
||||
await p1.getIframeAPI().executeCommand('toggleParticipantsPane', true);
|
||||
|
||||
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(true);
|
||||
expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(true);
|
||||
|
||||
await p1.getIframeAPI().executeCommand('toggleParticipantsPane', false);
|
||||
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
|
||||
expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(false);
|
||||
});
|
||||
|
||||
it('grant moderator', async () => {
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
|
||||
|
||||
await p2.driver.waitUntil(async () => await p2.getIframeAPI().getEventResult('isModerator'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Moderator role not granted'
|
||||
});
|
||||
|
||||
const event1 = await p1.getIframeAPI().getEventResult('participantRoleChanged');
|
||||
|
||||
expect(event1?.id).toBe(p2EpId);
|
||||
expect(event1?.role).toBe('moderator');
|
||||
|
||||
const event2 = await p2.getIframeAPI().getEventResult('participantRoleChanged');
|
||||
|
||||
expect(event2?.id).toBe(p2EpId);
|
||||
expect(event2?.role).toBe('moderator');
|
||||
|
||||
if (webhooksProxy) {
|
||||
// ROLE_CHANGED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
grantedBy: {
|
||||
participantId: string;
|
||||
};
|
||||
grantedTo: {
|
||||
participantId: string;
|
||||
};
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROLE_CHANGED');
|
||||
|
||||
expect('ROLE_CHANGED').toBe(event.eventType);
|
||||
expect(event.data.role).toBe('moderator');
|
||||
expect(event.data.grantedBy.participantId).toBe(await p1.getEndpointId());
|
||||
expect(event.data.grantedTo.participantId).toBe(await p2.getEndpointId());
|
||||
}
|
||||
});
|
||||
|
||||
it('kick participant', async () => {
|
||||
const { p1, p2, roomName } = ctx;
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
await p1.switchInPage();
|
||||
await p2.switchInPage();
|
||||
|
||||
const p1DisplayName = await p1.getLocalDisplayName();
|
||||
const p2DisplayName = await p2.getLocalDisplayName();
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantKickedOut');
|
||||
await p2.getIframeAPI().addEventListener('participantKickedOut');
|
||||
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
|
||||
|
||||
const eventP1 = await p1.driver.waitUntil(async () =>
|
||||
await 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'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'participantKickedOut event not received on participant2 side'
|
||||
});
|
||||
|
||||
await checkParticipantLeftHook(p2, 'kicked');
|
||||
|
||||
expect(eventP1).toBeDefined();
|
||||
expect(eventP2).toBeDefined();
|
||||
|
||||
expect(isEqual(eventP1, {
|
||||
kicked: {
|
||||
id: p2EpId,
|
||||
local: false,
|
||||
name: p2DisplayName
|
||||
},
|
||||
kicker: {
|
||||
id: p1EpId,
|
||||
local: true,
|
||||
name: p1DisplayName
|
||||
}
|
||||
})).toBe(true);
|
||||
|
||||
expect(isEqual(eventP2, {
|
||||
kicked: {
|
||||
id: 'local',
|
||||
local: true,
|
||||
name: p2DisplayName
|
||||
},
|
||||
kicker: {
|
||||
id: p1EpId,
|
||||
name: p1DisplayName
|
||||
}
|
||||
})).toBe(true);
|
||||
|
||||
const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
|
||||
await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
});
|
||||
|
||||
it('join after kick', async () => {
|
||||
const { p1, webhooksProxy } = ctx;
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantJoined');
|
||||
await p1.getIframeAPI().addEventListener('participantMenuButtonClick');
|
||||
|
||||
webhooksProxy?.clearCache();
|
||||
|
||||
// join again
|
||||
await ensureTwoParticipants(ctx);
|
||||
const { p2 } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_JOINED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
moderator: boolean;
|
||||
name: string;
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(ctx.conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.moderator).toBe(false);
|
||||
expect(event.data.name).toBe(await p2.getLocalDisplayName());
|
||||
expect(event.data.participantId).toBe(await p2.getEndpointId());
|
||||
}
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
const event = await p1.driver.waitUntil(async () =>
|
||||
await p1.getIframeAPI().getEventResult('participantJoined'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'participantJoined not received'
|
||||
});
|
||||
|
||||
const p2DisplayName = await p2.getLocalDisplayName();
|
||||
|
||||
expect(event).toBeDefined();
|
||||
expect(event.id).toBe(await p2.getEndpointId());
|
||||
expect(event.displayName).toBe(p2DisplayName);
|
||||
expect(event.formattedDisplayName).toBe(p2DisplayName);
|
||||
|
||||
});
|
||||
|
||||
it('overwrite names', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
const newP1Name = 'p1';
|
||||
const newP2Name = 'p2';
|
||||
const newNames: ({ id: string; name: string; })[] = [ {
|
||||
id: p2EpId,
|
||||
name: newP2Name
|
||||
}, {
|
||||
id: p1EpId,
|
||||
name: newP1Name
|
||||
} ];
|
||||
|
||||
await p1.getIframeAPI().executeCommand('overwriteNames', newNames);
|
||||
|
||||
await p1.switchInPage();
|
||||
|
||||
expect(await p1.getLocalDisplayName()).toBe(newP1Name);
|
||||
|
||||
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EpId)).toBe(newP2Name);
|
||||
|
||||
});
|
||||
|
||||
it('hangup', async () => {
|
||||
const { p1, p2, roomName } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
await p2.getIframeAPI().addEventListener('readyToClose');
|
||||
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
|
||||
await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(p2, 'left');
|
||||
|
||||
const eventReadyToCloseP2 = await p2.driver.waitUntil(async () =>
|
||||
await p2.getIframeAPI().getEventResult('readyToClose'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'readyToClose not received'
|
||||
});
|
||||
|
||||
expect(eventReadyToCloseP2).toBeDefined();
|
||||
});
|
||||
|
||||
it('dispose conference', async () => {
|
||||
const { conferenceJid, p1, roomName, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
await p1.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
await p1.getIframeAPI().addEventListener('readyToClose');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
const eventConferenceLeft = await p1.driver.waitUntil(async () =>
|
||||
await p1.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
expect(eventConferenceLeft).toBeDefined();
|
||||
expect(eventConferenceLeft.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(p1, 'left');
|
||||
if (webhooksProxy) {
|
||||
// ROOM_DESTROYED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
expect('ROOM_DESTROYED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
|
||||
const eventReadyToClose = await p1.driver.waitUntil(async () =>
|
||||
await p1.getIframeAPI().getEventResult('readyToClose'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'readyToClose not received'
|
||||
});
|
||||
|
||||
expect(eventReadyToClose).toBeDefined();
|
||||
|
||||
// dispose
|
||||
await p1.getIframeAPI().dispose();
|
||||
|
||||
// check there is no iframe on the page
|
||||
await p1.driver.$('iframe').waitForExist({
|
||||
reverse: true,
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'iframe is still on the page'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +1,35 @@
|
||||
/* global APP */
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { IContext, ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants';
|
||||
import { ensureThreeParticipants, muteAudioAndCheck } from '../../helpers/participants';
|
||||
|
||||
describe('ActiveSpeaker ', () => {
|
||||
const context = {} as IContext;
|
||||
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants(context);
|
||||
await ensureThreeParticipants(ctx);
|
||||
|
||||
await toggleMuteAndCheck(context.p1, context.p2);
|
||||
await toggleMuteAndCheck(context.p2, context.p1);
|
||||
await toggleMuteAndCheck(context.p3, context.p1);
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
await muteAudioAndCheck(p2, p1);
|
||||
await muteAudioAndCheck(p3, p1);
|
||||
|
||||
// participant1 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(context.p1, context.p2, context.p3);
|
||||
await testActiveSpeaker(p1, p2, p3);
|
||||
|
||||
// participant3 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(context.p3, context.p2, context.p1);
|
||||
await testActiveSpeaker(p3, p2, p1);
|
||||
|
||||
// participant2 becomes active speaker - check from participant1's perspective
|
||||
await testActiveSpeaker(context.p2, context.p1, context.p3);
|
||||
await testActiveSpeaker(p2, p1, p3);
|
||||
|
||||
// check the displayed speakers, there should be only one speaker
|
||||
await assertOneDominantSpeaker(context.p1);
|
||||
await assertOneDominantSpeaker(context.p2);
|
||||
await assertOneDominantSpeaker(context.p3);
|
||||
await assertOneDominantSpeaker(p1);
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tries to make given participant an active speaker by un-muting it.
|
||||
* Tries to make given participant an active speaker by unmuting it.
|
||||
* Verifies from {@code participant2}'s perspective that the active speaker
|
||||
* has been displayed on the large video area. Mutes him back.
|
||||
*
|
||||
@@ -38,7 +38,6 @@ describe('ActiveSpeaker ', () => {
|
||||
* @param {Participant} otherParticipant1 - <tt>Participant</tt> of the participant who will be observing and verifying
|
||||
* active speaker change.
|
||||
* @param {Participant} otherParticipant2 - Used only to print some debugging info.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function testActiveSpeaker(
|
||||
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
|
||||
@@ -62,9 +61,10 @@ async function testActiveSpeaker(
|
||||
const otherParticipant1Driver = otherParticipant1.driver;
|
||||
|
||||
await otherParticipant1Driver.waitUntil(
|
||||
() => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint),
|
||||
async () => await otherParticipant1Driver.execute(
|
||||
id => APP.UI.getLargeVideoID() === id, speakerEndpoint),
|
||||
{
|
||||
timeout: 30_1000, // 30 seconds
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: 'Active speaker not displayed on large video.'
|
||||
});
|
||||
|
||||
@@ -86,7 +86,6 @@ async function testActiveSpeaker(
|
||||
* indicator displayed equals 1.
|
||||
*
|
||||
* @param {Participant} participant - The participant to check.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function assertOneDominantSpeaker(participant: Participant) {
|
||||
expect(await participant.driver.$$(
|
||||
|
||||
284
tests/specs/3way/audioVideoModeration.spec.ts
Normal file
284
tests/specs/3way/audioVideoModeration.spec.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants, ensureTwoParticipants,
|
||||
unmuteAudioAndCheck,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('AVModeration -', () => {
|
||||
|
||||
it('check for moderators', async () => {
|
||||
// if all 3 participants are moderators, skip this test
|
||||
await ensureThreeParticipants(ctx);
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()
|
||||
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('check audio enable/disable', async () => {
|
||||
const { p1, p3 } = ctx;
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// Here we want to try unmuting and check that we are still muted.
|
||||
await tryToAudioUnmuteAndCheck(p3, p1);
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await unmuteAudioAndCheck(p3, p1);
|
||||
});
|
||||
|
||||
it('check video enable/disable', async () => {
|
||||
const { p1, p3 } = ctx;
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// Here we want to try unmuting and check that we are still muted.
|
||||
await tryToVideoUnmuteAndCheck(p3, p1);
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await unmuteVideoAndCheck(p3, p1);
|
||||
});
|
||||
|
||||
it('unmute by moderator', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await unmuteByModerator(p1, p3, true, true);
|
||||
|
||||
// moderation is stopped at this point, make sure participants 1 & 2 are also unmuted,
|
||||
// participant3 was unmuted by unmuteByModerator
|
||||
await unmuteAudioAndCheck(p2, p1);
|
||||
await unmuteVideoAndCheck(p2, p1);
|
||||
await unmuteAudioAndCheck(p1, p2);
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
});
|
||||
|
||||
it('hangup and change moderator', async () => {
|
||||
await Promise.all([ ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureThreeParticipants(ctx);
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p2.getToolbar().clickRaiseHandButton();
|
||||
await p3.getToolbar().clickRaiseHandButton();
|
||||
|
||||
await p1.hangup();
|
||||
|
||||
// we don't use ensureThreeParticipants to avoid all meeting join checks
|
||||
// all participants are muted and checks for media will fail
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
// After p1 re-joins either p2 or p3 is promoted to moderator. They should still be muted.
|
||||
const isP2Moderator = await p2.isModerator();
|
||||
const moderator = isP2Moderator ? p2 : p3;
|
||||
const nonModerator = isP2Moderator ? p3 : p2;
|
||||
const moderatorParticipantsPane = moderator.getParticipantsPane();
|
||||
const nonModeratorParticipantsPane = nonModerator.getParticipantsPane();
|
||||
|
||||
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
|
||||
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
|
||||
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(nonModerator, p1);
|
||||
await unmuteVideoAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
});
|
||||
it('grant moderator', async () => {
|
||||
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureThreeParticipants(ctx);
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p1.getFilmstrip().grantModerator(p3);
|
||||
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.isModerator(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: `${p3.name} is not moderator`
|
||||
});
|
||||
|
||||
await unmuteByModerator(p3, p2, false, true);
|
||||
});
|
||||
it('ask to unmute', async () => {
|
||||
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureTwoParticipants(ctx);
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// mute p2
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// ask p2 to unmute
|
||||
await p1.getParticipantsPane().askToUnmute(p2, true);
|
||||
|
||||
await p2.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await p1.getParticipantsPane().close();
|
||||
});
|
||||
it('remove from whitelist', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await unmuteByModerator(p1, p2, true, false);
|
||||
|
||||
// p1 mute audio on p2 and check
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
|
||||
// we try to unmute and test it that it was still muted
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
|
||||
// stop video and check
|
||||
await p1.getFilmstrip().muteVideo(p2);
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
await tryToVideoUnmuteAndCheck(p2, p1);
|
||||
});
|
||||
it('join moderated', async () => {
|
||||
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
const p1ParticipantsPane = ctx.p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
// join with second participant and check
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
await tryToVideoUnmuteAndCheck(p2, p1);
|
||||
|
||||
// asked to unmute and check
|
||||
await unmuteByModerator(p1, p2, false, false);
|
||||
|
||||
// mute and check
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks a user can unmute after being asked by moderator.
|
||||
* @param moderator - The participant that is moderator.
|
||||
* @param participant - The participant being asked to unmute.
|
||||
* @param turnOnModeration - if we want to turn on moderation before testing (when it is currently off).
|
||||
* @param stopModeration - true if moderation to be stopped when done.
|
||||
*/
|
||||
async function unmuteByModerator(
|
||||
moderator: Participant,
|
||||
participant: Participant,
|
||||
turnOnModeration: boolean,
|
||||
stopModeration: boolean) {
|
||||
const moderatorParticipantsPane = moderator.getParticipantsPane();
|
||||
|
||||
if (turnOnModeration) {
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await moderatorParticipantsPane.close();
|
||||
}
|
||||
|
||||
// raise hand to speak
|
||||
await participant.getToolbar().clickRaiseHandButton();
|
||||
await moderator.getNotifications().waitForRaisedHandNotification();
|
||||
|
||||
// ask participant to unmute
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await moderatorParticipantsPane.askToUnmute(participant, false);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(participant, moderator);
|
||||
await unmuteVideoAndCheck(participant, moderator);
|
||||
|
||||
if (stopModeration) {
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
|
||||
await moderatorParticipantsPane.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of moderation, tries to audio unmute but stays muted.
|
||||
* Checks locally and remotely that this is still the case.
|
||||
* @param participant
|
||||
* @param observer
|
||||
*/
|
||||
async function tryToAudioUnmuteAndCheck(participant: Participant, observer: Participant) {
|
||||
// try to audio unmute and check
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Check local audio muted icon state
|
||||
await participant.getFilmstrip().assertAudioMuteIconIsDisplayed(participant);
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of moderation, tries to video unmute but stays muted.
|
||||
* Checks locally and remotely that this is still the case.
|
||||
* @param participant
|
||||
* @param observer
|
||||
*/
|
||||
async function tryToVideoUnmuteAndCheck(participant: Participant, observer: Participant) {
|
||||
// try to video unmute and check
|
||||
await participant.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
// Check local audio muted icon state
|
||||
await participant.getParticipantsPane().assertVideoMuteIconIsDisplayed(participant);
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(participant);
|
||||
}
|
||||
202
tests/specs/3way/avatars.spec.ts
Normal file
202
tests/specs/3way/avatars.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
const EMAIL = 'support@jitsi.org';
|
||||
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
|
||||
|
||||
describe('Avatar - ', () => {
|
||||
it('setup the meeting', async () => {
|
||||
// Start p1
|
||||
await 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;
|
||||
|
||||
// check default avatar for p1 on p2
|
||||
await p2.assertDefaultAvatarExist(p1);
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
const settings = p1.getSettingsDialog();
|
||||
|
||||
await settings.waitForDisplay();
|
||||
await settings.setEmail(EMAIL);
|
||||
await settings.submit();
|
||||
|
||||
// check if the local avatar in the toolbar menu has changed
|
||||
await p1.driver.waitUntil(
|
||||
async () => (await p1.getToolbar().getProfileImage())?.includes(HASH), {
|
||||
timeout: 3000, // give more time for the initial download of the image
|
||||
timeoutMsg: 'Avatar has not changed for p1'
|
||||
});
|
||||
|
||||
// check if the avatar in the local thumbnail has changed
|
||||
expect(await p1.getLocalVideoAvatar()).toContain(HASH);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
async () => (await p2.getFilmstrip().getAvatar(p1EndpointId))?.includes(HASH), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Avatar has not changed for p1 on p2'
|
||||
});
|
||||
|
||||
// check if the avatar in the large video has changed
|
||||
expect(await p2.getLargeVideoAvatar()).toContain(HASH);
|
||||
|
||||
// we check whether the default avatar of participant2 is displayed on both sides
|
||||
await p1.assertDefaultAvatarExist(p2);
|
||||
await p2.assertDefaultAvatarExist(p2);
|
||||
|
||||
// the problem on FF where we can send keys to the input field,
|
||||
// and the m from the text can mute the call, check whether we are muted
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
});
|
||||
|
||||
it('when video muted', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await ctx.p2.hangup();
|
||||
|
||||
// Mute p1's video
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => (await p1.getLargeVideoAvatar())?.includes(HASH), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
const p1LargeSrc = await p1.getLargeVideoAvatar();
|
||||
const p1ThumbSrc = await p1.getLocalVideoAvatar();
|
||||
|
||||
// Check if avatar on large video is the same as on local thumbnail
|
||||
expect(p1ThumbSrc).toBe(p1LargeSrc);
|
||||
|
||||
// Join p2
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipDisplayName: true
|
||||
});
|
||||
const { p2 } = ctx;
|
||||
|
||||
// Verify that p1 is muted from the perspective of p2
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p2.getFilmstrip().pinParticipant(p1);
|
||||
|
||||
// Check if p1's avatar is on large video now
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getLargeVideoAvatar() === p1LargeSrc, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
// p1 pins p2's video
|
||||
await p1.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
// Check if avatar is displayed on p1's local video thumbnail
|
||||
await p1.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
|
||||
// Unmute - now local avatar should be hidden and local video displayed
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
|
||||
await p1.asserLocalThumbnailShowsVideo();
|
||||
|
||||
// Now both p1 and p2 have video muted
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p2.getToolbar().clickVideoMuteButton();
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
// Start the third participant
|
||||
await ensureThreeParticipants(ctx);
|
||||
const { p3 } = ctx;
|
||||
|
||||
// Pin local video and verify avatars are displayed
|
||||
await p3.getFilmstrip().pinParticipant(p3);
|
||||
|
||||
await p3.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p2, false, true);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
|
||||
expect(await p3.getFilmstrip().getAvatar(p1EndpointId)).toBe(p1ThumbSrc);
|
||||
|
||||
// Click on p1's video
|
||||
await p3.getFilmstrip().pinParticipant(p1);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.getLargeVideoResource() === p1EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${p1.name}`
|
||||
});
|
||||
|
||||
await p3.assertDisplayNameVisibleOnStage(
|
||||
await p3.getFilmstrip().getRemoteDisplayName(p1EndpointId));
|
||||
|
||||
// p2 has the default avatar
|
||||
await p3.assertThumbnailShowsAvatar(p2, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p3, true);
|
||||
|
||||
// Click on p2's video
|
||||
await p3.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.getLargeVideoResource() === p2EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${p2.name}`
|
||||
});
|
||||
|
||||
await p3.assertDisplayNameVisibleOnStage(
|
||||
await p3.getFilmstrip().getRemoteDisplayName(p2EndpointId)
|
||||
);
|
||||
|
||||
await p3.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p3, true);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Unmute p1's and p2's videos
|
||||
await p1.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
});
|
||||
|
||||
it('email persistence', async () => {
|
||||
let { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
|
||||
await p1.hangup();
|
||||
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipDisplayName: true,
|
||||
skipFirstModerator: true
|
||||
});
|
||||
p1 = ctx.p1;
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"include": ["**/*.ts", "../globals.d.ts"],
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../globals.d.ts",
|
||||
"./globals.d.ts"
|
||||
],
|
||||
"extends": "../tsconfig.web",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"@types/jasmine",
|
||||
"@wdio/jasmine-framework"
|
||||
"@types/mocha",
|
||||
"@wdio/mocha-framework"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import AllureReporter from '@wdio/allure-reporter';
|
||||
import { multiremotebrowser } from '@wdio/globals';
|
||||
import { Buffer } from 'buffer';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import pretty from 'pretty';
|
||||
|
||||
import WebhookProxy from './helpers/WebhookProxy';
|
||||
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
|
||||
import { IContext } from './helpers/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const allure = require('allure-commandline');
|
||||
@@ -24,7 +28,11 @@ const chromeArgs = [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
|
||||
|
||||
// Avoids - "You are checking for animations on an inactive tab, animations do not run for inactive tabs"
|
||||
// when executing waitForStable()
|
||||
'--disable-renderer-backgrounding',
|
||||
`--use-file-for-fake-audio-capture=${process.env.REMOTE_RESOURCE_PATH || 'tests/resources'}/fakeAudioStream.wav`
|
||||
];
|
||||
|
||||
if (process.env.RESOLVER_RULES) {
|
||||
@@ -56,7 +64,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
],
|
||||
maxInstances: 1,
|
||||
|
||||
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture',
|
||||
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/',
|
||||
tsConfigPath: './tsconfig.json',
|
||||
|
||||
// Default timeout for all waitForXXX commands.
|
||||
@@ -69,10 +77,10 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
// Default request retries count
|
||||
connectionRetryCount: 3,
|
||||
|
||||
framework: 'jasmine',
|
||||
framework: 'mocha',
|
||||
|
||||
jasmineOpts: {
|
||||
defaultTimeoutInterval: 60_000
|
||||
mochaOpts: {
|
||||
timeout: 60_000
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
@@ -105,6 +113,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
prefs: chromePreferences
|
||||
},
|
||||
'wdio:exclude': [
|
||||
'specs/alone/**',
|
||||
'specs/2way/**'
|
||||
]
|
||||
}
|
||||
@@ -117,6 +126,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
prefs: chromePreferences
|
||||
},
|
||||
'wdio:exclude': [
|
||||
'specs/alone/**',
|
||||
'specs/2way/**',
|
||||
'specs/3way/**'
|
||||
]
|
||||
}
|
||||
@@ -157,19 +168,64 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
before() {
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
initLogger(multiremotebrowser.getInstance(instance), instance, TEST_RESULTS_DIR);
|
||||
});
|
||||
async before() {
|
||||
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
|
||||
const bInstance = multiremotebrowser.getInstance(instance);
|
||||
|
||||
initLogger(bInstance, instance, TEST_RESULTS_DIR);
|
||||
|
||||
if (bInstance.isFirefox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (process.env.GRID_HOST_URL) {
|
||||
// TODO: make sure we use uploadFile only with chrome (it does not work with FF),
|
||||
// we need to test it with the grid and FF, does it work there
|
||||
const rpath = await bInstance.uploadFile('tests/resources/iframeAPITest.html');
|
||||
|
||||
// @ts-ignore
|
||||
bInstance.iframePageBase = `file://${path.dirname(rpath)}`;
|
||||
}));
|
||||
|
||||
const globalAny: any = global;
|
||||
const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
|
||||
|
||||
globalAny.ctx = {} as IContext;
|
||||
globalAny.ctx.roomName = roomName;
|
||||
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
|
||||
globalAny.ctx.jwtKid = process.env.JWT_KID;
|
||||
},
|
||||
|
||||
after() {
|
||||
const { ctx }: any = global;
|
||||
|
||||
if (ctx.webhooksProxy) {
|
||||
ctx.webhooksProxy.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets executed before the suite starts (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} suite - Suite details.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
beforeSuite(suite) {
|
||||
const { ctx }: any = global;
|
||||
|
||||
// If we are running the iFrameApi tests, we need to mark it as such and if needed to create the proxy
|
||||
// and connect to it.
|
||||
if (path.basename(suite.file).startsWith('iFrameApi')) {
|
||||
ctx.iframeAPI = true;
|
||||
|
||||
if (!ctx.webhooksProxy
|
||||
&& process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
|
||||
ctx.webhooksProxy = new WebhookProxy(
|
||||
`${process.env.WEBHOOKS_PROXY_URL}&room=${ctx.roomName}`,
|
||||
process.env.WEBHOOKS_PROXY_SHARED_SECRET);
|
||||
ctx.webhooksProxy.connect();
|
||||
}
|
||||
}
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance),
|
||||
`---=== Begin ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
|
||||
@@ -180,11 +236,13 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
* Function to be executed before a test (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} test - Test object.
|
||||
* @returns {Promise<void>}
|
||||
* @param {Object} context - The context object.
|
||||
*/
|
||||
beforeTest(test) {
|
||||
beforeTest(test, context) {
|
||||
ctx.skipSuiteTests && context.skip();
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.fullName} ===---`);
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.title} ===---`);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -198,7 +256,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
*/
|
||||
async afterTest(test, context, { error }) {
|
||||
multiremotebrowser.instances.forEach((instance: string) =>
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.fullName} ===---`));
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.title} ===---`));
|
||||
|
||||
if (error) {
|
||||
const allProcessing: Promise<any>[] = [];
|
||||
@@ -217,7 +275,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
|
||||
|
||||
allProcessing.push(bInstance.getPageSource().then(source => {
|
||||
AllureReporter.addAttachment(`html-source-${instance}`, source, 'text/plain');
|
||||
AllureReporter.addAttachment(`html-source-${instance}`, pretty(source), 'text/plain');
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -270,4 +328,4 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
} as WebdriverIO.MultiremoteConfig;
|
||||
|
||||
11
tests/wdio.dev.conf.ts
Normal file
11
tests/wdio.dev.conf.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// wdio.dev.conf.ts
|
||||
// extends te main configuration file for the development environment (make dev)
|
||||
// it will connect to the webpack-dev-server running locally on port 8080
|
||||
import { deepmerge } from 'deepmerge-ts';
|
||||
|
||||
// @ts-ignore
|
||||
import { config as defaultConfig } from './wdio.conf.ts';
|
||||
|
||||
export const config = deepmerge(defaultConfig, {
|
||||
baseUrl: 'https://127.0.0.1:8080/torture'
|
||||
}, { clone: false });
|
||||
39
tests/wdio.firefox.conf.ts
Normal file
39
tests/wdio.firefox.conf.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// wdio.firefox.conf.ts
|
||||
// extends te main configuration file changing first participant to be Firefox
|
||||
import { merge } from 'lodash-es';
|
||||
import process from 'node:process';
|
||||
|
||||
// @ts-ignore
|
||||
import { config as defaultConfig } from './wdio.conf.ts';
|
||||
|
||||
const ffArgs = [];
|
||||
|
||||
const ffPreferences = {
|
||||
'intl.accept_languages': 'en-US',
|
||||
'media.navigator.permission.disabled': true,
|
||||
'media.navigator.streams.fake': true,
|
||||
'media.autoplay.default': 0
|
||||
};
|
||||
|
||||
if (process.env.HEADLESS === 'true') {
|
||||
ffArgs.push('--headless');
|
||||
}
|
||||
|
||||
export const config = merge(defaultConfig, {
|
||||
exclude: [
|
||||
'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
|
||||
'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
|
||||
],
|
||||
capabilities: {
|
||||
participant1: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
'moz:firefoxOptions': {
|
||||
args: ffArgs,
|
||||
prefs: ffPreferences
|
||||
},
|
||||
acceptInsecureCerts: process.env.ALLOW_INSECURE_CERTS === 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { clone: false });
|
||||
18
tests/wdio.grid.conf.ts
Normal file
18
tests/wdio.grid.conf.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// wdio.grid.conf.ts
|
||||
// extends the main configuration file to add the selenium grid address
|
||||
import { deepmerge } from 'deepmerge-ts';
|
||||
import { URL } from 'url';
|
||||
|
||||
// @ts-ignore
|
||||
import { config as defaultConfig } from './wdio.conf.ts';
|
||||
|
||||
const gridUrl = new URL(process.env.GRID_HOST_URL as string);
|
||||
const protocol = gridUrl.protocol.replace(':', '');
|
||||
|
||||
export const config = deepmerge(defaultConfig, {
|
||||
protocol,
|
||||
hostname: gridUrl.hostname,
|
||||
port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number
|
||||
: protocol === 'http' ? 80 : 443,
|
||||
path: gridUrl.pathname
|
||||
}, { clone: false });
|
||||
@@ -253,7 +253,10 @@ function getDevServerConfig() {
|
||||
],
|
||||
server: process.env.CODESPACES ? 'http' : 'https',
|
||||
static: {
|
||||
directory: process.cwd()
|
||||
directory: process.cwd(),
|
||||
watch: {
|
||||
ignored: file => file.endsWith('.log')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user