diff --git a/tests/helpers/Participant.ts b/tests/helpers/Participant.ts index 6b12304db7e5..18814451518f 100644 --- a/tests/helpers/Participant.ts +++ b/tests/helpers/Participant.ts @@ -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'; @@ -242,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. * @@ -330,6 +339,13 @@ export class Participant { return new Filmstrip(this); } + /** + * Returns the notifications. + */ + getNotifications(): Notifications { + return new Notifications(this); + } + /** * Returns the participants pane. * diff --git a/tests/helpers/participants.ts b/tests/helpers/participants.ts index 950b33734bef..2ed58664a0fb 100644 --- a/tests/helpers/participants.ts +++ b/tests/helpers/participants.ts @@ -29,36 +29,34 @@ export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions * @returns {Promise} */ export async function ensureThreeParticipants(ctx: IContext): Promise { - const p1 = new Participant('participant1'); + await joinTheModeratorAsP1(ctx); + const p2 = new Participant('participant2'); const p3 = new Participant('participant3'); - ctx.p1 = p1; ctx.p2 = p2; ctx.p3 = p3; // these need to be all, so we get the error when one fails await Promise.all([ - p1.joinConference(ctx), 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} ctx - The context. * @param {IJoinOptions} options - The options to join. * @returns {Promise} */ -export async function ensureTwoParticipants(ctx: IContext, options?: IJoinOptions): Promise { +async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) { const p1DisplayName = 'participant1'; let token; @@ -74,13 +72,25 @@ export async function ensureTwoParticipants(ctx: IContext, options?: IJoinOption ...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 { + await joinTheModeratorAsP1(ctx, options); + + const { skipInMeetingChecks } = options; await Promise.all([ _joinParticipant('participant2', ctx.p2, p => { ctx.p2 = p; }, options), - ctx.p1.waitForRemoteStreams(1), - ctx.p2.waitForRemoteStreams(1) + skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1), + skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1) ]); } @@ -143,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 { +export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise { await testee.getToolbar().clickVideoUnmuteButton(); - await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true); await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true); + await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true); } /** diff --git a/tests/pageobjects/AVModerationMenu.ts b/tests/pageobjects/AVModerationMenu.ts new file mode 100644 index 000000000000..62e91eaa7fc2 --- /dev/null +++ b/tests/pageobjects/AVModerationMenu.ts @@ -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 + }); + } +} diff --git a/tests/pageobjects/Filmstrip.ts b/tests/pageobjects/Filmstrip.ts index 04f5ab5151c2..1bd003e59158 100644 --- a/tests/pageobjects/Filmstrip.ts +++ b/tests/pageobjects/Filmstrip.ts @@ -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); + } } diff --git a/tests/pageobjects/Notifications.ts b/tests/pageobjects/Notifications.ts new file mode 100644 index 000000000000..e1ffc9ac9c18 --- /dev/null +++ b/tests/pageobjects/Notifications.ts @@ -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())); + } +} diff --git a/tests/pageobjects/ParticipantsPane.ts b/tests/pageobjects/ParticipantsPane.ts index 64bd73fe8e08..2e636ed72ea4 100644 --- a/tests/pageobjects/ParticipantsPane.ts +++ b/tests/pageobjects/ParticipantsPane.ts @@ -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(); + } } diff --git a/tests/pageobjects/Toolbar.ts b/tests/pageobjects/Toolbar.ts index 11d85051fde5..2a4b7a05f73b 100644 --- a/tests/pageobjects/Toolbar.ts +++ b/tests/pageobjects/Toolbar.ts @@ -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 { + 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(); diff --git a/tests/specs/3way/audioVideoModeration.spec.ts b/tests/specs/3way/audioVideoModeration.spec.ts new file mode 100644 index 000000000000..b5faaca27333 --- /dev/null +++ b/tests/specs/3way/audioVideoModeration.spec.ts @@ -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); +} diff --git a/tests/specs/3way/avatars.spec.ts b/tests/specs/3way/avatars.spec.ts index cfe94aaf4a14..9366f0a1f927 100644 --- a/tests/specs/3way/avatars.spec.ts +++ b/tests/specs/3way/avatars.spec.ts @@ -1,7 +1,7 @@ import { ensureThreeParticipants, ensureTwoParticipants, - unMuteVideoAndCheck + unmuteVideoAndCheck } from '../../helpers/participants'; const EMAIL = 'support@jitsi.org'; @@ -109,7 +109,7 @@ describe('Avatar - ', () => { await p1.assertThumbnailShowsAvatar(p1, false, false, true); // Unmute - now local avatar should be hidden and local video displayed - await unMuteVideoAndCheck(p1, p2); + await unmuteVideoAndCheck(p1, p2); await p1.asserLocalThumbnailShowsVideo(); diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts index e0e086d451cc..84f6772d4617 100644 --- a/tests/wdio.conf.ts +++ b/tests/wdio.conf.ts @@ -28,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` ];