您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

Participant.ts 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. /* global APP $ */
  2. import { multiremotebrowser } from '@wdio/globals';
  3. import assert from 'assert';
  4. import { Key } from 'webdriverio';
  5. import { IConfig } from '../../react/features/base/config/configType';
  6. import { urlObjectToString } from '../../react/features/base/util/uri';
  7. import BreakoutRooms from '../pageobjects/BreakoutRooms';
  8. import ChatPanel from '../pageobjects/ChatPanel';
  9. import Filmstrip from '../pageobjects/Filmstrip';
  10. import IframeAPI from '../pageobjects/IframeAPI';
  11. import InviteDialog from '../pageobjects/InviteDialog';
  12. import LargeVideo from '../pageobjects/LargeVideo';
  13. import LobbyScreen from '../pageobjects/LobbyScreen';
  14. import Notifications from '../pageobjects/Notifications';
  15. import ParticipantsPane from '../pageobjects/ParticipantsPane';
  16. import PasswordDialog from '../pageobjects/PasswordDialog';
  17. import PreJoinScreen from '../pageobjects/PreJoinScreen';
  18. import SecurityDialog from '../pageobjects/SecurityDialog';
  19. import SettingsDialog from '../pageobjects/SettingsDialog';
  20. import Toolbar from '../pageobjects/Toolbar';
  21. import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
  22. import { LOG_PREFIX, logInfo } from './browserLogger';
  23. import { IContext, IJoinOptions } from './types';
  24. export const P1_DISPLAY_NAME = 'p1';
  25. export const P2_DISPLAY_NAME = 'p2';
  26. export const P3_DISPLAY_NAME = 'p3';
  27. export const P4_DISPLAY_NAME = 'p4';
  28. interface IWaitForSendReceiveDataOptions {
  29. checkReceive?: boolean;
  30. checkSend?: boolean;
  31. msg?: string;
  32. timeout?: number;
  33. }
  34. /**
  35. * Participant.
  36. */
  37. export class Participant {
  38. /**
  39. * The current context.
  40. *
  41. * @private
  42. */
  43. private _name: string;
  44. private _displayName: string;
  45. private _endpointId: string;
  46. private _jwt?: string;
  47. /**
  48. * The default config to use when joining.
  49. *
  50. * @private
  51. */
  52. private config = {
  53. analytics: {
  54. disabled: true
  55. },
  56. requireDisplayName: false,
  57. testing: {
  58. testMode: true
  59. },
  60. disableAP: true,
  61. disable1On1Mode: true,
  62. disableModeratorIndicator: true,
  63. enableTalkWhileMuted: false,
  64. gatherStats: true,
  65. p2p: {
  66. enabled: false,
  67. useStunTurn: false
  68. },
  69. pcStatsInterval: 1500,
  70. prejoinConfig: {
  71. enabled: false
  72. },
  73. toolbarConfig: {
  74. alwaysVisible: true
  75. }
  76. } as IConfig;
  77. /**
  78. * Creates a participant with given name.
  79. *
  80. * @param {string} name - The name of the participant.
  81. * @param {string }jwt - The jwt if any.
  82. */
  83. constructor(name: string, jwt?: string) {
  84. this._name = name;
  85. this._jwt = jwt;
  86. }
  87. /**
  88. * Returns participant endpoint ID.
  89. *
  90. * @returns {Promise<string>} The endpoint ID.
  91. */
  92. async getEndpointId(): Promise<string> {
  93. if (!this._endpointId) {
  94. this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
  95. return APP?.conference?.getMyUserId();
  96. });
  97. }
  98. return this._endpointId;
  99. }
  100. /**
  101. * The driver it uses.
  102. */
  103. get driver() {
  104. return multiremotebrowser.getInstance(this._name);
  105. }
  106. /**
  107. * The name.
  108. */
  109. get name() {
  110. return this._name;
  111. }
  112. /**
  113. * The name.
  114. */
  115. get displayName() {
  116. return this._displayName || this.name;
  117. }
  118. /**
  119. * Adds a log to the participants log file.
  120. *
  121. * @param {string} message - The message to log.
  122. * @returns {void}
  123. */
  124. log(message: string): void {
  125. logInfo(this.driver, message);
  126. }
  127. /**
  128. * Joins conference.
  129. *
  130. * @param {IContext} ctx - The context.
  131. * @param {IJoinOptions} options - Options for joining.
  132. * @returns {Promise<void>}
  133. */
  134. async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  135. const config = {
  136. room: ctx.roomName,
  137. configOverwrite: {
  138. ...this.config,
  139. ...options.configOverwrite || {}
  140. },
  141. interfaceConfigOverwrite: {
  142. SHOW_CHROME_EXTENSION_BANNER: false
  143. }
  144. };
  145. if (!options.skipDisplayName) {
  146. // @ts-ignore
  147. config.userInfo = {
  148. displayName: this._displayName = options.displayName || this._name
  149. };
  150. }
  151. if (ctx.iframeAPI) {
  152. config.room = 'iframeAPITest.html';
  153. }
  154. let url = urlObjectToString(config) || '';
  155. if (ctx.iframeAPI) {
  156. const baseUrl = new URL(this.driver.options.baseUrl || '');
  157. // @ts-ignore
  158. url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
  159. if (baseUrl.pathname.length > 1) {
  160. // remove leading slash
  161. url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
  162. }
  163. }
  164. if (this._jwt) {
  165. url = `${url}&jwt="${this._jwt}"`;
  166. }
  167. await this.driver.setTimeout({ 'pageLoad': 30000 });
  168. // drop the leading '/' so we can use the tenant if any
  169. await this.driver.url(url.startsWith('/') ? url.substring(1) : url);
  170. await this.waitForPageToLoad();
  171. if (ctx.iframeAPI) {
  172. const mainFrame = this.driver.$('iframe');
  173. await this.driver.switchFrame(mainFrame);
  174. }
  175. if (!options.skipWaitToJoin) {
  176. await this.waitToJoinMUC();
  177. }
  178. await this.postLoadProcess(options.skipInMeetingChecks);
  179. }
  180. /**
  181. * Loads stuff after the page loads.
  182. *
  183. * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
  184. * @returns {Promise<void>}
  185. * @private
  186. */
  187. private async postLoadProcess(skipInMeetingChecks = false): Promise<void> {
  188. const driver = this.driver;
  189. const parallel = [];
  190. parallel.push(driver.execute((name, sessionId, prefix) => {
  191. APP?.UI?.dockToolbar(true);
  192. // disable keyframe animations (.fadeIn and .fadeOut classes)
  193. $('<style>.notransition * { '
  194. + 'animation-duration: 0s !important; -webkit-animation-duration: 0s !important; transition:none; '
  195. + '} </style>') // @ts-ignore
  196. .appendTo(document.head);
  197. // @ts-ignore
  198. $('body').toggleClass('notransition');
  199. document.title = `${name}`;
  200. console.log(`${new Date().toISOString()} ${prefix} sessionId: ${sessionId}`);
  201. // disable the blur effect in firefox as it has some performance issues
  202. const blur = document.querySelector('.video_blurred_container');
  203. if (blur) {
  204. // @ts-ignore
  205. document.querySelector('.video_blurred_container').style.display = 'none';
  206. }
  207. }, this._name, driver.sessionId, LOG_PREFIX));
  208. if (skipInMeetingChecks) {
  209. await Promise.allSettled(parallel);
  210. return;
  211. }
  212. parallel.push(this.waitForIceConnected());
  213. parallel.push(this.waitForSendReceiveData());
  214. await Promise.all(parallel);
  215. }
  216. /**
  217. * Waits for the page to load.
  218. *
  219. * @returns {Promise<void>}
  220. */
  221. async waitForPageToLoad(): Promise<void> {
  222. return this.driver.waitUntil(
  223. () => this.driver.execute(() => document.readyState === 'complete'),
  224. {
  225. timeout: 30_000, // 30 seconds
  226. timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
  227. }
  228. );
  229. }
  230. /**
  231. * Waits for the tile view to display.
  232. */
  233. async waitForTileViewDisplay(reverse = false) {
  234. await this.driver.$('//div[@id="videoconference_page" and contains(@class, "tile-view")]').waitForDisplayed({
  235. reverse,
  236. timeout: 10_000,
  237. timeoutMsg: `Tile view did not display in 10s for ${this.name}`
  238. });
  239. }
  240. /**
  241. * Checks if the participant is in the meeting.
  242. */
  243. isInMuc() {
  244. return this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
  245. }
  246. /**
  247. * Checks if the participant is a moderator in the meeting.
  248. */
  249. async isModerator() {
  250. return await this.driver.execute(() => typeof APP !== 'undefined'
  251. && APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
  252. }
  253. /**
  254. * Checks if the meeting supports breakout rooms.
  255. */
  256. async isBreakoutRoomsSupported() {
  257. return await this.driver.execute(() => typeof APP !== 'undefined'
  258. && APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
  259. }
  260. /**
  261. * Checks if the participant is in breakout room.
  262. */
  263. async isInBreakoutRoom() {
  264. return await this.driver.execute(() => typeof APP !== 'undefined'
  265. && APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
  266. }
  267. /**
  268. * Waits to join the muc.
  269. *
  270. * @returns {Promise<void>}
  271. */
  272. async waitToJoinMUC(): Promise<void> {
  273. return this.driver.waitUntil(
  274. () => this.isInMuc(),
  275. {
  276. timeout: 10_000, // 10 seconds
  277. timeoutMsg: `Timeout waiting to join muc for ${this.name}`
  278. }
  279. );
  280. }
  281. /**
  282. * Waits for ICE to get connected.
  283. *
  284. * @returns {Promise<void>}
  285. */
  286. async waitForIceConnected(): Promise<void> {
  287. const driver = this.driver;
  288. return driver.waitUntil(() =>
  289. driver.execute(() => APP?.conference?.getConnectionState() === 'connected'), {
  290. timeout: 15_000,
  291. timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
  292. });
  293. }
  294. /**
  295. * Waits for ICE to get connected on the p2p connection.
  296. *
  297. * @returns {Promise<void>}
  298. */
  299. async waitForP2PIceConnected(): Promise<void> {
  300. const driver = this.driver;
  301. return driver.waitUntil(() =>
  302. driver.execute(() => APP?.conference?.getP2PConnectionState() === 'connected'), {
  303. timeout: 15_000,
  304. timeoutMsg: `expected P2P ICE to be connected for 15s for ${this.name}`
  305. });
  306. }
  307. /**
  308. * Waits for send and receive data.
  309. *
  310. * @param {Object} options
  311. * @param {boolean} options.checkSend - If true we will chec
  312. * @returns {Promise<void>}
  313. */
  314. waitForSendReceiveData({
  315. checkSend = true,
  316. checkReceive = true,
  317. timeout = 15_000,
  318. msg
  319. } = {} as IWaitForSendReceiveDataOptions): Promise<void> {
  320. if (!checkSend && !checkReceive) {
  321. return Promise.resolve();
  322. }
  323. const lMsg = msg ?? `expected to ${
  324. checkSend && checkReceive ? 'receive/send' : checkSend ? 'send' : 'receive'} data in 15s for ${this.name}`;
  325. return this.driver.waitUntil(() => this.driver.execute((pCheckSend: boolean, pCheckReceive: boolean) => {
  326. const stats = APP?.conference?.getStats();
  327. const bitrateMap = stats?.bitrate || {};
  328. const rtpStats = {
  329. uploadBitrate: bitrateMap.upload || 0,
  330. downloadBitrate: bitrateMap.download || 0
  331. };
  332. return (rtpStats.uploadBitrate > 0 || !pCheckSend) && (rtpStats.downloadBitrate > 0 || !pCheckReceive);
  333. }, checkSend, checkReceive), {
  334. timeout,
  335. timeoutMsg: lMsg
  336. });
  337. }
  338. /**
  339. * Waits for remote streams.
  340. *
  341. * @param {number} number - The number of remote streams to wait for.
  342. * @returns {Promise<void>}
  343. */
  344. waitForRemoteStreams(number: number): Promise<void> {
  345. const driver = this.driver;
  346. return driver.waitUntil(() =>
  347. driver.execute(count => (APP?.conference?.getNumberOfParticipantsWithTracks() ?? -1) >= count, number), {
  348. timeout: 15_000,
  349. timeoutMsg: `expected number of remote streams:${number} in 15s for ${this.name}`
  350. });
  351. }
  352. /**
  353. * Waits for number of participants.
  354. *
  355. * @param {number} number - The number of participant to wait for.
  356. * @param {string} msg - A custom message to use.
  357. * @returns {Promise<void>}
  358. */
  359. waitForParticipants(number: number, msg?: string): Promise<void> {
  360. const driver = this.driver;
  361. return driver.waitUntil(
  362. () => driver.execute(count => (APP?.conference?.listMembers()?.length ?? -1) === count, number),
  363. {
  364. timeout: 15_000,
  365. timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
  366. });
  367. }
  368. /**
  369. * Returns the chat panel for this participant.
  370. */
  371. getChatPanel(): ChatPanel {
  372. return new ChatPanel(this);
  373. }
  374. /**
  375. * Returns the BreakoutRooms for this participant.
  376. *
  377. * @returns {BreakoutRooms}
  378. */
  379. getBreakoutRooms(): BreakoutRooms {
  380. return new BreakoutRooms(this);
  381. }
  382. /**
  383. * Returns the toolbar for this participant.
  384. *
  385. * @returns {Toolbar}
  386. */
  387. getToolbar(): Toolbar {
  388. return new Toolbar(this);
  389. }
  390. /**
  391. * Returns the filmstrip for this participant.
  392. *
  393. * @returns {Filmstrip}
  394. */
  395. getFilmstrip(): Filmstrip {
  396. return new Filmstrip(this);
  397. }
  398. /**
  399. * Returns the invite dialog for this participant.
  400. *
  401. * @returns {InviteDialog}
  402. */
  403. getInviteDialog(): InviteDialog {
  404. return new InviteDialog(this);
  405. }
  406. /**
  407. * Returns the notifications.
  408. */
  409. getNotifications(): Notifications {
  410. return new Notifications(this);
  411. }
  412. /**
  413. * Returns the participants pane.
  414. *
  415. * @returns {ParticipantsPane}
  416. */
  417. getParticipantsPane(): ParticipantsPane {
  418. return new ParticipantsPane(this);
  419. }
  420. /**
  421. * Returns the large video page object.
  422. *
  423. * @returns {LargeVideo}
  424. */
  425. getLargeVideo(): LargeVideo {
  426. return new LargeVideo(this);
  427. }
  428. /**
  429. * Returns the videoQuality Dialog.
  430. *
  431. * @returns {VideoQualityDialog}
  432. */
  433. getVideoQualityDialog(): VideoQualityDialog {
  434. return new VideoQualityDialog(this);
  435. }
  436. /**
  437. * Returns the security Dialog.
  438. *
  439. * @returns {SecurityDialog}
  440. */
  441. getSecurityDialog(): SecurityDialog {
  442. return new SecurityDialog(this);
  443. }
  444. /**
  445. * Returns the settings Dialog.
  446. *
  447. * @returns {SettingsDialog}
  448. */
  449. getSettingsDialog(): SettingsDialog {
  450. return new SettingsDialog(this);
  451. }
  452. /**
  453. * Returns the password dialog.
  454. */
  455. getPasswordDialog(): PasswordDialog {
  456. return new PasswordDialog(this);
  457. }
  458. /**
  459. * Returns the prejoin screen.
  460. */
  461. getPreJoinScreen(): PreJoinScreen {
  462. return new PreJoinScreen(this);
  463. }
  464. /**
  465. * Returns the lobby screen.
  466. */
  467. getLobbyScreen(): LobbyScreen {
  468. return new LobbyScreen(this);
  469. }
  470. /**
  471. * Switches to the iframe API context
  472. */
  473. async switchToAPI() {
  474. await this.driver.switchFrame(null);
  475. }
  476. /**
  477. * Switches to the meeting page context.
  478. */
  479. async switchInPage() {
  480. const mainFrame = this.driver.$('iframe');
  481. await this.driver.switchFrame(mainFrame);
  482. }
  483. /**
  484. * Returns the iframe API for this participant.
  485. */
  486. getIframeAPI() {
  487. return new IframeAPI(this);
  488. }
  489. /**
  490. * Hangups the participant by leaving the page. base.html is an empty page on all deployments.
  491. */
  492. async hangup() {
  493. const current = await this.driver.getUrl();
  494. // already hangup
  495. if (current.endsWith('/base.html')) {
  496. return;
  497. }
  498. // do a hangup, to make sure unavailable presence is sent
  499. await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
  500. // let's give it some time to leave the muc, we redirect after hangup so we should wait for the
  501. // change of url
  502. await this.driver.waitUntil(
  503. async () => current !== await this.driver.getUrl(),
  504. {
  505. timeout: 5000,
  506. timeoutMsg: `${this.name} did not leave the muc in 5s`
  507. }
  508. );
  509. await this.driver.url('/base.html');
  510. }
  511. /**
  512. * Returns the local display name element.
  513. * @private
  514. */
  515. private async getLocalDisplayNameElement() {
  516. const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
  517. await localVideoContainer.moveTo();
  518. return localVideoContainer.$('span[id="localDisplayName"]');
  519. }
  520. /**
  521. * Returns the local display name.
  522. */
  523. async getLocalDisplayName() {
  524. return (await this.getLocalDisplayNameElement()).getText();
  525. }
  526. /**
  527. * Sets the display name of the local participant.
  528. */
  529. async setLocalDisplayName(displayName: string) {
  530. const localDisplayName = await this.getLocalDisplayNameElement();
  531. await localDisplayName.click();
  532. await this.driver.keys(displayName);
  533. await this.driver.keys(Key.Return);
  534. // just click somewhere to lose focus, to make sure editing has ended
  535. const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
  536. await localVideoContainer.moveTo();
  537. await localVideoContainer.click();
  538. }
  539. /**
  540. * Gets avatar SRC attribute for the one displayed on local video thumbnail.
  541. */
  542. async getLocalVideoAvatar() {
  543. const avatar
  544. = this.driver.$('//span[@id="localVideoContainer"]//img[contains(@class,"userAvatar")]');
  545. return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
  546. }
  547. /**
  548. * Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
  549. * There are 3 options for avatar:
  550. * - defaultAvatar: true - the default avatar (with grey figure) is used
  551. * - image: true - the avatar is an image set in the settings
  552. * - defaultAvatar: false, image: false - the avatar is produced from the initials of the display name
  553. */
  554. async assertThumbnailShowsAvatar(
  555. participant: Participant, reverse = false, defaultAvatar = false, image = false): Promise<void> {
  556. const id = participant === this
  557. ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
  558. const xpath = defaultAvatar
  559. ? `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`
  560. : `//span[@id="${id}"]//${image ? 'img' : 'div'}[contains(@class,"userAvatar")]`;
  561. await this.driver.$(xpath).waitForDisplayed({
  562. reverse,
  563. timeout: 2000,
  564. timeoutMsg: `Avatar is ${reverse ? '' : 'not'} displayed in the local thumbnail for ${participant.name}`
  565. });
  566. await this.driver.$(`//span[@id="${id}"]//video`).waitForDisplayed({
  567. reverse: !reverse,
  568. timeout: 2000,
  569. timeoutMsg: `Video is ${reverse ? 'not' : ''} displayed in the local thumbnail for ${participant.name}`
  570. });
  571. }
  572. /**
  573. * Makes sure that the default avatar is used.
  574. */
  575. async assertDefaultAvatarExist(participant: Participant): Promise<void> {
  576. const id = participant === this
  577. ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
  578. await this.driver.$(
  579. `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`)
  580. .waitForExist({
  581. timeout: 2000,
  582. timeoutMsg: `Default avatar does not exist for ${participant.name}`
  583. });
  584. }
  585. /**
  586. * Makes sure that the local video is displayed in the local thumbnail and that the avatar is not displayed.
  587. */
  588. async asserLocalThumbnailShowsVideo(): Promise<void> {
  589. await this.assertThumbnailShowsAvatar(this, true);
  590. }
  591. /**
  592. * Make sure a display name is visible on the stage.
  593. * @param value
  594. */
  595. async assertDisplayNameVisibleOnStage(value: string) {
  596. const displayNameEl = this.driver.$('div[data-testid="stage-display-name"]');
  597. expect(await displayNameEl.isDisplayed()).toBe(true);
  598. expect(await displayNameEl.getText()).toBe(value);
  599. }
  600. /**
  601. * Checks if the leave reason dialog is open.
  602. */
  603. async isLeaveReasonDialogOpen() {
  604. return this.driver.$('div[data-testid="dialog.leaveReason"]').isDisplayed();
  605. }
  606. /**
  607. * Returns the audio level for a participant.
  608. *
  609. * @param observer
  610. * @param participant
  611. * @return
  612. */
  613. async getRemoteAudioLevel(p: Participant) {
  614. const jid = await p.getEndpointId();
  615. return await this.driver.execute(id => {
  616. const level = APP?.conference?.getPeerSSRCAudioLevel(id);
  617. return level ? level.toFixed(2) : null;
  618. }, jid);
  619. }
  620. /**
  621. * For the participant to have his audio muted/unmuted from given observer's
  622. * perspective. The method will fail the test if something goes wrong or
  623. * the audio muted status is different than the expected one. We wait up to
  624. * 3 seconds for the expected status to appear.
  625. *
  626. * @param testee - instance of the participant for whom we're checking the audio muted status.
  627. * @param muted - <tt>true</tt> to wait for audio muted status or <tt>false</tt> to wait for the participant to
  628. * unmute.
  629. */
  630. async waitForAudioMuted(testee: Participant, muted: boolean): Promise<void> {
  631. // Waits for the correct icon
  632. await this.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, !muted);
  633. // Extended timeout for 'unmuted' to make tests more resilient to
  634. // unexpected glitches.
  635. const timeout = muted ? 3_000 : 6_000;
  636. // Give it 3 seconds to not get any audio or to receive some
  637. // depending on "muted" argument
  638. try {
  639. await this.driver.waitUntil(async () => {
  640. const audioLevel = await this.getRemoteAudioLevel(testee);
  641. if (muted) {
  642. if (audioLevel !== null && audioLevel > 0.1) {
  643. console.log(`muted exiting on: ${audioLevel}`);
  644. return true;
  645. }
  646. return false;
  647. }
  648. // When testing for unmuted we wait for first sound
  649. if (audioLevel !== null && audioLevel > 0.1) {
  650. console.log(`unmuted exiting on: ${audioLevel}`);
  651. return true;
  652. }
  653. return false;
  654. },
  655. { timeout });
  656. // When testing for muted we don't want to have
  657. // the condition succeeded
  658. if (muted) {
  659. const name = await testee.displayName;
  660. assert.fail(`There was some sound coming from muted: '${name}'`);
  661. } // else we're good for unmuted participant
  662. } catch (_timeoutE) {
  663. if (!muted) {
  664. const name = await testee.displayName;
  665. assert.fail(`There was no sound from unmuted: '${name}'`);
  666. } // else we're good for muted participant
  667. }
  668. }
  669. /**
  670. * Waits for remote video state - receiving and displayed.
  671. * @param endpointId
  672. */
  673. async waitForRemoteVideo(endpointId: string) {
  674. await this.driver.waitUntil(async () =>
  675. await this.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
  676. endpointId) && await this.driver.$(
  677. `//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
  678. timeout: 15_000,
  679. timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
  680. });
  681. }
  682. /**
  683. * Waits for ninja icon to be displayed.
  684. * @param endpointId
  685. */
  686. async waitForNinjaIcon(endpointId: string) {
  687. await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
  688. .waitForDisplayed({
  689. timeout: 15_000,
  690. timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
  691. });
  692. }
  693. }