123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800 |
- /* 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 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 { LOG_PREFIX, logInfo } from './browserLogger';
- import { IContext, IJoinOptions } from './types';
-
- export const P1_DISPLAY_NAME = 'p1';
- export const P2_DISPLAY_NAME = 'p2';
- export const P3_DISPLAY_NAME = 'p3';
- export const P4_DISPLAY_NAME = 'p4';
-
- interface IWaitForSendReceiveDataOptions {
- checkReceive?: boolean;
- checkSend?: boolean;
- msg?: string;
- timeout?: number;
- }
-
- /**
- * Participant.
- */
- export class Participant {
- /**
- * The current context.
- *
- * @private
- */
- private _name: string;
- private _displayName: string;
- private _endpointId: string;
- private _jwt?: string;
-
- /**
- * The default config to use when joining.
- *
- * @private
- */
- private config = {
- analytics: {
- disabled: true
- },
- 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 name.
- *
- * @param {string} name - The name of the participant.
- * @param {string }jwt - The jwt if any.
- */
- constructor(name: string, jwt?: string) {
- this._name = name;
- this._jwt = jwt;
- }
-
- /**
- * Returns participant endpoint ID.
- *
- * @returns {Promise<string>} The endpoint ID.
- */
- async getEndpointId(): Promise<string> {
- if (!this._endpointId) {
- this._endpointId = await this.driver.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;
- }
-
- /**
- * The name.
- */
- get displayName() {
- return this._displayName || 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 {IContext} ctx - The context.
- * @param {IJoinOptions} options - Options for joining.
- * @returns {Promise<void>}
- */
- async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
- const config = {
- room: ctx.roomName,
- configOverwrite: {
- ...this.config,
- ...options.configOverwrite || {}
- },
- interfaceConfigOverwrite: {
- SHOW_CHROME_EXTENSION_BANNER: false
- }
- };
-
- if (!options.skipDisplayName) {
- // @ts-ignore
- config.userInfo = {
- displayName: this._displayName = options.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 });
-
- // 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);
- }
-
- if (!options.skipWaitToJoin) {
- await this.waitToJoinMUC();
- }
-
- await this.postLoadProcess(options.skipInMeetingChecks);
- }
-
- /**
- * Loads stuff after the page loads.
- *
- * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
- * @returns {Promise<void>}
- * @private
- */
- private async postLoadProcess(skipInMeetingChecks = false): Promise<void> {
- const driver = this.driver;
-
- const parallel = [];
-
- parallel.push(driver.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));
-
- if (skipInMeetingChecks) {
- await Promise.allSettled(parallel);
-
- return;
- }
-
- parallel.push(this.waitForIceConnected());
- parallel.push(this.waitForSendReceiveData());
-
- await Promise.all(parallel);
- }
-
- /**
- * Waits for the page to load.
- *
- * @returns {Promise<void>}
- */
- async waitForPageToLoad(): Promise<void> {
- return this.driver.waitUntil(
- () => this.driver.execute(() => 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.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');
- }
-
- /**
- * Checks if the meeting supports breakout rooms.
- */
- async isBreakoutRoomsSupported() {
- return await this.driver.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.driver.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<void>}
- */
- async waitForIceConnected(): Promise<void> {
- const driver = this.driver;
-
- return driver.waitUntil(() =>
- driver.execute(() => APP?.conference?.getConnectionState() === 'connected'), {
- timeout: 15_000,
- timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
- });
- }
-
- /**
- * Waits for send and receive data.
- *
- * @param {Object} options
- * @param {boolean} options.checkSend - If true we will chec
- * @returns {Promise<void>}
- */
- waitForSendReceiveData({
- checkSend = true,
- checkReceive = true,
- timeout = 15_000,
- msg
- } = {} as IWaitForSendReceiveDataOptions): Promise<void> {
- if (!checkSend && !checkReceive) {
- return Promise.resolve();
- }
-
- const lMsg = msg ?? `expected to ${
- checkSend && checkReceive ? 'receive/send' : checkSend ? 'send' : 'receive'} data in 15s for ${this.name}`;
-
- return this.driver.waitUntil(() => this.driver.execute((pCheckSend: boolean, pCheckReceive: boolean) => {
- const stats = APP?.conference?.getStats();
- const bitrateMap = stats?.bitrate || {};
- const rtpStats = {
- uploadBitrate: bitrateMap.upload || 0,
- downloadBitrate: bitrateMap.download || 0
- };
-
- return (rtpStats.uploadBitrate > 0 || !pCheckSend) && (rtpStats.downloadBitrate > 0 || !pCheckReceive);
- }, checkSend, checkReceive), {
- timeout,
- timeoutMsg: lMsg
- });
- }
-
- /**
- * Waits for remote streams.
- *
- * @param {number} number - The number of remote streams to wait for.
- * @returns {Promise<void>}
- */
- waitForRemoteStreams(number: number): Promise<void> {
- const driver = this.driver;
-
- return driver.waitUntil(() =>
- driver.execute(count => (APP?.conference?.getNumberOfParticipantsWithTracks() ?? -1) >= count, number), {
- timeout: 15_000,
- timeoutMsg: `expected number of remote streams:${number} in 15s for ${this.name}`
- });
- }
-
- /**
- * Waits for number of participants.
- *
- * @param {number} number - The number of participant to wait for.
- * @param {string} msg - A custom message to use.
- * @returns {Promise<void>}
- */
- waitForParticipants(number: number, msg?: string): Promise<void> {
- const driver = this.driver;
-
- return driver.waitUntil(
- () => driver.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);
- }
-
- /**
- * 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() {
- const current = await this.driver.getUrl();
-
- // already hangup
- if (current.endsWith('/base.html')) {
- return;
- }
-
- // do a hangup, to make sure unavailable presence is sent
- await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
-
- // let's give it some time to leave the muc, we redirect after hangup so we should wait for the
- // change of url
- await this.driver.waitUntil(
- async () => current !== await this.driver.getUrl(),
- {
- timeout: 5000,
- timeoutMsg: `${this.name} did not leave the muc in 5s`
- }
- );
-
- await this.driver.url('/base.html');
- }
-
- /**
- * 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 observer
- * @param participant
- * @return
- */
- async getRemoteAudioLevel(p: Participant) {
- const jid = await p.getEndpointId();
-
- return await this.driver.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) {
- const name = await testee.displayName;
-
- assert.fail(`There was some sound coming from muted: '${name}'`);
- } // else we're good for unmuted participant
- } catch (_timeoutE) {
- if (!muted) {
- const name = await testee.displayName;
-
- assert.fail(`There was no sound from unmuted: '${name}'`);
- } // else we're good for muted participant
- }
- }
-
-
- /**
- * Waits for remote video state - receiving and displayed.
- * @param endpointId
- */
- async waitForRemoteVideo(endpointId: string) {
- await this.driver.waitUntil(async () =>
- await this.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
- endpointId) && await this.driver.$(
- `//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
- timeout: 15_000,
- timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
- });
- }
-
- /**
- * Waits for ninja icon to be displayed.
- * @param endpointId
- */
- async waitForNinjaIcon(endpointId: string) {
- await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
- .waitForDisplayed({
- timeout: 15_000,
- timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
- });
- }
- }
|