You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. /* global APP $ */
  2. import { multiremotebrowser } from '@wdio/globals';
  3. import { IConfig } from '../../react/features/base/config/configType';
  4. import { urlObjectToString } from '../../react/features/base/util/uri';
  5. import BreakoutRooms from '../pageobjects/BreakoutRooms';
  6. import Filmstrip from '../pageobjects/Filmstrip';
  7. import IframeAPI from '../pageobjects/IframeAPI';
  8. import Notifications from '../pageobjects/Notifications';
  9. import ParticipantsPane from '../pageobjects/ParticipantsPane';
  10. import SettingsDialog from '../pageobjects/SettingsDialog';
  11. import Toolbar from '../pageobjects/Toolbar';
  12. import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
  13. import { LOG_PREFIX, logInfo } from './browserLogger';
  14. import { IContext, IJoinOptions } from './types';
  15. /**
  16. * Participant.
  17. */
  18. export class Participant {
  19. /**
  20. * The current context.
  21. *
  22. * @private
  23. */
  24. private _name: string;
  25. private _endpointId: string;
  26. private _jwt?: string;
  27. /**
  28. * The default config to use when joining.
  29. *
  30. * @private
  31. */
  32. private config = {
  33. analytics: {
  34. disabled: true
  35. },
  36. debug: true,
  37. requireDisplayName: false,
  38. testing: {
  39. testMode: true
  40. },
  41. disableAP: true,
  42. disable1On1Mode: true,
  43. disableModeratorIndicator: true,
  44. enableTalkWhileMuted: false,
  45. gatherStats: true,
  46. p2p: {
  47. enabled: false,
  48. useStunTurn: false
  49. },
  50. pcStatsInterval: 1500,
  51. prejoinConfig: {
  52. enabled: false
  53. },
  54. toolbarConfig: {
  55. alwaysVisible: true
  56. }
  57. } as IConfig;
  58. /**
  59. * Creates a participant with given name.
  60. *
  61. * @param {string} name - The name of the participant.
  62. * @param {string }jwt - The jwt if any.
  63. */
  64. constructor(name: string, jwt?: string) {
  65. this._name = name;
  66. this._jwt = jwt;
  67. }
  68. /**
  69. * Returns participant endpoint ID.
  70. *
  71. * @returns {Promise<string>} The endpoint ID.
  72. */
  73. async getEndpointId(): Promise<string> {
  74. if (!this._endpointId) {
  75. this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
  76. return APP.conference.getMyUserId();
  77. });
  78. }
  79. return this._endpointId;
  80. }
  81. /**
  82. * The driver it uses.
  83. */
  84. get driver() {
  85. return multiremotebrowser.getInstance(this._name);
  86. }
  87. /**
  88. * The name.
  89. */
  90. get name() {
  91. return this._name;
  92. }
  93. /**
  94. * Adds a log to the participants log file.
  95. *
  96. * @param {string} message - The message to log.
  97. * @returns {void}
  98. */
  99. log(message: string): void {
  100. logInfo(this.driver, message);
  101. }
  102. /**
  103. * Joins conference.
  104. *
  105. * @param {IContext} ctx - The context.
  106. * @param {IJoinOptions} options - Options for joining.
  107. * @returns {Promise<void>}
  108. */
  109. async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  110. const config = {
  111. room: ctx.roomName,
  112. configOverwrite: this.config,
  113. interfaceConfigOverwrite: {
  114. SHOW_CHROME_EXTENSION_BANNER: false
  115. }
  116. };
  117. if (!options.skipDisplayName) {
  118. // @ts-ignore
  119. config.userInfo = {
  120. displayName: this._name
  121. };
  122. }
  123. if (ctx.iframeAPI) {
  124. config.room = 'iframeAPITest.html';
  125. }
  126. let url = urlObjectToString(config) || '';
  127. if (ctx.iframeAPI) {
  128. const baseUrl = new URL(this.driver.options.baseUrl || '');
  129. // @ts-ignore
  130. url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
  131. if (baseUrl.pathname.length > 1) {
  132. // remove leading slash
  133. url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
  134. }
  135. }
  136. if (this._jwt) {
  137. url = `${url}&jwt="${this._jwt}"`;
  138. }
  139. await this.driver.setTimeout({ 'pageLoad': 30000 });
  140. // drop the leading '/' so we can use the tenant if any
  141. await this.driver.url(url.startsWith('/') ? url.substring(1) : url);
  142. await this.waitForPageToLoad();
  143. if (ctx.iframeAPI) {
  144. const mainFrame = this.driver.$('iframe');
  145. await this.driver.switchFrame(mainFrame);
  146. }
  147. await this.waitToJoinMUC();
  148. await this.postLoadProcess(options.skipInMeetingChecks);
  149. }
  150. /**
  151. * Loads stuff after the page loads.
  152. *
  153. * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
  154. * @returns {Promise<void>}
  155. * @private
  156. */
  157. private async postLoadProcess(skipInMeetingChecks = false): Promise<void> {
  158. const driver = this.driver;
  159. const parallel = [];
  160. parallel.push(driver.execute((name, sessionId, prefix) => {
  161. APP.UI.dockToolbar(true);
  162. // disable keyframe animations (.fadeIn and .fadeOut classes)
  163. $('<style>.notransition * { '
  164. + 'animation-duration: 0s !important; -webkit-animation-duration: 0s !important; transition:none; '
  165. + '} </style>') // @ts-ignore
  166. .appendTo(document.head);
  167. // @ts-ignore
  168. $('body').toggleClass('notransition');
  169. document.title = `${name}`;
  170. console.log(`${new Date().toISOString()} ${prefix} sessionId: ${sessionId}`);
  171. // disable the blur effect in firefox as it has some performance issues
  172. const blur = document.querySelector('.video_blurred_container');
  173. if (blur) {
  174. // @ts-ignore
  175. document.querySelector('.video_blurred_container').style.display = 'none';
  176. }
  177. }, this._name, driver.sessionId, LOG_PREFIX));
  178. if (skipInMeetingChecks) {
  179. await Promise.allSettled(parallel);
  180. return;
  181. }
  182. parallel.push(this.waitForIceConnected());
  183. parallel.push(this.waitForSendReceiveData());
  184. await Promise.all(parallel);
  185. }
  186. /**
  187. * Waits for the page to load.
  188. *
  189. * @returns {Promise<void>}
  190. */
  191. async waitForPageToLoad(): Promise<void> {
  192. return this.driver.waitUntil(
  193. async () => await this.driver.execute(() => document.readyState === 'complete'),
  194. {
  195. timeout: 30_000, // 30 seconds
  196. timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
  197. }
  198. );
  199. }
  200. /**
  201. * Checks if the participant is in the meeting.
  202. */
  203. async isInMuc() {
  204. return await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
  205. }
  206. /**
  207. * Checks if the participant is a moderator in the meeting.
  208. */
  209. async isModerator() {
  210. return await this.driver.execute(() => typeof APP !== 'undefined'
  211. && APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
  212. }
  213. /**
  214. * Checks if the meeting supports breakout rooms.
  215. */
  216. async isBreakoutRoomsSupported() {
  217. return await this.driver.execute(() => typeof APP !== 'undefined'
  218. && APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
  219. }
  220. /**
  221. * Checks if the participant is in breakout room.
  222. */
  223. async isInBreakoutRoom() {
  224. return await this.driver.execute(() => typeof APP !== 'undefined'
  225. && APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
  226. }
  227. /**
  228. * Waits to join the muc.
  229. *
  230. * @returns {Promise<void>}
  231. */
  232. async waitToJoinMUC(): Promise<void> {
  233. return this.driver.waitUntil(
  234. () => this.isInMuc(),
  235. {
  236. timeout: 10_000, // 10 seconds
  237. timeoutMsg: 'Timeout waiting to join muc.'
  238. }
  239. );
  240. }
  241. /**
  242. * Waits for ICE to get connected.
  243. *
  244. * @returns {Promise<void>}
  245. */
  246. async waitForIceConnected(): Promise<void> {
  247. const driver = this.driver;
  248. return driver.waitUntil(async () =>
  249. await driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
  250. timeout: 15_000,
  251. timeoutMsg: 'expected ICE to be connected for 15s'
  252. });
  253. }
  254. /**
  255. * Waits for send and receive data.
  256. *
  257. * @returns {Promise<void>}
  258. */
  259. async waitForSendReceiveData(): Promise<void> {
  260. const driver = this.driver;
  261. return driver.waitUntil(async () =>
  262. await driver.execute(() => {
  263. const stats = APP.conference.getStats();
  264. const bitrateMap = stats?.bitrate || {};
  265. const rtpStats = {
  266. uploadBitrate: bitrateMap.upload || 0,
  267. downloadBitrate: bitrateMap.download || 0
  268. };
  269. return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
  270. }), {
  271. timeout: 15_000,
  272. timeoutMsg: 'expected to receive/send data in 15s'
  273. });
  274. }
  275. /**
  276. * Waits for remote streams.
  277. *
  278. * @param {number} number - The number of remote streams o wait for.
  279. * @returns {Promise<void>}
  280. */
  281. waitForRemoteStreams(number: number): Promise<void> {
  282. const driver = this.driver;
  283. return driver.waitUntil(async () =>
  284. await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
  285. timeout: 15_000,
  286. timeoutMsg: 'expected remote streams in 15s'
  287. });
  288. }
  289. /**
  290. * Returns the BreakoutRooms for this participant.
  291. *
  292. * @returns {BreakoutRooms}
  293. */
  294. getBreakoutRooms(): BreakoutRooms {
  295. return new BreakoutRooms(this);
  296. }
  297. /**
  298. * Returns the toolbar for this participant.
  299. *
  300. * @returns {Toolbar}
  301. */
  302. getToolbar(): Toolbar {
  303. return new Toolbar(this);
  304. }
  305. /**
  306. * Returns the filmstrip for this participant.
  307. *
  308. * @returns {Filmstrip}
  309. */
  310. getFilmstrip(): Filmstrip {
  311. return new Filmstrip(this);
  312. }
  313. /**
  314. * Returns the notifications.
  315. */
  316. getNotifications(): Notifications {
  317. return new Notifications(this);
  318. }
  319. /**
  320. * Returns the participants pane.
  321. *
  322. * @returns {ParticipantsPane}
  323. */
  324. getParticipantsPane(): ParticipantsPane {
  325. return new ParticipantsPane(this);
  326. }
  327. /**
  328. * Returns the videoQuality Dialog.
  329. *
  330. * @returns {VideoQualityDialog}
  331. */
  332. getVideoQualityDialog(): VideoQualityDialog {
  333. return new VideoQualityDialog(this);
  334. }
  335. /**
  336. * Returns the settings Dialog.
  337. *
  338. * @returns {SettingsDialog}
  339. */
  340. getSettingsDialog(): SettingsDialog {
  341. return new SettingsDialog(this);
  342. }
  343. /**
  344. * Switches to the iframe API context
  345. */
  346. async switchToAPI() {
  347. await this.driver.switchFrame(null);
  348. }
  349. /**
  350. * Switches to the meeting page context.
  351. */
  352. async switchInPage() {
  353. const mainFrame = this.driver.$('iframe');
  354. await this.driver.switchFrame(mainFrame);
  355. }
  356. /**
  357. * Returns the iframe API for this participant.
  358. */
  359. getIframeAPI() {
  360. return new IframeAPI(this);
  361. }
  362. /**
  363. * Hangups the participant by leaving the page. base.html is an empty page on all deployments.
  364. */
  365. async hangup() {
  366. await this.driver.url('/base.html');
  367. }
  368. /**
  369. * Returns the local display name.
  370. */
  371. async getLocalDisplayName() {
  372. const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
  373. await localVideoContainer.moveTo();
  374. const localDisplayName = localVideoContainer.$('span[id="localDisplayName"]');
  375. return await localDisplayName.getText();
  376. }
  377. /**
  378. * Gets avatar SRC attribute for the one displayed on local video thumbnail.
  379. */
  380. async getLocalVideoAvatar() {
  381. const avatar
  382. = this.driver.$('//span[@id="localVideoContainer"]//img[contains(@class,"userAvatar")]');
  383. return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
  384. }
  385. /**
  386. * Gets avatar SRC attribute for the one displayed on large video.
  387. */
  388. async getLargeVideoAvatar() {
  389. const avatar = this.driver.$('//img[@id="dominantSpeakerAvatar"]');
  390. return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
  391. }
  392. /**
  393. * Returns resource part of the JID of the user who is currently displayed in the large video area.
  394. */
  395. async getLargeVideoResource() {
  396. return await this.driver.execute(() => APP.UI.getLargeVideoID());
  397. }
  398. /**
  399. * Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
  400. * There are 3 options for avatar:
  401. * - defaultAvatar: true - the default avatar (with grey figure) is used
  402. * - image: true - the avatar is an image set in the settings
  403. * - defaultAvatar: false, image: false - the avatar is produced from the initials of the display name
  404. */
  405. async assertThumbnailShowsAvatar(
  406. participant: Participant, reverse = false, defaultAvatar = false, image = false): Promise<void> {
  407. const id = participant === this
  408. ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
  409. const xpath = defaultAvatar
  410. ? `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`
  411. : `//span[@id="${id}"]//${image ? 'img' : 'div'}[contains(@class,"userAvatar")]`;
  412. await this.driver.$(xpath).waitForDisplayed({
  413. reverse,
  414. timeout: 2000,
  415. timeoutMsg: `Avatar is ${reverse ? '' : 'not'} displayed in the local thumbnail for ${participant.name}`
  416. });
  417. await this.driver.$(`//span[@id="${id}"]//video`).waitForDisplayed({
  418. reverse: !reverse,
  419. timeout: 2000,
  420. timeoutMsg: `Video is ${reverse ? 'not' : ''} displayed in the local thumbnail for ${participant.name}`
  421. });
  422. }
  423. /**
  424. * Makes sure that the default avatar is used.
  425. */
  426. async assertDefaultAvatarExist(participant: Participant): Promise<void> {
  427. const id = participant === this
  428. ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
  429. await this.driver.$(
  430. `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`)
  431. .waitForExist({
  432. timeout: 2000,
  433. timeoutMsg: `Default avatar does not exist for ${participant.name}`
  434. });
  435. }
  436. /**
  437. * Makes sure that the local video is displayed in the local thumbnail and that the avatar is not displayed.
  438. */
  439. async asserLocalThumbnailShowsVideo(): Promise<void> {
  440. await this.assertThumbnailShowsAvatar(this, true);
  441. }
  442. /**
  443. * Make sure a display name is visible on the stage.
  444. * @param value
  445. */
  446. async assertDisplayNameVisibleOnStage(value: string) {
  447. const displayNameEl = this.driver.$('div[data-testid="stage-display-name"]');
  448. expect(await displayNameEl.isDisplayed()).toBe(true);
  449. expect(await displayNameEl.getText()).toBe(value);
  450. }
  451. }