mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
* feat(tests): Adds option to skip suite. * fix(tests): Rename context to ctx to avoid clashing mocha's one. * feat(tests): Moves room name generation in hooks. Move also the proxy connection in the hooks. * fix(tests): Avatar checks when using a token. Token has its avatar so we skip the token for avatar tests. * feat(tests): Renames avatars to drop Test from name. * feat(tests): Updates dependencies. * feat(tests): Fix end test log. * feat(tests): AVModeration tests.
507 lines
14 KiB
TypeScript
507 lines
14 KiB
TypeScript
/* global APP $ */
|
|
|
|
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, IJoinOptions } from './types';
|
|
|
|
/**
|
|
* Participant.
|
|
*/
|
|
export class Participant {
|
|
/**
|
|
* The current context.
|
|
*
|
|
* @private
|
|
*/
|
|
private _name: string;
|
|
private _endpointId: string;
|
|
private _jwt?: string;
|
|
|
|
/**
|
|
* The default config to use when joining.
|
|
*
|
|
* @private
|
|
*/
|
|
private config = {
|
|
analytics: {
|
|
disabled: true
|
|
},
|
|
debug: 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;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
interfaceConfigOverwrite: {
|
|
SHOW_CHROME_EXTENSION_BANNER: false
|
|
}
|
|
};
|
|
|
|
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 });
|
|
|
|
// 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(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(
|
|
async () => await this.driver.execute(() => document.readyState === 'complete'),
|
|
{
|
|
timeout: 30_000, // 30 seconds
|
|
timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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(): Promise<void> {
|
|
return this.driver.waitUntil(
|
|
() => this.isInMuc(),
|
|
{
|
|
timeout: 10_000, // 10 seconds
|
|
timeoutMsg: 'Timeout waiting to join muc.'
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Waits for ICE to get connected.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async waitForIceConnected(): Promise<void> {
|
|
const driver = this.driver;
|
|
|
|
return driver.waitUntil(async () =>
|
|
await driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
|
|
timeout: 15_000,
|
|
timeoutMsg: 'expected ICE to be connected for 15s'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits for send and receive data.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async waitForSendReceiveData(): Promise<void> {
|
|
const driver = this.driver;
|
|
|
|
return driver.waitUntil(async () =>
|
|
await driver.execute(() => {
|
|
const stats = APP.conference.getStats();
|
|
const bitrateMap = stats?.bitrate || {};
|
|
const rtpStats = {
|
|
uploadBitrate: bitrateMap.upload || 0,
|
|
downloadBitrate: bitrateMap.download || 0
|
|
};
|
|
|
|
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
|
|
}), {
|
|
timeout: 15_000,
|
|
timeoutMsg: 'expected to receive/send data in 15s'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits for remote streams.
|
|
*
|
|
* @param {number} number - The number of remote streams o wait for.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
waitForRemoteStreams(number: number): Promise<void> {
|
|
const driver = this.driver;
|
|
|
|
return driver.waitUntil(async () =>
|
|
await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
|
|
timeout: 15_000,
|
|
timeoutMsg: 'expected remote streams in 15s'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 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);
|
|
}
|
|
}
|