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:
Дамян Минков
2024-12-19 07:12:14 -06:00
committed by GitHub
parent 697c41bffa
commit 7288a6dce4
20 changed files with 2081 additions and 1032 deletions

1795
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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
View File

@@ -1,5 +1,5 @@
import { IContext } from './helpers/types';
declare global {
const context: IContext;
const ctx: IContext;
}

View File

@@ -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);
}
}

View File

@@ -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',

View File

@@ -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.
*/

View 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
});
}
}

View File

@@ -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);
}
}

View 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()));
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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);
});

View File

@@ -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);
}

View File

@@ -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.$$(

View 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);
}

View File

@@ -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);
});
});

View 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);
});
});

View File

@@ -9,8 +9,8 @@
"types": [
"node",
"@wdio/globals/types",
"@types/jasmine",
"@wdio/jasmine-framework"
"@types/mocha",
"@wdio/mocha-framework"
]
}
}

View File

@@ -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>[] = [];