mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(tests): AVModeration tests. (#15408)
* 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.
This commit is contained in:
1795
package-lock.json
generated
1795
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -139,10 +139,10 @@
|
||||
"@types/amplitude-js": "8.16.5",
|
||||
"@types/audioworklet": "0.0.29",
|
||||
"@types/dom-screen-wake-lock": "1.0.1",
|
||||
"@types/jasmine": "5.1.4",
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/moment-duration-format": "2.2.6",
|
||||
"@types/offscreencanvas": "2019.7.2",
|
||||
"@types/pixelmatch": "5.2.5",
|
||||
@@ -162,12 +162,12 @@
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@wdio/allure-reporter": "9.2.14",
|
||||
"@wdio/cli": "9.4.1",
|
||||
"@wdio/globals": "9.4.1",
|
||||
"@wdio/jasmine-framework": "9.4.1",
|
||||
"@wdio/junit-reporter": "9.2.14",
|
||||
"@wdio/local-runner": "9.4.1",
|
||||
"@wdio/allure-reporter": "9.4.3",
|
||||
"@wdio/cli": "9.4.3",
|
||||
"@wdio/globals": "9.4.3",
|
||||
"@wdio/junit-reporter": "9.4.3",
|
||||
"@wdio/local-runner": "9.4.3",
|
||||
"@wdio/mocha-framework": "9.4.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-optional-require": "0.3.1",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
@@ -191,7 +191,7 @@
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript": "5.0.4",
|
||||
"unorm": "1.6.0",
|
||||
"webdriverio": "9.4.1",
|
||||
"webdriverio": "9.4.3",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
|
||||
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
|
||||
#BASE_URL=
|
||||
|
||||
# To be able to match a domain to a specific address
|
||||
|
||||
2
tests/globals.d.ts
vendored
2
tests/globals.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { IContext } from './helpers/types';
|
||||
|
||||
declare global {
|
||||
const context: IContext;
|
||||
const ctx: IContext;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -112,13 +113,13 @@ export class Participant {
|
||||
/**
|
||||
* Joins conference.
|
||||
*
|
||||
* @param {IContext} context - The context.
|
||||
* @param {IContext} ctx - The context.
|
||||
* @param {IJoinOptions} options - Options for joining.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async joinConference(context: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
const config = {
|
||||
room: context.roomName,
|
||||
room: ctx.roomName,
|
||||
configOverwrite: this.config,
|
||||
interfaceConfigOverwrite: {
|
||||
SHOW_CHROME_EXTENSION_BANNER: false
|
||||
@@ -132,17 +133,17 @@ export class Participant {
|
||||
};
|
||||
}
|
||||
|
||||
if (context.iframeAPI) {
|
||||
if (ctx.iframeAPI) {
|
||||
config.room = 'iframeAPITest.html';
|
||||
}
|
||||
|
||||
let url = urlObjectToString(config) || '';
|
||||
|
||||
if (context.iframeAPI) {
|
||||
if (ctx.iframeAPI) {
|
||||
const baseUrl = new URL(this.driver.options.baseUrl || '');
|
||||
|
||||
// @ts-ignore
|
||||
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${context.roomName}"`;
|
||||
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
|
||||
|
||||
if (baseUrl.pathname.length > 1) {
|
||||
// remove leading slash
|
||||
@@ -155,17 +156,12 @@ export class Participant {
|
||||
|
||||
await this.driver.setTimeout({ 'pageLoad': 30000 });
|
||||
|
||||
// workaround for https://github.com/webdriverio/webdriverio/issues/13956
|
||||
if (url.startsWith('file://')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
await this.driver.url(url).catch(() => {});
|
||||
} else {
|
||||
await this.driver.url(url.substring(1)); // drop the leading '/' so we can use the tenant if any
|
||||
}
|
||||
// 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 (context.iframeAPI) {
|
||||
if (ctx.iframeAPI) {
|
||||
const mainFrame = this.driver.$('iframe');
|
||||
|
||||
await this.driver.switchFrame(mainFrame);
|
||||
@@ -247,6 +243,14 @@ export class Participant {
|
||||
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.
|
||||
*
|
||||
@@ -335,6 +339,13 @@ export class Participant {
|
||||
return new Filmstrip(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notifications.
|
||||
*/
|
||||
getNotifications(): Notifications {
|
||||
return new Notifications(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the participants pane.
|
||||
*
|
||||
@@ -489,7 +500,7 @@ export class Participant {
|
||||
async assertDisplayNameVisibleOnStage(value: string) {
|
||||
const displayNameEl = this.driver.$('div[data-testid="stage-display-name"]');
|
||||
|
||||
expect(await displayNameEl.isDisplayed()).toBeTrue();
|
||||
expect(await displayNameEl.isDisplayed()).toBe(true);
|
||||
expect(await displayNameEl.getText()).toBe(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,44 +4,19 @@ import process from 'node:process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Participant } from './Participant';
|
||||
import WebhookProxy from './WebhookProxy';
|
||||
import { IContext, IJoinOptions } from './types';
|
||||
|
||||
/**
|
||||
* Generate a random room name.
|
||||
* Everytime we generate a name and iframeAPI is enabled and there is a configured
|
||||
* webhooks proxy we connect to it with the new room name.
|
||||
*
|
||||
* @returns {string} - The random room name.
|
||||
*/
|
||||
function generateRandomRoomName(): string {
|
||||
const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
|
||||
|
||||
if (context.iframeAPI && !context.webhooksProxy
|
||||
&& process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
|
||||
context.webhooksProxy = new WebhookProxy(`${process.env.WEBHOOKS_PROXY_URL}&room=${roomName}`,
|
||||
process.env.WEBHOOKS_PROXY_SHARED_SECRET);
|
||||
context.webhooksProxy.connect();
|
||||
}
|
||||
|
||||
return roomName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there is on participant.
|
||||
*
|
||||
* @param {IContext} context - The context.
|
||||
* @param {IContext} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureOneParticipant(context: IContext, options?: IJoinOptions): Promise<void> {
|
||||
if (!context.roomName) {
|
||||
context.roomName = generateRandomRoomName();
|
||||
}
|
||||
export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
ctx.p1 = new Participant('participant1');
|
||||
|
||||
context.p1 = new Participant('participant1');
|
||||
|
||||
await context.p1.joinConference(context, {
|
||||
await ctx.p1.joinConference(ctx, {
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
@@ -50,70 +25,72 @@ export async function ensureOneParticipant(context: IContext, options?: IJoinOpt
|
||||
/**
|
||||
* Ensure that there are three participants.
|
||||
*
|
||||
* @param {Object} context - The context.
|
||||
* @param {Object} ctx - The context.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureThreeParticipants(context: IContext): Promise<void> {
|
||||
if (!context.roomName) {
|
||||
context.roomName = generateRandomRoomName();
|
||||
}
|
||||
export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx);
|
||||
|
||||
const p1 = new Participant('participant1');
|
||||
const p2 = new Participant('participant2');
|
||||
const p3 = new Participant('participant3');
|
||||
|
||||
context.p1 = p1;
|
||||
context.p2 = p2;
|
||||
context.p3 = p3;
|
||||
ctx.p2 = p2;
|
||||
ctx.p3 = p3;
|
||||
|
||||
// these need to be all, so we get the error when one fails
|
||||
await Promise.all([
|
||||
p1.joinConference(context),
|
||||
p2.joinConference(context),
|
||||
p3.joinConference(context)
|
||||
p2.joinConference(ctx),
|
||||
p3.joinConference(ctx)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
p1.waitForRemoteStreams(2),
|
||||
p2.waitForRemoteStreams(2),
|
||||
p3.waitForRemoteStreams(2)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are two participants.
|
||||
* Ensure that the first participant is moderator.
|
||||
*
|
||||
* @param {Object} context - The context.
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to join.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureTwoParticipants(context: IContext, options?: IJoinOptions): Promise<void> {
|
||||
if (!context.roomName) {
|
||||
context.roomName = generateRandomRoomName();
|
||||
}
|
||||
|
||||
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
|
||||
const p1DisplayName = 'participant1';
|
||||
let token;
|
||||
|
||||
// if it is jaas create the first one to be moderator and second not moderator
|
||||
if (context.jwtPrivateKeyPath) {
|
||||
if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
|
||||
token = getModeratorToken(p1DisplayName);
|
||||
}
|
||||
|
||||
// make sure the first participant is moderator, if supported by deployment
|
||||
await _joinParticipant(p1DisplayName, context.p1, p => {
|
||||
context.p1 = p;
|
||||
await _joinParticipant(p1DisplayName, ctx.p1, p => {
|
||||
ctx.p1 = p;
|
||||
}, {
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
}, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are two participants.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to join.
|
||||
*/
|
||||
export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
|
||||
const { skipInMeetingChecks } = options;
|
||||
|
||||
await Promise.all([
|
||||
_joinParticipant('participant2', context.p2, p => {
|
||||
context.p2 = p;
|
||||
_joinParticipant('participant2', ctx.p2, p => {
|
||||
ctx.p2 = p;
|
||||
}, options),
|
||||
context.p1.waitForRemoteStreams(1),
|
||||
context.p2.waitForRemoteStreams(1)
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -132,7 +109,7 @@ async function _joinParticipant( // eslint-disable-line max-params
|
||||
options: IJoinOptions = {},
|
||||
jwtToken?: string) {
|
||||
if (p) {
|
||||
if (context.iframeAPI) {
|
||||
if (ctx.iframeAPI) {
|
||||
await p.switchInPage();
|
||||
}
|
||||
|
||||
@@ -140,7 +117,7 @@ async function _joinParticipant( // eslint-disable-line max-params
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.iframeAPI) {
|
||||
if (ctx.iframeAPI) {
|
||||
// when loading url make sure we are on the top page context or strange errors may occur
|
||||
await p.switchToAPI();
|
||||
}
|
||||
@@ -156,7 +133,7 @@ async function _joinParticipant( // eslint-disable-line max-params
|
||||
// set the new participant instance, pass it to setter
|
||||
setter(newParticipant);
|
||||
|
||||
await newParticipant.joinConference(context, options);
|
||||
await newParticipant.joinConference(ctx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,16 +153,28 @@ export async function muteAudioAndCheck(testee: Participant, observer: Participa
|
||||
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute audio, checks if the local UI has been updated accordingly and then does the verification from
|
||||
* the other observer participant perspective.
|
||||
* @param testee
|
||||
* @param observer
|
||||
*/
|
||||
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
|
||||
await testee.getToolbar().clickAudioUnmuteButton();
|
||||
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the video on testee and check on observer.
|
||||
* @param testee
|
||||
* @param observer
|
||||
*/
|
||||
export async function unMuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
await testee.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,7 +195,7 @@ function getModeratorToken(displayName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = fs.readFileSync(context.jwtPrivateKeyPath);
|
||||
const key = fs.readFileSync(ctx.jwtPrivateKeyPath);
|
||||
|
||||
const payload = {
|
||||
'aud': 'jitsi',
|
||||
|
||||
@@ -11,6 +11,7 @@ export type IContext = {
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
roomName: string;
|
||||
skipSuiteTests: boolean;
|
||||
webhooksProxy: WebhookProxy;
|
||||
};
|
||||
|
||||
@@ -21,6 +22,11 @@ export type IJoinOptions = {
|
||||
*/
|
||||
skipDisplayName?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip setting the moderator role for the first participant (whether to use jwt for it).
|
||||
*/
|
||||
skipFirstModerator?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip in meeting checks like ice connected and send receive data. For single in meeting participant.
|
||||
*/
|
||||
|
||||
66
tests/pageobjects/AVModerationMenu.ts
Normal file
66
tests/pageobjects/AVModerationMenu.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
const START_AUDIO_MODERATION = 'participants-pane-context-menu-start-audio-moderation';
|
||||
const STOP_AUDIO_MODERATION = 'participants-pane-context-menu-stop-audio-moderation';
|
||||
const START_VIDEO_MODERATION = 'participants-pane-context-menu-start-video-moderation';
|
||||
const STOP_VIDEO_MODERATION = 'participants-pane-context-menu-stop-video-moderation';
|
||||
|
||||
/**
|
||||
* Represents the Audio Video Moderation menu in the participants pane.
|
||||
*/
|
||||
export default class AVModerationMenu {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Represents the Audio Video Moderation menu in the participants pane.
|
||||
* @param participant
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the start audio moderation menu item.
|
||||
*/
|
||||
async clickStartAudioModeration() {
|
||||
await this.clickButton(START_AUDIO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the stop audio moderation menu item.
|
||||
*/
|
||||
async clickStopAudioModeration() {
|
||||
await this.clickButton(STOP_AUDIO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the start video moderation menu item.
|
||||
*/
|
||||
async clickStartVideoModeration() {
|
||||
await this.clickButton(START_VIDEO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the stop audio moderation menu item.
|
||||
*/
|
||||
async clickStopVideoModeration() {
|
||||
await this.clickButton(STOP_VIDEO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a context menu button.
|
||||
* @param id
|
||||
* @private
|
||||
*/
|
||||
private async clickButton(id: string) {
|
||||
const button = this.participant.driver.$(`#${id}`);
|
||||
|
||||
await button.waitForDisplayed();
|
||||
await button.click();
|
||||
|
||||
await button.moveTo({
|
||||
xOffset: -40,
|
||||
yOffset: -40
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
/**
|
||||
* Filmstrip elements.
|
||||
*/
|
||||
@@ -40,7 +42,7 @@ export default class Filmstrip {
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Audio mute icon is ${reverse ? '' : 'not'} displayed for ${testee.name}`
|
||||
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,4 +79,56 @@ export default class Filmstrip {
|
||||
|
||||
return await elem.isExisting() ? elem.getAttribute('src') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants moderator rights to a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async grantModerator(participant: Participant) {
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'grantmoderatorlink', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the link in the remote participant actions menu.
|
||||
* @param participantId
|
||||
* @param linkClassname
|
||||
* @param dialogConfirm
|
||||
* @private
|
||||
*/
|
||||
private async clickOnRemoteMenuLink(participantId: string, linkClassname: string, dialogConfirm: boolean) {
|
||||
const thumbnail = this.participant.driver.$(
|
||||
`//span[@id='participant_${participantId}']//span[@id='remotevideomenu']`);
|
||||
|
||||
await thumbnail.moveTo();
|
||||
|
||||
const popoverElement = this.participant.driver.$(
|
||||
`//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
|
||||
|
||||
await popoverElement.waitForDisplayed();
|
||||
await popoverElement.click();
|
||||
|
||||
if (dialogConfirm) {
|
||||
await new BaseDialog(this.participant).clickOkButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the audio of a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async muteAudio(participant: Participant) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await this.participant.driver.$(`#participant-item-${participantId}`).moveTo();
|
||||
|
||||
await this.participant.driver.$(`button[data-testid="mute-audio-${participantId}"]`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the video of a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async muteVideo(participant: Participant) {
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
|
||||
}
|
||||
}
|
||||
|
||||
54
tests/pageobjects/Notifications.ts
Normal file
54
tests/pageobjects/Notifications.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
|
||||
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
|
||||
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
|
||||
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
|
||||
const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
|
||||
|
||||
/**
|
||||
* Gathers all notifications logic in the UI and obtaining those.
|
||||
*/
|
||||
export default class Notifications {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Represents the Audio Video Moderation menu in the participants pane.
|
||||
* @param participant
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the raised hand notification to be displayed.
|
||||
* The notification on moderators page when the participant tries to unmute.
|
||||
*/
|
||||
async waitForRaisedHandNotification() {
|
||||
const displayNameEl
|
||||
= this.participant.driver.$(`div[data-testid="${RAISE_HAND_NOTIFICATION_ID}"]`);
|
||||
|
||||
await displayNameEl.waitForExist({ timeout: 2000 });
|
||||
await displayNameEl.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification on participants page when the moderator asks to unmute.
|
||||
*/
|
||||
async waitForAskToUnmuteNotification() {
|
||||
const displayNameEl
|
||||
= this.participant.driver.$(`div[data-testid="${ASK_TO_UNMUTE_NOTIFICATION_ID}"]`);
|
||||
|
||||
await displayNameEl.waitForExist({ timeout: 2000 });
|
||||
await displayNameEl.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses any join notifications.
|
||||
*/
|
||||
async dismissAnyJoinNotification() {
|
||||
await Promise.allSettled(
|
||||
[ `${JOIN_ONE_TEST_ID}-dismiss`, `${JOIN_TWO_TEST_ID}-dismiss`, `${JOIN_MULTIPLE_TEST_ID}-dismiss` ]
|
||||
.map(async id => this.participant.driver.$(`#${id}"]`).click()));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import AVModerationMenu from './AVModerationMenu';
|
||||
|
||||
/**
|
||||
* Classname of the closed/hidden participants pane
|
||||
*/
|
||||
@@ -20,6 +22,13 @@ export default class ParticipantsPane {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio video moderation menu.
|
||||
*/
|
||||
getAVModerationMenu() {
|
||||
return new AVModerationMenu(this.participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pane is open.
|
||||
*/
|
||||
@@ -33,7 +42,11 @@ export default class ParticipantsPane {
|
||||
async open() {
|
||||
await this.participant.getToolbar().clickParticipantsPaneButton();
|
||||
|
||||
await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed();
|
||||
const pane = this.participant.driver.$(`.${PARTICIPANTS_PANE}`);
|
||||
|
||||
await pane.waitForExist();
|
||||
await pane.waitForStable();
|
||||
await pane.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,11 +81,85 @@ export default class ParticipantsPane {
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Video mute icon is ${reverse ? '' : 'not'} displayed for ${testee.name}`
|
||||
timeoutMsg: `Video mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the context menu button in the participants pane.
|
||||
*/
|
||||
async clickContextMenuButton() {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const menu = this.participant.driver.$('#participants-pane-context-menu');
|
||||
|
||||
await menu.waitForDisplayed();
|
||||
await menu.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trys to click allow video button.
|
||||
* @param participantToUnmute
|
||||
*/
|
||||
async allowVideo(participantToUnmute: Participant) {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const participantId = await participantToUnmute.getEndpointId();
|
||||
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
|
||||
|
||||
await participantItem.waitForExist();
|
||||
await participantItem.moveTo();
|
||||
|
||||
const unmuteButton = this.participant.driver
|
||||
.$(`button[data-testid="unmute-video-${participantId}"]`);
|
||||
|
||||
await unmuteButton.waitForExist();
|
||||
await unmuteButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trys to click ask to unmute button.
|
||||
* @param participantToUnmute
|
||||
* @param fromContextMenu
|
||||
*/
|
||||
async askToUnmute(participantToUnmute: Participant, fromContextMenu: boolean) {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
await this.participant.getNotifications().dismissAnyJoinNotification();
|
||||
|
||||
const participantId = await participantToUnmute.getEndpointId();
|
||||
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
|
||||
|
||||
await participantItem.waitForExist();
|
||||
await participantItem.waitForStable();
|
||||
await participantItem.waitForDisplayed();
|
||||
await participantItem.moveTo();
|
||||
|
||||
if (fromContextMenu) {
|
||||
const meetingParticipantMoreOptions = this.participant.driver
|
||||
.$(`[data-testid="participant-more-options-${participantId}"]`);
|
||||
|
||||
await meetingParticipantMoreOptions.waitForExist();
|
||||
await meetingParticipantMoreOptions.waitForDisplayed();
|
||||
await meetingParticipantMoreOptions.waitForStable();
|
||||
await meetingParticipantMoreOptions.moveTo();
|
||||
await meetingParticipantMoreOptions.click();
|
||||
}
|
||||
|
||||
const unmuteButton = this.participant.driver
|
||||
.$(`[data-testid="unmute-audio-${participantId}"]`);
|
||||
|
||||
await unmuteButton.waitForExist();
|
||||
await unmuteButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const OVERFLOW_MENU = 'More actions menu';
|
||||
const OVERFLOW = 'More actions';
|
||||
const PARTICIPANTS = 'Open participants pane';
|
||||
const PROFILE = 'Edit your profile';
|
||||
const RAISE_HAND = 'Raise your hand';
|
||||
const VIDEO_QUALITY = 'Manage video quality';
|
||||
const VIDEO_MUTE = 'Stop camera';
|
||||
const VIDEO_UNMUTE = 'Start camera';
|
||||
@@ -142,6 +143,14 @@ export default class Toolbar {
|
||||
return this.clickButtonInOverflowMenu(PROFILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the raise hand button that enables participants will to speak.
|
||||
*/
|
||||
async clickRaiseHandButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Raise hand Button');
|
||||
await this.getButton(RAISE_HAND).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the overflow menu is open and clicks on a specified button.
|
||||
* @param accessibilityLabel The accessibility label of the button to be clicked.
|
||||
@@ -150,6 +159,7 @@ export default class Toolbar {
|
||||
private async clickButtonInOverflowMenu(accessibilityLabel: string) {
|
||||
await this.openOverflowMenu();
|
||||
|
||||
this.participant.log(`Clicking on: ${accessibilityLabel}`);
|
||||
await this.getButton(accessibilityLabel).click();
|
||||
|
||||
await this.closeOverflowMenu();
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Audio only - ', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants(context);
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipFirstModerator: true
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -16,10 +18,12 @@ describe('Audio only - ', () => {
|
||||
* Verifies that participant1 sees avatars for itself and other participants.
|
||||
*/
|
||||
it('avatars check', async () => {
|
||||
await context.p1.driver.$('//div[@id="dominantSpeaker"]').waitForDisplayed();
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.driver.$('//div[@id="dominantSpeaker"]').waitForDisplayed();
|
||||
|
||||
// Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
|
||||
await context.p1.assertThumbnailShowsAvatar(context.p1);
|
||||
await p1.assertThumbnailShowsAvatar(p1);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -35,11 +39,13 @@ describe('Audio only - ', () => {
|
||||
* @param enable
|
||||
*/
|
||||
async function setAudioOnlyAndCheck(enable: boolean) {
|
||||
await context.p1.getVideoQualityDialog().setVideoQuality(enable);
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getVideoQualityDialog().setVideoQuality(enable);
|
||||
|
||||
await verifyVideoMute(enable);
|
||||
|
||||
await context.p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
|
||||
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
|
||||
.waitForDisplayed({ reverse: !enable });
|
||||
}
|
||||
|
||||
@@ -48,11 +54,13 @@ describe('Audio only - ', () => {
|
||||
* @param muted
|
||||
*/
|
||||
async function verifyVideoMute(muted: boolean) {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// Verify the observer sees the testee in the desired muted state.
|
||||
await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, !muted);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
|
||||
|
||||
// Verify the testee sees itself in the desired muted state.
|
||||
await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, !muted);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +68,10 @@ describe('Audio only - ', () => {
|
||||
* as video muted.
|
||||
*/
|
||||
it('mute video, set twice and check muted', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
// Mute video on participant1.
|
||||
await context.p1.getToolbar().clickVideoMuteButton();
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await verifyVideoMute(true);
|
||||
|
||||
@@ -69,7 +79,7 @@ describe('Audio only - ', () => {
|
||||
await setAudioOnlyAndCheck(true);
|
||||
|
||||
// Disable audio-only mode.
|
||||
await context.p1.getVideoQualityDialog().setVideoQuality(false);
|
||||
await p1.getVideoQualityDialog().setVideoQuality(false);
|
||||
|
||||
// p1 should stay muted since it was muted before audio-only was enabled.
|
||||
await verifyVideoMute(true);
|
||||
@@ -77,7 +87,7 @@ describe('Audio only - ', () => {
|
||||
|
||||
it('unmute video and check not muted', async () => {
|
||||
// Unmute video on participant1.
|
||||
await context.p1.getToolbar().clickVideoUnmuteButton();
|
||||
await ctx.p1.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await verifyVideoMute(false);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
|
||||
* Tests PARTICIPANT_LEFT webhook.
|
||||
*/
|
||||
async function checkParticipantLeftHook(p: Participant, reason: string) {
|
||||
const { webhooksProxy } = context;
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_LEFT webhook
|
||||
@@ -20,10 +20,10 @@ async function checkParticipantLeftHook(p: Participant, reason: string) {
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await context.webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect('PARTICIPANT_LEFT').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(context.conferenceJid);
|
||||
expect(event.data.conference).toBe(ctx.conferenceJid);
|
||||
expect(event.data.disconnectReason).toBe(reason);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.participantId).toBe(await p.getEndpointId());
|
||||
@@ -32,12 +32,10 @@ async function checkParticipantLeftHook(p: Participant, reason: string) {
|
||||
|
||||
describe('Participants presence - ', () => {
|
||||
it('joining the meeting', async () => {
|
||||
context.iframeAPI = true;
|
||||
|
||||
// ensure 2 participants one moderator and one guest, we will load both with iframeAPI
|
||||
await ensureTwoParticipants(context);
|
||||
await ensureTwoParticipants(ctx);
|
||||
|
||||
const { p1, p2, webhooksProxy } = context;
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
// let's populate endpoint ids
|
||||
await Promise.all([
|
||||
@@ -48,12 +46,8 @@ describe('Participants presence - ', () => {
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator'))
|
||||
.withContext('Is p1 moderator')
|
||||
.toBeTrue();
|
||||
expect(await p2.getIframeAPI().getEventResult('isModerator'))
|
||||
.withContext('Is p2 non-moderator')
|
||||
.toBeFalse();
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p2.getIframeAPI().getEventResult('isModerator')).toBe(false);
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
@@ -66,7 +60,7 @@ describe('Participants presence - ', () => {
|
||||
{ participantId: string; }
|
||||
];
|
||||
eventType: string;
|
||||
} = await context.webhooksProxy.waitForEvent('USAGE');
|
||||
} = await webhooksProxy.waitForEvent('USAGE');
|
||||
|
||||
expect('USAGE').toBe(event.eventType);
|
||||
|
||||
@@ -85,11 +79,11 @@ describe('Participants presence - ', () => {
|
||||
|
||||
it('participants info',
|
||||
async () => {
|
||||
const { p1, roomName, webhooksProxy } = context;
|
||||
const { p1, roomName, webhooksProxy } = ctx;
|
||||
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
|
||||
|
||||
expect(roomsInfo).toBeDefined();
|
||||
expect(roomsInfo.isMainRoom).toBeTrue();
|
||||
expect(roomsInfo.isMainRoom).toBe(true);
|
||||
|
||||
expect(roomsInfo.id).toBeDefined();
|
||||
const { node: roomNode } = parseJid(roomsInfo.id);
|
||||
@@ -98,7 +92,7 @@ describe('Participants presence - ', () => {
|
||||
|
||||
const { node, resource } = parseJid(roomsInfo.jid);
|
||||
|
||||
context.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
ctx.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
|
||||
@@ -117,17 +111,17 @@ describe('Participants presence - ', () => {
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await context.webhooksProxy.waitForEvent('ROOM_CREATED');
|
||||
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
|
||||
|
||||
expect('ROOM_CREATED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(context.conferenceJid);
|
||||
expect(event.data.conference).toBe(ctx.conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('participants pane', async () => {
|
||||
const { p1 } = context;
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
@@ -145,7 +139,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
it('grant moderator', async () => {
|
||||
const { p1, p2, webhooksProxy } = context;
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
|
||||
@@ -179,7 +173,7 @@ describe('Participants presence - ', () => {
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await context.webhooksProxy.waitForEvent('ROLE_CHANGED');
|
||||
} = await webhooksProxy.waitForEvent('ROLE_CHANGED');
|
||||
|
||||
expect('ROLE_CHANGED').toBe(event.eventType);
|
||||
expect(event.data.role).toBe('moderator');
|
||||
@@ -189,7 +183,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
it('kick participant', async () => {
|
||||
const { p1, p2 } = context;
|
||||
const { p1, p2, roomName } = ctx;
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
@@ -235,7 +229,7 @@ describe('Participants presence - ', () => {
|
||||
local: true,
|
||||
name: p1DisplayName
|
||||
}
|
||||
})).toBeTrue();
|
||||
})).toBe(true);
|
||||
|
||||
expect(isEqual(eventP2, {
|
||||
kicked: {
|
||||
@@ -247,7 +241,7 @@ describe('Participants presence - ', () => {
|
||||
id: p1EpId,
|
||||
name: p1DisplayName
|
||||
}
|
||||
})).toBeTrue();
|
||||
})).toBe(true);
|
||||
|
||||
const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
|
||||
await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
@@ -256,11 +250,11 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(context.roomName);
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
});
|
||||
|
||||
it('join after kick', async () => {
|
||||
const { p1, webhooksProxy } = context;
|
||||
const { p1, webhooksProxy } = ctx;
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantJoined');
|
||||
await p1.getIframeAPI().addEventListener('participantMenuButtonClick');
|
||||
@@ -268,7 +262,8 @@ describe('Participants presence - ', () => {
|
||||
webhooksProxy?.clearCache();
|
||||
|
||||
// join again
|
||||
await ensureTwoParticipants(context);
|
||||
await ensureTwoParticipants(ctx);
|
||||
const { p2 } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_JOINED webhook
|
||||
@@ -282,14 +277,14 @@ describe('Participants presence - ', () => {
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await context.webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(context.conferenceJid);
|
||||
expect(event.data.conference).toBe(ctx.conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.moderator).toBe(false);
|
||||
expect(event.data.name).toBe(await context.p2.getLocalDisplayName());
|
||||
expect(event.data.participantId).toBe(await context.p2.getEndpointId());
|
||||
expect(event.data.name).toBe(await p2.getLocalDisplayName());
|
||||
expect(event.data.participantId).toBe(await p2.getEndpointId());
|
||||
}
|
||||
|
||||
await p1.switchToAPI();
|
||||
@@ -300,7 +295,6 @@ describe('Participants presence - ', () => {
|
||||
timeoutMsg: 'participantJoined not received'
|
||||
});
|
||||
|
||||
const { p2 } = context;
|
||||
const p2DisplayName = await p2.getLocalDisplayName();
|
||||
|
||||
expect(event).toBeDefined();
|
||||
@@ -311,7 +305,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
it('overwrite names', async () => {
|
||||
const { p1, p2 } = context;
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
@@ -337,7 +331,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
it('hangup', async () => {
|
||||
const { p1, p2 } = context;
|
||||
const { p1, p2, roomName } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
@@ -354,7 +348,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(context.roomName);
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(p2, 'left');
|
||||
|
||||
@@ -368,7 +362,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
it('dispose conference', async () => {
|
||||
const { p1, webhooksProxy } = context;
|
||||
const { conferenceJid, p1, roomName, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
@@ -384,7 +378,7 @@ describe('Participants presence - ', () => {
|
||||
});
|
||||
|
||||
expect(eventConferenceLeft).toBeDefined();
|
||||
expect(eventConferenceLeft.roomName).toBe(context.roomName);
|
||||
expect(eventConferenceLeft.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(p1, 'left');
|
||||
if (webhooksProxy) {
|
||||
@@ -396,10 +390,10 @@ describe('Participants presence - ', () => {
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await context.webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
} = await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
expect('ROOM_DESTROYED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(context.conferenceJid);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
|
||||
@@ -4,30 +4,32 @@ import { ensureThreeParticipants, muteAudioAndCheck } from '../../helpers/partic
|
||||
|
||||
describe('ActiveSpeaker ', () => {
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants(context);
|
||||
await ensureThreeParticipants(ctx);
|
||||
|
||||
await muteAudioAndCheck(context.p1, context.p2);
|
||||
await muteAudioAndCheck(context.p2, context.p1);
|
||||
await muteAudioAndCheck(context.p3, context.p1);
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
await muteAudioAndCheck(p2, p1);
|
||||
await muteAudioAndCheck(p3, p1);
|
||||
|
||||
// participant1 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(context.p1, context.p2, context.p3);
|
||||
await testActiveSpeaker(p1, p2, p3);
|
||||
|
||||
// participant3 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(context.p3, context.p2, context.p1);
|
||||
await testActiveSpeaker(p3, p2, p1);
|
||||
|
||||
// participant2 becomes active speaker - check from participant1's perspective
|
||||
await testActiveSpeaker(context.p2, context.p1, context.p3);
|
||||
await testActiveSpeaker(p2, p1, p3);
|
||||
|
||||
// check the displayed speakers, there should be only one speaker
|
||||
await assertOneDominantSpeaker(context.p1);
|
||||
await assertOneDominantSpeaker(context.p2);
|
||||
await assertOneDominantSpeaker(context.p3);
|
||||
await assertOneDominantSpeaker(p1);
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tries to make given participant an active speaker by un-muting it.
|
||||
* Tries to make given participant an active speaker by unmuting it.
|
||||
* Verifies from {@code participant2}'s perspective that the active speaker
|
||||
* has been displayed on the large video area. Mutes him back.
|
||||
*
|
||||
@@ -36,7 +38,6 @@ describe('ActiveSpeaker ', () => {
|
||||
* @param {Participant} otherParticipant1 - <tt>Participant</tt> of the participant who will be observing and verifying
|
||||
* active speaker change.
|
||||
* @param {Participant} otherParticipant2 - Used only to print some debugging info.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function testActiveSpeaker(
|
||||
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
|
||||
@@ -85,7 +86,6 @@ async function testActiveSpeaker(
|
||||
* indicator displayed equals 1.
|
||||
*
|
||||
* @param {Participant} participant - The participant to check.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function assertOneDominantSpeaker(participant: Participant) {
|
||||
expect(await participant.driver.$$(
|
||||
|
||||
284
tests/specs/3way/audioVideoModeration.spec.ts
Normal file
284
tests/specs/3way/audioVideoModeration.spec.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants, ensureTwoParticipants,
|
||||
unmuteAudioAndCheck,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('AVModeration -', () => {
|
||||
|
||||
it('check for moderators', async () => {
|
||||
// if all 3 participants are moderators, skip this test
|
||||
await ensureThreeParticipants(ctx);
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()
|
||||
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('check audio enable/disable', async () => {
|
||||
const { p1, p3 } = ctx;
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// Here we want to try unmuting and check that we are still muted.
|
||||
await tryToAudioUnmuteAndCheck(p3, p1);
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await unmuteAudioAndCheck(p3, p1);
|
||||
});
|
||||
|
||||
it('check video enable/disable', async () => {
|
||||
const { p1, p3 } = ctx;
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// Here we want to try unmuting and check that we are still muted.
|
||||
await tryToVideoUnmuteAndCheck(p3, p1);
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await unmuteVideoAndCheck(p3, p1);
|
||||
});
|
||||
|
||||
it('unmute by moderator', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await unmuteByModerator(p1, p3, true, true);
|
||||
|
||||
// moderation is stopped at this point, make sure participants 1 & 2 are also unmuted,
|
||||
// participant3 was unmuted by unmuteByModerator
|
||||
await unmuteAudioAndCheck(p2, p1);
|
||||
await unmuteVideoAndCheck(p2, p1);
|
||||
await unmuteAudioAndCheck(p1, p2);
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
});
|
||||
|
||||
it('hangup and change moderator', async () => {
|
||||
await Promise.all([ ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureThreeParticipants(ctx);
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p2.getToolbar().clickRaiseHandButton();
|
||||
await p3.getToolbar().clickRaiseHandButton();
|
||||
|
||||
await p1.hangup();
|
||||
|
||||
// we don't use ensureThreeParticipants to avoid all meeting join checks
|
||||
// all participants are muted and checks for media will fail
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
// After p1 re-joins either p2 or p3 is promoted to moderator. They should still be muted.
|
||||
const isP2Moderator = await p2.isModerator();
|
||||
const moderator = isP2Moderator ? p2 : p3;
|
||||
const nonModerator = isP2Moderator ? p3 : p2;
|
||||
const moderatorParticipantsPane = moderator.getParticipantsPane();
|
||||
const nonModeratorParticipantsPane = nonModerator.getParticipantsPane();
|
||||
|
||||
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
|
||||
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
|
||||
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(nonModerator, p1);
|
||||
await unmuteVideoAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
});
|
||||
it('grant moderator', async () => {
|
||||
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureThreeParticipants(ctx);
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p1.getFilmstrip().grantModerator(p3);
|
||||
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.isModerator(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: `${p3.name} is not moderator`
|
||||
});
|
||||
|
||||
await unmuteByModerator(p3, p2, false, true);
|
||||
});
|
||||
it('ask to unmute', async () => {
|
||||
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureTwoParticipants(ctx);
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// mute p2
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// ask p2 to unmute
|
||||
await p1.getParticipantsPane().askToUnmute(p2, true);
|
||||
|
||||
await p2.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await p1.getParticipantsPane().close();
|
||||
});
|
||||
it('remove from whitelist', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await unmuteByModerator(p1, p2, true, false);
|
||||
|
||||
// p1 mute audio on p2 and check
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
|
||||
// we try to unmute and test it that it was still muted
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
|
||||
// stop video and check
|
||||
await p1.getFilmstrip().muteVideo(p2);
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
await tryToVideoUnmuteAndCheck(p2, p1);
|
||||
});
|
||||
it('join moderated', async () => {
|
||||
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
const p1ParticipantsPane = ctx.p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
// join with second participant and check
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
await tryToVideoUnmuteAndCheck(p2, p1);
|
||||
|
||||
// asked to unmute and check
|
||||
await unmuteByModerator(p1, p2, false, false);
|
||||
|
||||
// mute and check
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks a user can unmute after being asked by moderator.
|
||||
* @param moderator - The participant that is moderator.
|
||||
* @param participant - The participant being asked to unmute.
|
||||
* @param turnOnModeration - if we want to turn on moderation before testing (when it is currently off).
|
||||
* @param stopModeration - true if moderation to be stopped when done.
|
||||
*/
|
||||
async function unmuteByModerator(
|
||||
moderator: Participant,
|
||||
participant: Participant,
|
||||
turnOnModeration: boolean,
|
||||
stopModeration: boolean) {
|
||||
const moderatorParticipantsPane = moderator.getParticipantsPane();
|
||||
|
||||
if (turnOnModeration) {
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await moderatorParticipantsPane.close();
|
||||
}
|
||||
|
||||
// raise hand to speak
|
||||
await participant.getToolbar().clickRaiseHandButton();
|
||||
await moderator.getNotifications().waitForRaisedHandNotification();
|
||||
|
||||
// ask participant to unmute
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await moderatorParticipantsPane.askToUnmute(participant, false);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(participant, moderator);
|
||||
await unmuteVideoAndCheck(participant, moderator);
|
||||
|
||||
if (stopModeration) {
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
|
||||
await moderatorParticipantsPane.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of moderation, tries to audio unmute but stays muted.
|
||||
* Checks locally and remotely that this is still the case.
|
||||
* @param participant
|
||||
* @param observer
|
||||
*/
|
||||
async function tryToAudioUnmuteAndCheck(participant: Participant, observer: Participant) {
|
||||
// try to audio unmute and check
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Check local audio muted icon state
|
||||
await participant.getFilmstrip().assertAudioMuteIconIsDisplayed(participant);
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of moderation, tries to video unmute but stays muted.
|
||||
* Checks locally and remotely that this is still the case.
|
||||
* @param participant
|
||||
* @param observer
|
||||
*/
|
||||
async function tryToVideoUnmuteAndCheck(participant: Participant, observer: Participant) {
|
||||
// try to video unmute and check
|
||||
await participant.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
// Check local audio muted icon state
|
||||
await participant.getParticipantsPane().assertVideoMuteIconIsDisplayed(participant);
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(participant);
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import {
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
unMuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
const EMAIL = 'support@jitsi.org';
|
||||
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
|
||||
|
||||
describe('Avatar - ', () => {
|
||||
it('setup the meeting', async () => {
|
||||
// Start p1
|
||||
await ensureTwoParticipants(context, {
|
||||
skipDisplayName: true
|
||||
});
|
||||
});
|
||||
|
||||
it('change and check', async () => {
|
||||
// check default avatar for p1 on p2
|
||||
await context.p2.assertDefaultAvatarExist(context.p1);
|
||||
|
||||
await context.p1.getToolbar().clickProfileButton();
|
||||
|
||||
const settings = context.p1.getSettingsDialog();
|
||||
|
||||
await settings.waitForDisplay();
|
||||
await settings.setEmail(EMAIL);
|
||||
await settings.submit();
|
||||
|
||||
// check if the local avatar in the toolbar menu has changed
|
||||
await context.p1.driver.waitUntil(
|
||||
async () => (await context.p1.getToolbar().getProfileImage())?.includes(HASH), {
|
||||
timeout: 3000, // give more time for the initial download of the image
|
||||
timeoutMsg: 'Avatar has not changed for p1'
|
||||
});
|
||||
|
||||
// check if the avatar in the local thumbnail has changed
|
||||
expect(await context.p1.getLocalVideoAvatar()).toContain(HASH);
|
||||
|
||||
const p1EndpointId = await context.p1.getEndpointId();
|
||||
|
||||
await context.p2.driver.waitUntil(
|
||||
async () => (await context.p2.getFilmstrip().getAvatar(p1EndpointId))?.includes(HASH), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Avatar has not changed for p1 on p2'
|
||||
});
|
||||
|
||||
// check if the avatar in the large video has changed
|
||||
expect(await context.p2.getLargeVideoAvatar()).toContain(HASH);
|
||||
|
||||
// we check whether the default avatar of participant2 is displayed on both sides
|
||||
await context.p1.assertDefaultAvatarExist(context.p2);
|
||||
await context.p2.assertDefaultAvatarExist(context.p2);
|
||||
|
||||
// the problem on FF where we can send keys to the input field,
|
||||
// and the m from the text can mute the call, check whether we are muted
|
||||
await context.p2.getFilmstrip().assertAudioMuteIconIsDisplayed(context.p1, true);
|
||||
});
|
||||
|
||||
it('when video muted', async () => {
|
||||
await context.p2.hangup();
|
||||
|
||||
// Mute p1's video
|
||||
await context.p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1);
|
||||
|
||||
await context.p1.driver.waitUntil(
|
||||
async () => (await context.p1.getLargeVideoAvatar())?.includes(HASH), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
const p1LargeSrc = await context.p1.getLargeVideoAvatar();
|
||||
const p1ThumbSrc = await context.p1.getLocalVideoAvatar();
|
||||
|
||||
// Check if avatar on large video is the same as on local thumbnail
|
||||
expect(p1ThumbSrc).toBe(p1LargeSrc);
|
||||
|
||||
// Join p2
|
||||
await ensureTwoParticipants(context, {
|
||||
skipDisplayName: true
|
||||
});
|
||||
|
||||
// Verify that p1 is muted from the perspective of p2
|
||||
await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1);
|
||||
|
||||
await context.p2.getFilmstrip().pinParticipant(context.p1);
|
||||
|
||||
// Check if p1's avatar is on large video now
|
||||
await context.p2.driver.waitUntil(
|
||||
async () => await context.p2.getLargeVideoAvatar() === p1LargeSrc, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
// p1 pins p2's video
|
||||
await context.p1.getFilmstrip().pinParticipant(context.p2);
|
||||
|
||||
// Check if avatar is displayed on p1's local video thumbnail
|
||||
await context.p1.assertThumbnailShowsAvatar(context.p1, false, false, true);
|
||||
|
||||
// Unmute - now local avatar should be hidden and local video displayed
|
||||
await unMuteVideoAndCheck(context.p1, context.p2);
|
||||
|
||||
await context.p1.asserLocalThumbnailShowsVideo();
|
||||
|
||||
// Now both p1 and p2 have video muted
|
||||
await context.p1.getToolbar().clickVideoMuteButton();
|
||||
await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1);
|
||||
await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1);
|
||||
|
||||
await context.p2.getToolbar().clickVideoMuteButton();
|
||||
await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p2);
|
||||
await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p2);
|
||||
|
||||
// Start the third participant
|
||||
await ensureThreeParticipants(context);
|
||||
|
||||
// Pin local video and verify avatars are displayed
|
||||
await context.p3.getFilmstrip().pinParticipant(context.p3);
|
||||
|
||||
await context.p3.assertThumbnailShowsAvatar(context.p1, false, false, true);
|
||||
await context.p3.assertThumbnailShowsAvatar(context.p2, false, true);
|
||||
|
||||
const p1EndpointId = await context.p1.getEndpointId();
|
||||
const p2EndpointId = await context.p2.getEndpointId();
|
||||
|
||||
expect(await context.p3.getFilmstrip().getAvatar(p1EndpointId)).toBe(p1ThumbSrc);
|
||||
|
||||
// Click on p1's video
|
||||
await context.p3.getFilmstrip().pinParticipant(context.p1);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await context.p3.driver.waitUntil(
|
||||
async () => await context.p3.getLargeVideoResource() === p1EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${context.p1.name}`
|
||||
});
|
||||
|
||||
await context.p3.assertDisplayNameVisibleOnStage(
|
||||
await context.p3.getFilmstrip().getRemoteDisplayName(p1EndpointId));
|
||||
|
||||
// p2 has the default avatar
|
||||
await context.p3.assertThumbnailShowsAvatar(context.p2, false, true);
|
||||
await context.p3.assertThumbnailShowsAvatar(context.p3, true);
|
||||
|
||||
// Click on p2's video
|
||||
await context.p3.getFilmstrip().pinParticipant(context.p2);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await context.p3.driver.waitUntil(
|
||||
async () => await context.p3.getLargeVideoResource() === p2EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${context.p2.name}`
|
||||
});
|
||||
|
||||
await context.p3.assertDisplayNameVisibleOnStage(
|
||||
await context.p3.getFilmstrip().getRemoteDisplayName(p2EndpointId)
|
||||
);
|
||||
|
||||
await context.p3.assertThumbnailShowsAvatar(context.p1, false, false, true);
|
||||
await context.p3.assertThumbnailShowsAvatar(context.p3, true);
|
||||
|
||||
await context.p3.hangup();
|
||||
|
||||
// Unmute p1's and p2's videos
|
||||
await context.p1.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, true);
|
||||
await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, true);
|
||||
});
|
||||
|
||||
it('email persistence', async () => {
|
||||
await context.p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await context.p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
|
||||
await context.p1.hangup();
|
||||
|
||||
await ensureTwoParticipants(context, {
|
||||
skipDisplayName: true
|
||||
});
|
||||
|
||||
await context.p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await context.p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
});
|
||||
});
|
||||
202
tests/specs/3way/avatars.spec.ts
Normal file
202
tests/specs/3way/avatars.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
const EMAIL = 'support@jitsi.org';
|
||||
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
|
||||
|
||||
describe('Avatar - ', () => {
|
||||
it('setup the meeting', async () => {
|
||||
// Start p1
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipDisplayName: true,
|
||||
|
||||
// no default avatar if we have used to join a token with an avatar and no option to set it
|
||||
skipFirstModerator: true
|
||||
});
|
||||
});
|
||||
|
||||
it('change and check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// check default avatar for p1 on p2
|
||||
await p2.assertDefaultAvatarExist(p1);
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
const settings = p1.getSettingsDialog();
|
||||
|
||||
await settings.waitForDisplay();
|
||||
await settings.setEmail(EMAIL);
|
||||
await settings.submit();
|
||||
|
||||
// check if the local avatar in the toolbar menu has changed
|
||||
await p1.driver.waitUntil(
|
||||
async () => (await p1.getToolbar().getProfileImage())?.includes(HASH), {
|
||||
timeout: 3000, // give more time for the initial download of the image
|
||||
timeoutMsg: 'Avatar has not changed for p1'
|
||||
});
|
||||
|
||||
// check if the avatar in the local thumbnail has changed
|
||||
expect(await p1.getLocalVideoAvatar()).toContain(HASH);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
async () => (await p2.getFilmstrip().getAvatar(p1EndpointId))?.includes(HASH), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Avatar has not changed for p1 on p2'
|
||||
});
|
||||
|
||||
// check if the avatar in the large video has changed
|
||||
expect(await p2.getLargeVideoAvatar()).toContain(HASH);
|
||||
|
||||
// we check whether the default avatar of participant2 is displayed on both sides
|
||||
await p1.assertDefaultAvatarExist(p2);
|
||||
await p2.assertDefaultAvatarExist(p2);
|
||||
|
||||
// the problem on FF where we can send keys to the input field,
|
||||
// and the m from the text can mute the call, check whether we are muted
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
});
|
||||
|
||||
it('when video muted', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await ctx.p2.hangup();
|
||||
|
||||
// Mute p1's video
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => (await p1.getLargeVideoAvatar())?.includes(HASH), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
const p1LargeSrc = await p1.getLargeVideoAvatar();
|
||||
const p1ThumbSrc = await p1.getLocalVideoAvatar();
|
||||
|
||||
// Check if avatar on large video is the same as on local thumbnail
|
||||
expect(p1ThumbSrc).toBe(p1LargeSrc);
|
||||
|
||||
// Join p2
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipDisplayName: true
|
||||
});
|
||||
const { p2 } = ctx;
|
||||
|
||||
// Verify that p1 is muted from the perspective of p2
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p2.getFilmstrip().pinParticipant(p1);
|
||||
|
||||
// Check if p1's avatar is on large video now
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getLargeVideoAvatar() === p1LargeSrc, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
// p1 pins p2's video
|
||||
await p1.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
// Check if avatar is displayed on p1's local video thumbnail
|
||||
await p1.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
|
||||
// Unmute - now local avatar should be hidden and local video displayed
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
|
||||
await p1.asserLocalThumbnailShowsVideo();
|
||||
|
||||
// Now both p1 and p2 have video muted
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p2.getToolbar().clickVideoMuteButton();
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
// Start the third participant
|
||||
await ensureThreeParticipants(ctx);
|
||||
const { p3 } = ctx;
|
||||
|
||||
// Pin local video and verify avatars are displayed
|
||||
await p3.getFilmstrip().pinParticipant(p3);
|
||||
|
||||
await p3.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p2, false, true);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
|
||||
expect(await p3.getFilmstrip().getAvatar(p1EndpointId)).toBe(p1ThumbSrc);
|
||||
|
||||
// Click on p1's video
|
||||
await p3.getFilmstrip().pinParticipant(p1);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.getLargeVideoResource() === p1EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${p1.name}`
|
||||
});
|
||||
|
||||
await p3.assertDisplayNameVisibleOnStage(
|
||||
await p3.getFilmstrip().getRemoteDisplayName(p1EndpointId));
|
||||
|
||||
// p2 has the default avatar
|
||||
await p3.assertThumbnailShowsAvatar(p2, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p3, true);
|
||||
|
||||
// Click on p2's video
|
||||
await p3.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.getLargeVideoResource() === p2EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${p2.name}`
|
||||
});
|
||||
|
||||
await p3.assertDisplayNameVisibleOnStage(
|
||||
await p3.getFilmstrip().getRemoteDisplayName(p2EndpointId)
|
||||
);
|
||||
|
||||
await p3.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p3, true);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Unmute p1's and p2's videos
|
||||
await p1.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
});
|
||||
|
||||
it('email persistence', async () => {
|
||||
let { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
|
||||
await p1.hangup();
|
||||
|
||||
await ensureTwoParticipants(ctx, {
|
||||
skipDisplayName: true,
|
||||
skipFirstModerator: true
|
||||
});
|
||||
p1 = ctx.p1;
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,8 @@
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"@types/jasmine",
|
||||
"@wdio/jasmine-framework"
|
||||
"@types/mocha",
|
||||
"@wdio/mocha-framework"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import pretty from 'pretty';
|
||||
|
||||
import WebhookProxy from './helpers/WebhookProxy';
|
||||
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
|
||||
import { IContext } from './helpers/types';
|
||||
|
||||
@@ -27,6 +28,10 @@ const chromeArgs = [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-setuid-sandbox',
|
||||
|
||||
// Avoids - "You are checking for animations on an inactive tab, animations do not run for inactive tabs"
|
||||
// when executing waitForStable()
|
||||
'--disable-renderer-backgrounding',
|
||||
`--use-file-for-fake-audio-capture=${process.env.REMOTE_RESOURCE_PATH || 'tests/resources'}/fakeAudioStream.wav`
|
||||
];
|
||||
|
||||
@@ -72,10 +77,10 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
// Default request retries count
|
||||
connectionRetryCount: 3,
|
||||
|
||||
framework: 'jasmine',
|
||||
framework: 'mocha',
|
||||
|
||||
jasmineOpts: {
|
||||
defaultTimeoutInterval: 60_000
|
||||
mochaOpts: {
|
||||
timeout: 60_000
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
@@ -183,16 +188,19 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
}));
|
||||
|
||||
const globalAny: any = global;
|
||||
const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
|
||||
|
||||
globalAny.context = {} as IContext;
|
||||
|
||||
globalAny.context.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
|
||||
globalAny.context.jwtKid = process.env.JWT_KID;
|
||||
globalAny.ctx = {} as IContext;
|
||||
globalAny.ctx.roomName = roomName;
|
||||
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
|
||||
globalAny.ctx.jwtKid = process.env.JWT_KID;
|
||||
},
|
||||
|
||||
after() {
|
||||
if (context.webhooksProxy) {
|
||||
context.webhooksProxy.disconnect();
|
||||
const { ctx }: any = global;
|
||||
|
||||
if (ctx.webhooksProxy) {
|
||||
ctx.webhooksProxy.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -200,9 +208,24 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
* Gets executed before the suite starts (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} suite - Suite details.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
beforeSuite(suite) {
|
||||
const { ctx }: any = global;
|
||||
|
||||
// If we are running the iFrameApi tests, we need to mark it as such and if needed to create the proxy
|
||||
// and connect to it.
|
||||
if (path.basename(suite.file).startsWith('iFrameApi')) {
|
||||
ctx.iframeAPI = true;
|
||||
|
||||
if (!ctx.webhooksProxy
|
||||
&& process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
|
||||
ctx.webhooksProxy = new WebhookProxy(
|
||||
`${process.env.WEBHOOKS_PROXY_URL}&room=${ctx.roomName}`,
|
||||
process.env.WEBHOOKS_PROXY_SHARED_SECRET);
|
||||
ctx.webhooksProxy.connect();
|
||||
}
|
||||
}
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance),
|
||||
`---=== Begin ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
|
||||
@@ -213,11 +236,13 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
* Function to be executed before a test (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} test - Test object.
|
||||
* @returns {Promise<void>}
|
||||
* @param {Object} context - The context object.
|
||||
*/
|
||||
beforeTest(test) {
|
||||
beforeTest(test, context) {
|
||||
ctx.skipSuiteTests && context.skip();
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.fullName} ===---`);
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.title} ===---`);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -231,7 +256,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
*/
|
||||
async afterTest(test, context, { error }) {
|
||||
multiremotebrowser.instances.forEach((instance: string) =>
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.fullName} ===---`));
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.title} ===---`));
|
||||
|
||||
if (error) {
|
||||
const allProcessing: Promise<any>[] = [];
|
||||
|
||||
Reference in New Issue
Block a user