jitsi-meet/tests/helpers/Participant.ts
theluyuan 38ba663466
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
init
2025-09-02 14:49:16 +08:00

974 lines
30 KiB
TypeScript

/* global APP $ */
import { multiremotebrowser } from '@wdio/globals';
import assert from 'assert';
import { Key } from 'webdriverio';
import { IConfig } from '../../react/features/base/config/configType';
import { urlObjectToString } from '../../react/features/base/util/uri';
import BreakoutRooms from '../pageobjects/BreakoutRooms';
import ChatPanel from '../pageobjects/ChatPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import InviteDialog from '../pageobjects/InviteDialog';
import LargeVideo from '../pageobjects/LargeVideo';
import LobbyScreen from '../pageobjects/LobbyScreen';
import Notifications, {
MAX_USERS_TEST_ID,
TOKEN_AUTH_FAILED_TEST_ID,
TOKEN_AUTH_FAILED_TITLE_TEST_ID
} from '../pageobjects/Notifications';
import ParticipantsPane from '../pageobjects/ParticipantsPane';
import PasswordDialog from '../pageobjects/PasswordDialog';
import PreJoinScreen from '../pageobjects/PreJoinScreen';
import SecurityDialog from '../pageobjects/SecurityDialog';
import SettingsDialog from '../pageobjects/SettingsDialog';
import Toolbar from '../pageobjects/Toolbar';
import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
import Visitors from '../pageobjects/Visitors';
import { config as testsConfig } from './TestsConfig';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IToken } from './token';
import { IParticipantJoinOptions, IParticipantOptions } from './types';
export const P1 = 'p1';
export const P2 = 'p2';
export const P3 = 'p3';
export const P4 = 'p4';
/**
* Participant.
*/
export class Participant {
/**
* The current context.
*
* @private
*/
private _name: string;
private _endpointId: string;
/**
* The token that this participant was initialized with.
*/
private _token?: IToken;
/**
* Cache the dial in pin code so that it doesn't have to be read from the UI.
*/
private _dialInPin?: string;
private _iFrameApi: boolean = false;
/**
* The default config to use when joining.
*
* @private
*/
private config = {
analytics: {
disabled: true
},
// if there is a video file to play, use deployment config,
// otherwise use lower resolution to avoid high CPU usage
constraints: process.env.VIDEO_CAPTURE_FILE ? undefined : {
video: {
height: {
ideal: 360,
max: 360,
min: 180
},
// @ts-ignore
width: {
ideal: 640,
max: 640,
min: 320
},
frameRate: {
max: 30
}
}
},
resolution: process.env.VIDEO_CAPTURE_FILE ? undefined : 360,
requireDisplayName: false,
testing: {
testMode: true
},
disableAP: true,
disable1On1Mode: true,
disableModeratorIndicator: true,
enableTalkWhileMuted: false,
gatherStats: true,
p2p: {
enabled: false,
useStunTurn: false
},
pcStatsInterval: 1500,
prejoinConfig: {
enabled: false
},
toolbarConfig: {
alwaysVisible: true
}
} as IConfig;
/**
* Creates a participant with given options.
*/
constructor(options: IParticipantOptions) {
this._name = options.name;
this._token = options.token;
this._iFrameApi = options.iFrameApi || false;
}
/**
* A wrapper for <tt>this.driver.execute</tt> that would catch errors, print them and throw them again.
*
* @param {string | ((...innerArgs: InnerArguments) => ReturnValue)} script - The script that will be executed.
* @param {any[]} args - The rest of the arguments.
* @returns {ReturnValue} - The result of the script.
*/
async execute<ReturnValue, InnerArguments extends any[]>(
script: string | ((...innerArgs: InnerArguments) => ReturnValue),
...args: InnerArguments): Promise<ReturnValue> {
try {
// @ts-ignore
return await this.driver.execute(script, ...args);
} catch (error) {
console.error('An error occurred while trying to execute a script: ', error);
throw error;
}
}
/**
* Returns participant endpoint ID.
*
* @returns {Promise<string>} The endpoint ID.
*/
async getEndpointId(): Promise<string> {
if (!this._endpointId) {
this._endpointId = await this.execute(() => { // eslint-disable-line arrow-body-style
return APP?.conference?.getMyUserId();
});
}
return this._endpointId;
}
/**
* The driver it uses.
*/
get driver() {
return multiremotebrowser.getInstance(this._name);
}
/**
* The name.
*/
get name() {
return this._name;
}
/**
* Adds a log to the participants log file.
*
* @param {string} message - The message to log.
* @returns {void}
*/
log(message: string): void {
logInfo(this.driver, message);
}
/**
* Joins conference.
*
* @param {IParticipantJoinOptions} options - Options for joining.
* @returns {Promise<void>}
*/
async joinConference(options: IParticipantJoinOptions): Promise<Participant> {
const config = {
room: options.roomName,
configOverwrite: {
...this.config,
...options.configOverwrite || {}
},
interfaceConfigOverwrite: {
SHOW_CHROME_EXTENSION_BANNER: false
}
};
if (!options.skipDisplayName) {
// @ts-ignore
config.userInfo = {
displayName: this._name
};
}
if (this._iFrameApi) {
config.room = 'iframeAPITest.html';
}
let url = urlObjectToString(config) || '';
if (this._iFrameApi) {
const baseUrl = new URL(this.driver.options.baseUrl || '');
// @ts-ignore
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${options.roomName}"`;
if (testsConfig.iframe.tenant) {
url = `${url}&tenant="${testsConfig.iframe.tenant}"`;
} else if (baseUrl.pathname.length > 1) {
// remove leading slash
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
}
}
if (this._token?.jwt) {
url = `${url}&jwt="${this._token.jwt}"`;
}
await this.driver.setTimeout({ 'pageLoad': 30000 });
// drop the leading '/' so we can use the tenant if any
url = url.startsWith('/') ? url.substring(1) : url;
if (options.forceTenant) {
url = `/${options.forceTenant}/${url}`;
}
await this.driver.url(url);
await this.waitForPageToLoad();
if (this._iFrameApi) {
const mainFrame = this.driver.$('iframe');
await this.driver.switchFrame(mainFrame);
}
if (!options.skipWaitToJoin) {
await this.waitForMucJoinedOrError();
}
await this.postLoadProcess();
return this;
}
/**
* Loads stuff after the page loads.
*
* @returns {Promise<void>}
* @private
*/
private async postLoadProcess(): Promise<void> {
const driver = this.driver;
const parallel = [];
parallel.push(this.execute((name, sessionId, prefix) => {
APP?.UI?.dockToolbar(true);
// disable keyframe animations (.fadeIn and .fadeOut classes)
$('<style>.notransition * { '
+ 'animation-duration: 0s !important; -webkit-animation-duration: 0s !important; transition:none; '
+ '} </style>') // @ts-ignore
.appendTo(document.head);
// @ts-ignore
$('body').toggleClass('notransition');
document.title = `${name}`;
console.log(`${new Date().toISOString()} ${prefix} sessionId: ${sessionId}`);
// disable the blur effect in firefox as it has some performance issues
const blur = document.querySelector('.video_blurred_container');
if (blur) {
// @ts-ignore
document.querySelector('.video_blurred_container').style.display = 'none';
}
}, this._name, driver.sessionId, LOG_PREFIX));
await Promise.all(parallel);
}
/**
* Waits for the page to load.
*
* @returns {Promise<boolean>}
*/
async waitForPageToLoad(): Promise<boolean> {
return this.driver.waitUntil(
() => this.execute(() => {
console.log(`${new Date().toISOString()} document.readyState: ${document.readyState}`);
return document.readyState === 'complete';
}),
{
timeout: 30_000, // 30 seconds
timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
}
);
}
/**
* Waits for the tile view to display.
*/
async waitForTileViewDisplay(reverse = false) {
await this.driver.$('//div[@id="videoconference_page" and contains(@class, "tile-view")]').waitForDisplayed({
reverse,
timeout: 10_000,
timeoutMsg: `Tile view did not display in 10s for ${this.name}`
});
}
/**
* Checks if the participant is in the meeting.
*/
isInMuc() {
return this.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
}
/**
* Waits until either the MUC is joined, or a password prompt is displayed, or an authentication failure
* notification is displayed, or max users notification is displayed.
*/
async waitForMucJoinedOrError(): Promise<void> {
await this.driver.waitUntil(async () => {
return await this.isInMuc() || await this.getPasswordDialog().isOpen()
|| await this.getNotifications().getNotificationText(MAX_USERS_TEST_ID)
|| await this.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID)
|| await this.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
}, {
timeout: 10_000,
timeoutMsg: 'Timeout waiting for MUC joined or error.'
});
}
/**
* Checks if the participant is a moderator in the meeting.
*/
async isModerator() {
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
}
async isVisitor() {
return await this.execute(() => APP?.store?.getState()['features/visitors']?.iAmVisitor || false);
}
/**
* Checks if the meeting supports breakout rooms.
*/
async isBreakoutRoomsSupported() {
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
}
/**
* Checks if the participant is in breakout room.
*/
async isInBreakoutRoom() {
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
}
/**
* Waits to join the muc.
*
* @returns {Promise<void>}
*/
async waitToJoinMUC(): Promise<void> {
return this.driver.waitUntil(
() => this.isInMuc(),
{
timeout: 10_000, // 10 seconds
timeoutMsg: `Timeout waiting to join muc for ${this.name}`
}
);
}
/**
* Waits for ICE to get connected.
*
* @returns {Promise<boolean>}
*/
waitForIceConnected(): Promise<boolean> {
return this.driver.waitUntil(() =>
this.execute(() => APP?.conference?.getConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
});
}
/**
* Waits for ICE to get connected on the p2p connection.
*
* @returns {Promise<boolean>}
*/
waitForP2PIceConnected(): Promise<boolean> {
return this.driver.waitUntil(() =>
this.execute(() => APP?.conference?.getP2PConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected P2P ICE to be connected for 15s for ${this.name}`
});
}
/**
* Waits until the conference stats show positive upload and download bitrate (independently).
*
* @returns {Promise<boolean>}
*/
async waitForSendReceiveData(timeout = 15_000, msg?: string): Promise<boolean> {
const values = await Promise.all([
await this.waitForSendMedia(timeout, msg ? `${msg} (send)` : undefined),
await this.waitForReceiveMedia(timeout, msg ? `${msg} (receive)` : undefined)
]);
return values[0] && values[1];
}
/**
* Waits until the conference stats show positive upload bitrate.
* @param timeout max time to wait in ms
* @param timeoutMsg the message to log if the timeout is reached
*/
async waitForSendMedia(
timeout = 15_000,
timeoutMsg = `expected to send media in ${timeout / 1000}s for ${this.name}`): Promise<boolean> {
return this.driver.waitUntil(() => this.execute(() => {
return APP?.conference?.getStats()?.bitrate?.upload > 0;
}), {
timeout,
timeoutMsg
});
}
/**
* Waits until the conference stats show positive upload bitrate.
* @param timeout max time to wait in ms
* @param timeoutMsg the message to log if the timeout is reached
*/
async waitForReceiveMedia(
timeout = 15_000,
timeoutMsg = `expected to receive media in ${timeout / 1000}s for ${this.name}`): Promise<boolean> {
return this.driver.waitUntil(() => this.execute(() => {
return APP?.conference?.getStats()?.bitrate?.download > 0;
}), {
timeout,
timeoutMsg
});
}
/**
* Waits until there are at least [number] participants that have at least one track.
*
* @param {number} number - The number of remote streams to wait for.
* @returns {Promise<boolean>}
*/
async waitForRemoteStreams(number: number): Promise<boolean> {
return await this.driver.waitUntil(async () => await this.execute(
count => (APP?.conference?.getNumberOfParticipantsWithTracks() ?? -1) >= count,
number
), {
timeout: 15_000,
timeoutMsg: `expected number of remote streams:${number} in 15s for ${this.name}`
});
}
/**
* Waits until the number of participants is exactly the given number.
*
* @param {number} number - The number of participant to wait for.
* @param {string} msg - A custom message to use.
* @returns {Promise<boolean>}
*/
waitForParticipants(number: number, msg?: string): Promise<boolean> {
return this.driver.waitUntil(
() => this.execute(count => (APP?.conference?.listMembers()?.length ?? -1) === count, number),
{
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
});
}
/**
* Returns the chat panel for this participant.
*/
getChatPanel(): ChatPanel {
return new ChatPanel(this);
}
/**
* Returns the BreakoutRooms for this participant.
*
* @returns {BreakoutRooms}
*/
getBreakoutRooms(): BreakoutRooms {
return new BreakoutRooms(this);
}
/**
* Returns the toolbar for this participant.
*
* @returns {Toolbar}
*/
getToolbar(): Toolbar {
return new Toolbar(this);
}
/**
* Returns the filmstrip for this participant.
*
* @returns {Filmstrip}
*/
getFilmstrip(): Filmstrip {
return new Filmstrip(this);
}
/**
* Returns the invite dialog for this participant.
*
* @returns {InviteDialog}
*/
getInviteDialog(): InviteDialog {
return new InviteDialog(this);
}
/**
* Returns the notifications.
*/
getNotifications(): Notifications {
return new Notifications(this);
}
/**
* Returns the participants pane.
*
* @returns {ParticipantsPane}
*/
getParticipantsPane(): ParticipantsPane {
return new ParticipantsPane(this);
}
/**
* Returns the large video page object.
*
* @returns {LargeVideo}
*/
getLargeVideo(): LargeVideo {
return new LargeVideo(this);
}
/**
* Returns the videoQuality Dialog.
*
* @returns {VideoQualityDialog}
*/
getVideoQualityDialog(): VideoQualityDialog {
return new VideoQualityDialog(this);
}
/**
* Returns the security Dialog.
*
* @returns {SecurityDialog}
*/
getSecurityDialog(): SecurityDialog {
return new SecurityDialog(this);
}
/**
* Returns the settings Dialog.
*
* @returns {SettingsDialog}
*/
getSettingsDialog(): SettingsDialog {
return new SettingsDialog(this);
}
/**
* Returns the password dialog.
*/
getPasswordDialog(): PasswordDialog {
return new PasswordDialog(this);
}
/**
* Returns the prejoin screen.
*/
getPreJoinScreen(): PreJoinScreen {
return new PreJoinScreen(this);
}
/**
* Returns the lobby screen.
*/
getLobbyScreen(): LobbyScreen {
return new LobbyScreen(this);
}
/**
* Returns the Visitors page object.
*
* @returns {Visitors}
*/
getVisitors(): Visitors {
return new Visitors(this);
}
/**
* Switches to the iframe API context
*/
async switchToAPI() {
await this.driver.switchFrame(null);
}
/**
* Switches to the meeting page context.
*/
switchInPage() {
const mainFrame = this.driver.$('iframe');
return 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() {
const current = await this.driver.getUrl();
// already hangup
if (current.endsWith('/base.html')) {
return;
}
// do a hangup, to make sure unavailable presence is sent
await this.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
await this.driver.waitUntil(
async () => {
const u = await this.driver.getUrl();
// trying to debug some failures of reporting not leaving, where we see the close page in screenshot
console.log(`initialUrl: ${current} currentUrl: ${u}`);
return current !== u;
},
{
timeout: 8000,
timeoutMsg: `${this.name} did not leave the muc in 8s initialUrl: ${current}`
}
);
await this.driver.url('/base.html')
// This was fixed in wdio v9.9.1, we can drop once we update to that version
.catch(_ => {}); // eslint-disable-line @typescript-eslint/no-empty-function
}
/**
* Returns the local display name element.
* @private
*/
private async getLocalDisplayNameElement() {
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
await localVideoContainer.moveTo();
return localVideoContainer.$('span[id="localDisplayName"]');
}
/**
* Returns the local display name.
*/
async getLocalDisplayName() {
return (await this.getLocalDisplayNameElement()).getText();
}
/**
* Sets the display name of the local participant.
*/
async setLocalDisplayName(displayName: string) {
const localDisplayName = await this.getLocalDisplayNameElement();
await localDisplayName.click();
await this.driver.keys(displayName);
await this.driver.keys(Key.Return);
// just click somewhere to lose focus, to make sure editing has ended
const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
await localVideoContainer.moveTo();
await localVideoContainer.click();
}
/**
* 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;
}
/**
* 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);
}
/**
* Checks if the leave reason dialog is open.
*/
async isLeaveReasonDialogOpen() {
return this.driver.$('div[data-testid="dialog.leaveReason"]').isDisplayed();
}
/**
* Returns the audio level for a participant.
*
* @param p
* @return
*/
async getRemoteAudioLevel(p: Participant) {
const jid = await p.getEndpointId();
return await this.execute(id => {
const level = APP?.conference?.getPeerSSRCAudioLevel(id);
return level ? level.toFixed(2) : null;
}, jid);
}
/**
* For the participant to have his audio muted/unmuted from given observer's
* perspective. The method will fail the test if something goes wrong or
* the audio muted status is different than the expected one. We wait up to
* 3 seconds for the expected status to appear.
*
* @param testee - instance of the participant for whom we're checking the audio muted status.
* @param muted - <tt>true</tt> to wait for audio muted status or <tt>false</tt> to wait for the participant to
* unmute.
*/
async waitForAudioMuted(testee: Participant, muted: boolean): Promise<void> {
// Waits for the correct icon
await this.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, !muted);
// Extended timeout for 'unmuted' to make tests more resilient to
// unexpected glitches.
const timeout = muted ? 3_000 : 6_000;
// Give it 3 seconds to not get any audio or to receive some
// depending on "muted" argument
try {
await this.driver.waitUntil(async () => {
const audioLevel = await this.getRemoteAudioLevel(testee);
if (muted) {
if (audioLevel !== null && audioLevel > 0.1) {
console.log(`muted exiting on: ${audioLevel}`);
return true;
}
return false;
}
// When testing for unmuted we wait for first sound
if (audioLevel !== null && audioLevel > 0.1) {
console.log(`unmuted exiting on: ${audioLevel}`);
return true;
}
return false;
},
{ timeout });
// When testing for muted we don't want to have
// the condition succeeded
if (muted) {
assert.fail(`There was some sound coming from muted: '${this.name}'`);
} // else we're good for unmuted participant
} catch (_timeoutE) {
if (!muted) {
assert.fail(`There was no sound from unmuted: '${this.name}'`);
} // else we're good for muted participant
}
}
/**
* Checks if video is currently received for the given remote endpoint ID (there is a track, it's not muted,
* and it's streaming status according to the connection-indicator is active).
*/
async isRemoteVideoReceived(endpointId: string): Promise<boolean> {
return this.execute(e => JitsiMeetJS.app.testing.isRemoteVideoReceived(e), endpointId);
}
/**
* Checks if the remove video is displayed for the given remote endpoint ID.
* @param endpointId
*/
async isRemoteVideoDisplayed(endpointId: string): Promise<boolean> {
return this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting();
}
/**
* Check if remote video for a specific remote endpoint is both received and displayed.
* @param endpointId
*/
async isRemoteVideoReceivedAndDisplayed(endpointId: string): Promise<boolean> {
return await this.isRemoteVideoReceived(endpointId) && await this.isRemoteVideoDisplayed(endpointId);
}
/**
* Waits for remote video state - receiving and displayed.
* @param endpointId
* @param reverse if true, waits for the remote video to NOT be received AND NOT displayed.
*/
async waitForRemoteVideo(endpointId: string, reverse = false) {
if (reverse) {
await this.driver.waitUntil(async () =>
!await this.isRemoteVideoReceived(endpointId) && !await this.isRemoteVideoDisplayed(endpointId), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.name}`
});
} else {
await this.driver.waitUntil(async () =>
await this.isRemoteVideoReceivedAndDisplayed(endpointId), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
});
}
}
/**
* Waits for ninja icon to be displayed.
* @param endpointId When no endpoint id is passed we check for any ninja icon.
*/
async waitForNinjaIcon(endpointId?: string) {
if (endpointId) {
await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000,
timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
});
} else {
await this.driver.$('//span[contains(@class,"videocontainer")]//span[contains(@class,"connection_ninja")]')
.waitForDisplayed({
timeout: 5_000,
timeoutMsg: `expected ninja icon to be displayed in 5s by ${this.name}`
});
}
}
/**
* Waits for dominant speaker icon to appear in remote video of a participant.
* @param endpointId the endpoint ID of the participant whose dominant speaker icon status will be checked.
*/
waitForDominantSpeaker(endpointId: string) {
return this.driver.$(`//span[@id="participant_${endpointId}" and contains(@class, "dominant-speaker")]`)
.waitForDisplayed({ timeout: 5_000 });
}
/**
* Returns the token that this participant was initialized with.
*/
getToken(): IToken | undefined {
return this._token;
}
/**
* Gets the dial in pin for the conference. Reads it from the invite dialog if the pin hasn't been cached yet.
*/
async getDialInPin(): Promise<string> {
if (!this._dialInPin) {
const dialInPin = await this.getInviteDialog().getPinNumber();
await this.getInviteDialog().clickCloseButton();
this._dialInPin = dialInPin;
}
return this._dialInPin;
}
}