This commit is contained in:
56
tests/pageobjects/AVModerationMenu.ts
Normal file
56
tests/pageobjects/AVModerationMenu.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
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 extends BasePageObject {
|
||||
/**
|
||||
* Clicks the start audio moderation menu item.
|
||||
*/
|
||||
clickStartAudioModeration() {
|
||||
return this.clickButton(START_AUDIO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the stop audio moderation menu item.
|
||||
*/
|
||||
clickStopAudioModeration() {
|
||||
return this.clickButton(STOP_AUDIO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the start video moderation menu item.
|
||||
*/
|
||||
clickStartVideoModeration() {
|
||||
return this.clickButton(START_VIDEO_MODERATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the stop audio moderation menu item.
|
||||
*/
|
||||
clickStopVideoModeration() {
|
||||
return 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
|
||||
});
|
||||
}
|
||||
}
|
||||
23
tests/pageobjects/BaseDialog.ts
Normal file
23
tests/pageobjects/BaseDialog.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
const CLOSE_BUTTON = 'modal-header-close-button';
|
||||
const OK_BUTTON = 'modal-dialog-ok-button';
|
||||
|
||||
/**
|
||||
* Base class for all dialogs.
|
||||
*/
|
||||
export default class BaseDialog extends BasePageObject {
|
||||
/**
|
||||
* Clicks on the X (close) button.
|
||||
*/
|
||||
clickCloseButton(): Promise<void> {
|
||||
return this.participant.driver.$(`#${CLOSE_BUTTON}`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the ok button.
|
||||
*/
|
||||
clickOkButton(): Promise<void> {
|
||||
return this.participant.driver.$(`#${OK_BUTTON}`).click();
|
||||
}
|
||||
}
|
||||
16
tests/pageobjects/BasePageObject.ts
Normal file
16
tests/pageobjects/BasePageObject.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
/**
|
||||
* Represents the base page object.
|
||||
* All page object has the current participant (holding the driver/browser session).
|
||||
*/
|
||||
export default class BasePageObject {
|
||||
participant: Participant;
|
||||
|
||||
/**
|
||||
* Represents the base page object.
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
}
|
||||
212
tests/pageobjects/BreakoutRooms.ts
Normal file
212
tests/pageobjects/BreakoutRooms.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
const BREAKOUT_ROOMS_CLASS = 'breakout-room-container';
|
||||
const ADD_BREAKOUT_ROOM = 'Add breakout room';
|
||||
const MORE_LABEL = 'More';
|
||||
const LEAVE_ROOM_LABEL = 'Leave breakout room';
|
||||
const AUTO_ASSIGN_LABEL = 'Auto assign to breakout rooms';
|
||||
|
||||
/**
|
||||
* Represents a single breakout room and the operations for it.
|
||||
*/
|
||||
class BreakoutRoom extends BasePageObject {
|
||||
title: string;
|
||||
id: string;
|
||||
count: number;
|
||||
|
||||
/**
|
||||
* Constructs a breakout room.
|
||||
*/
|
||||
constructor(participant: Participant, title: string, id: string) {
|
||||
super(participant);
|
||||
|
||||
this.title = title;
|
||||
this.id = id;
|
||||
|
||||
const tMatch = title.match(/.*\((.*)\)/);
|
||||
|
||||
if (tMatch) {
|
||||
this.count = parseInt(tMatch[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns room name.
|
||||
*/
|
||||
get name() {
|
||||
return this.title.split('(')[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of participants in the room.
|
||||
*/
|
||||
get participantCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses the breakout room.
|
||||
*/
|
||||
collapse() {
|
||||
const collapseElem = this.participant.driver.$(
|
||||
`div[data-testid="${this.id}"]`);
|
||||
|
||||
return collapseElem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the breakout room.
|
||||
*/
|
||||
async joinRoom() {
|
||||
const joinButton = this.participant.driver
|
||||
.$(`button[data-testid="join-room-${this.id}"]`);
|
||||
|
||||
await joinButton.waitForClickable();
|
||||
await joinButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the breakout room.
|
||||
*/
|
||||
async removeRoom() {
|
||||
await this.openContextMenu();
|
||||
|
||||
const removeButton = this.participant.driver.$(`#remove-room-${this.id}`);
|
||||
|
||||
await removeButton.waitForClickable();
|
||||
await removeButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the breakout room.
|
||||
*/
|
||||
async renameRoom(newName: string) {
|
||||
await this.openContextMenu();
|
||||
|
||||
const renameButton = this.participant.driver.$(`#rename-room-${this.id}`);
|
||||
|
||||
await renameButton.click();
|
||||
|
||||
const newNameInput = this.participant.driver.$('input[name="breakoutRoomName"]');
|
||||
|
||||
await newNameInput.waitForStable();
|
||||
await newNameInput.setValue(newName);
|
||||
|
||||
await new BaseDialog(this.participant).clickOkButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the breakout room.
|
||||
*/
|
||||
async closeRoom() {
|
||||
await this.openContextMenu();
|
||||
|
||||
const closeButton = this.participant.driver.$(`#close-room-${this.id}`);
|
||||
|
||||
await closeButton.waitForClickable();
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the context menu.
|
||||
* @private
|
||||
*/
|
||||
private async openContextMenu() {
|
||||
const listItem = this.participant.driver.$(`div[data-testid="${this.id}"]`);
|
||||
|
||||
await listItem.click();
|
||||
|
||||
const button = listItem.$(`aria/${MORE_LABEL}`);
|
||||
|
||||
await button.waitForClickable();
|
||||
await button.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All breakout rooms objects and operations.
|
||||
*/
|
||||
export default class BreakoutRooms extends BasePageObject {
|
||||
/**
|
||||
* Returns the number of breakout rooms.
|
||||
*/
|
||||
async getRoomsCount() {
|
||||
const participantsPane = this.participant.getParticipantsPane();
|
||||
|
||||
if (!await participantsPane.isOpen()) {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
return await this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a breakout room.
|
||||
*/
|
||||
async addBreakoutRoom() {
|
||||
const participantsPane = this.participant.getParticipantsPane();
|
||||
|
||||
if (!await participantsPane.isOpen()) {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
const addBreakoutButton = this.participant.driver.$(`aria/${ADD_BREAKOUT_ROOM}`);
|
||||
|
||||
await addBreakoutButton.waitForDisplayed();
|
||||
await addBreakoutButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all breakout rooms.
|
||||
*/
|
||||
async getRooms(): Promise<BreakoutRoom[]> {
|
||||
const rooms = this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`);
|
||||
|
||||
return rooms.map(async room => new BreakoutRoom(
|
||||
this.participant, await room.$('span').getText(), await room.getAttribute('data-testid')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave by clicking the leave button in participant pane.
|
||||
*/
|
||||
async leaveBreakoutRoom() {
|
||||
const participantsPane = this.participant.getParticipantsPane();
|
||||
|
||||
if (!await participantsPane.isOpen()) {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
const leaveButton = this.participant.driver.$(`aria/${LEAVE_ROOM_LABEL}`);
|
||||
|
||||
await leaveButton.isClickable();
|
||||
await leaveButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto assign participants to breakout rooms.
|
||||
*/
|
||||
async autoAssignToBreakoutRooms() {
|
||||
const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
|
||||
|
||||
await button.waitForClickable();
|
||||
await button.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to send a participant to a breakout room.
|
||||
*/
|
||||
async sendParticipantToBreakoutRoom(participant: Participant, roomName: string) {
|
||||
const participantsPane = this.participant.getParticipantsPane();
|
||||
|
||||
await participantsPane.selectParticipant(participant);
|
||||
await participantsPane.openParticipantContextMenu(participant);
|
||||
|
||||
const sendButton = this.participant.driver.$(`aria/${roomName}`);
|
||||
|
||||
await sendButton.waitForClickable();
|
||||
await sendButton.click();
|
||||
}
|
||||
}
|
||||
22
tests/pageobjects/ChatPanel.ts
Normal file
22
tests/pageobjects/ChatPanel.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
/**
|
||||
* Chat panel elements.
|
||||
*/
|
||||
export default class ChatPanel extends BasePageObject {
|
||||
/**
|
||||
* Is chat panel open.
|
||||
*/
|
||||
isOpen() {
|
||||
return this.participant.driver.$('#sideToolbarContainer').isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Presses the "chat" keyboard shortcut which opens or closes the chat
|
||||
* panel.
|
||||
*/
|
||||
async pressShortcut() {
|
||||
await this.participant.driver.$('body').click();
|
||||
await this.participant.driver.keys([ 'c' ]);
|
||||
}
|
||||
}
|
||||
276
tests/pageobjects/Filmstrip.ts
Normal file
276
tests/pageobjects/Filmstrip.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
const LOCAL_VIDEO_XPATH = '//span[@id="localVideoContainer"]';
|
||||
const LOCAL_VIDEO_MENU_TRIGGER = '#local-video-menu-trigger';
|
||||
const LOCAL_USER_CONTROLS = 'aria/Local user controls';
|
||||
const HIDE_SELF_VIEW_BUTTON_XPATH = '//div[contains(@class, "popover")]//div[@id="hideselfviewButton"]';
|
||||
|
||||
/**
|
||||
* Filmstrip elements.
|
||||
*/
|
||||
export default class Filmstrip extends BasePageObject {
|
||||
/**
|
||||
* Asserts that {@code participant} shows or doesn't show the audio
|
||||
* mute icon for the conference participant identified by
|
||||
* {@code testee}.
|
||||
*
|
||||
* @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
|
||||
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
|
||||
* otherwise, it will assert its presence.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
|
||||
let id;
|
||||
|
||||
if (testee === this.participant) {
|
||||
id = 'localVideoContainer';
|
||||
} else {
|
||||
id = `participant_${await testee.getEndpointId()}`;
|
||||
}
|
||||
|
||||
const mutedIconXPath
|
||||
= `//span[@id='${id}']//span[contains(@id, 'audioMuted')]//*[local-name()='svg' and @id='mic-disabled']`;
|
||||
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 5_000,
|
||||
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remote display name for an endpoint.
|
||||
* @param endpointId The endpoint id.
|
||||
*/
|
||||
async getRemoteDisplayName(endpointId: string) {
|
||||
const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}_name"]`);
|
||||
|
||||
await remoteDisplayName.moveTo();
|
||||
|
||||
return await remoteDisplayName.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remote video id of a participant with endpointID.
|
||||
* @param endpointId
|
||||
*/
|
||||
async getRemoteVideoId(endpointId: string) {
|
||||
const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}"]`);
|
||||
|
||||
await remoteDisplayName.moveTo();
|
||||
|
||||
return await this.participant.execute(eId =>
|
||||
document.evaluate(`//span[@id="participant_${eId}"]//video`,
|
||||
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue?.srcObject?.id, endpointId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local video id.
|
||||
*/
|
||||
getLocalVideoId() {
|
||||
return this.participant.execute(
|
||||
'return document.getElementById("localVideo_container").srcObject.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins a participant by clicking on their thumbnail.
|
||||
* @param participant The participant.
|
||||
*/
|
||||
async pinParticipant(participant: Participant) {
|
||||
let videoIdToSwitchTo;
|
||||
|
||||
if (participant === this.participant) {
|
||||
videoIdToSwitchTo = await this.getLocalVideoId();
|
||||
|
||||
// when looking up the element and clicking it, it doesn't work if we do it twice in a row (oneOnOne.spec)
|
||||
await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
|
||||
} else {
|
||||
const epId = await participant.getEndpointId();
|
||||
|
||||
videoIdToSwitchTo = await this.getRemoteVideoId(epId);
|
||||
|
||||
await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
|
||||
}
|
||||
|
||||
await this.participant.driver.waitUntil(
|
||||
async () => await this.participant.getLargeVideo().getId() === videoIdToSwitchTo,
|
||||
{
|
||||
timeout: 3_000,
|
||||
timeoutMsg: `${this.participant.name} did not switch the large video to ${
|
||||
participant.name}`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpins a participant by clicking on their thumbnail.
|
||||
* @param participant
|
||||
*/
|
||||
async unpinParticipant(participant: Participant) {
|
||||
const epId = await participant.getEndpointId();
|
||||
|
||||
if (participant === this.participant) {
|
||||
await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
|
||||
} else {
|
||||
await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
|
||||
}
|
||||
|
||||
await this.participant.driver.$(`//div[ @id="pin-indicator-${epId}" ]`).waitForDisplayed({
|
||||
timeout: 2_000,
|
||||
timeoutMsg: `${this.participant.name} did not unpin ${participant.name}`,
|
||||
reverse: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets avatar SRC attribute for the one displayed on small video thumbnail.
|
||||
* @param endpointId
|
||||
*/
|
||||
async getAvatar(endpointId: string) {
|
||||
const elem = this.participant.driver.$(
|
||||
`//span[@id='participant_${endpointId}']//img[contains(@class,'userAvatar')]`);
|
||||
|
||||
return await elem.isExisting() ? await 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) {
|
||||
await this.participant.driver.$(`//span[@id='participant_${participantId}']`).moveTo();
|
||||
|
||||
await this.participant.driver.$(
|
||||
`//span[@id='participant_${participantId
|
||||
}']//span[@id='remotevideomenu']//div[@id='remote-video-menu-trigger']`).moveTo();
|
||||
|
||||
const popoverElement = this.participant.driver.$(
|
||||
`//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
|
||||
|
||||
await popoverElement.waitForExist();
|
||||
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) {
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutelink', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the video of a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async muteVideo(participant: Participant) {
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks a participant.
|
||||
* @param participantId
|
||||
*/
|
||||
kickParticipant(participantId: string) {
|
||||
return this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over local video.
|
||||
*/
|
||||
hoverOverLocalVideo() {
|
||||
return this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the hide self view button from local video.
|
||||
*/
|
||||
async hideSelfView() {
|
||||
// open local video menu
|
||||
await this.hoverOverLocalVideo();
|
||||
await this.participant.driver.$(LOCAL_USER_CONTROLS).moveTo();
|
||||
|
||||
// click Hide self view button
|
||||
const hideSelfViewButton = this.participant.driver.$(HIDE_SELF_VIEW_BUTTON_XPATH);
|
||||
|
||||
await hideSelfViewButton.waitForExist();
|
||||
await hideSelfViewButton.waitForClickable();
|
||||
await hideSelfViewButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the local self view is displayed or not.
|
||||
*/
|
||||
assertSelfViewIsHidden(hidden: boolean) {
|
||||
return this.participant.driver.$(LOCAL_VIDEO_XPATH).waitForDisplayed({
|
||||
reverse: hidden,
|
||||
timeout: 5000,
|
||||
timeoutMsg: `Local video thumbnail is${hidden ? '' : ' not'} displayed for ${this.participant.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the filmstrip.
|
||||
*/
|
||||
async toggle() {
|
||||
const toggleButton = this.participant.driver.$('#toggleFilmstripButton');
|
||||
|
||||
await toggleButton.moveTo();
|
||||
await toggleButton.waitForDisplayed();
|
||||
await toggleButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the remote videos are hidden or not.
|
||||
* @param reverse
|
||||
*/
|
||||
assertRemoteVideosHidden(reverse = false) {
|
||||
return this.participant.driver.waitUntil(
|
||||
async () =>
|
||||
await this.participant.driver.$$('//div[@id="remoteVideos" and contains(@class, "hidden")]').length > 0,
|
||||
{
|
||||
timeout: 10_000, // 10 seconds
|
||||
timeoutMsg: `Timeout waiting fore remote videos to be hidden: ${!reverse}.`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the displayed remote video thumbnails.
|
||||
*/
|
||||
async countVisibleThumbnails() {
|
||||
return (await this.participant.driver.$$('//div[@id="remoteVideos"]//span[contains(@class,"videocontainer")]')
|
||||
.filter(thumbnail => thumbnail.isDisplayed())).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if remote videos in filmstrip are visible.
|
||||
*
|
||||
* @param isDisplayed whether or not filmstrip remote videos should be visible
|
||||
*/
|
||||
verifyRemoteVideosDisplay(isDisplayed: boolean) {
|
||||
return this.participant.driver.$('//div[contains(@class, "remote-videos")]/div').waitForDisplayed({
|
||||
timeout: 5_000,
|
||||
reverse: !isDisplayed,
|
||||
});
|
||||
}
|
||||
}
|
||||
139
tests/pageobjects/IframeAPI.ts
Normal file
139
tests/pageobjects/IframeAPI.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { LOG_PREFIX } from '../helpers/browserLogger';
|
||||
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
/**
|
||||
* The Iframe API and helpers from iframeAPITest.html
|
||||
*/
|
||||
export default class IframeAPI extends BasePageObject {
|
||||
/**
|
||||
* Returns the json object from the iframeAPI helper.
|
||||
* @param event
|
||||
*/
|
||||
getEventResult(event: string): Promise<any> {
|
||||
return this.participant.execute(
|
||||
eventName => {
|
||||
const result = window.jitsiAPI.test[eventName];
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the history of an event.
|
||||
* @param event
|
||||
*/
|
||||
clearEventResults(event: string) {
|
||||
return this.participant.execute(
|
||||
eventName => {
|
||||
window.jitsiAPI.test[eventName] = undefined;
|
||||
}, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to the iframeAPI.
|
||||
* @param eventName The event name.
|
||||
*/
|
||||
addEventListener(eventName: string) {
|
||||
return this.participant.execute(
|
||||
(event, prefix) => {
|
||||
// we want to add it once as we use static .test[event] to store the last event
|
||||
if (window.jitsiAPI.listenerCount(event) > 0) {
|
||||
return;
|
||||
}
|
||||
console.log(`${new Date().toISOString()} ${prefix}iframeAPI - Adding listener for event: ${event}`);
|
||||
window.jitsiAPI.addListener(event, evt => {
|
||||
console.log(
|
||||
`${new Date().toISOString()} ${prefix}iframeAPI - Received ${event} event: ${JSON.stringify(evt)}`);
|
||||
window.jitsiAPI.test[event] = evt;
|
||||
});
|
||||
}, eventName, LOG_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of available rooms and details of it.
|
||||
*/
|
||||
getRoomsInfo() {
|
||||
return this.participant.execute(() => window.jitsiAPI.getRoomsInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of participants in the conference.
|
||||
*/
|
||||
getNumberOfParticipants() {
|
||||
return this.participant.execute(() => window.jitsiAPI.getNumberOfParticipants());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes command using iframeAPI.
|
||||
* @param command The command.
|
||||
* @param args The arguments.
|
||||
*/
|
||||
executeCommand(command: string, ...args: any[]) {
|
||||
return this.participant.execute(
|
||||
(commandName, commandArgs) =>
|
||||
window.jitsiAPI.executeCommand(commandName, ...commandArgs)
|
||||
, command, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the participant's pane.
|
||||
*/
|
||||
isParticipantsPaneOpen() {
|
||||
return this.participant.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the embedded Jitsi Meet conference.
|
||||
*/
|
||||
dispose() {
|
||||
return this.participant.execute(() => window.jitsiAPI.dispose());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite the given participant to the meeting via PSTN.
|
||||
*/
|
||||
invitePhone(value: string) {
|
||||
return this.participant.execute(v => window.jitsiAPI.invite([ {
|
||||
type: 'phone',
|
||||
number: v
|
||||
} ]), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite the given participant to the meeting via sip (sip jibri).
|
||||
*/
|
||||
inviteSIP(value: string) {
|
||||
return this.participant.execute(v => window.jitsiAPI.invite([ {
|
||||
type: 'sip',
|
||||
address: v
|
||||
} ]), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a file recording or streaming session.
|
||||
* @param options
|
||||
*/
|
||||
startRecording(options: any) {
|
||||
return this.participant.execute(o => window.jitsiAPI.startRecording(o), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a file recording or streaming session.
|
||||
* @param mode
|
||||
*/
|
||||
stopRecording(mode: string) {
|
||||
return this.participant.execute(m => window.jitsiAPI.stopRecording(m), mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the live-streaming url.
|
||||
*/
|
||||
async getLivestreamUrl() {
|
||||
return this.participant.execute(() => window.jitsiAPI.getLivestreamUrl());
|
||||
}
|
||||
}
|
||||
102
tests/pageobjects/InviteDialog.ts
Normal file
102
tests/pageobjects/InviteDialog.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const CONFERENCE_ID = 'conference-id';
|
||||
const CONFERENCE_URL = 'invite-more-dialog-conference-url';
|
||||
const DIALOG_CONTAINER = 'invite-more-dialog';
|
||||
const MORE_NUMBERS = 'more-numbers';
|
||||
const PHONE_NUMBER = 'phone-number';
|
||||
|
||||
/**
|
||||
* Represents the invite dialog in a particular participant.
|
||||
*/
|
||||
export default class InviteDialog extends BaseDialog {
|
||||
/**
|
||||
* Checks if the dialog is open.
|
||||
*/
|
||||
isOpen() {
|
||||
return this.participant.driver.$(`.${DIALOG_CONTAINER}`).isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the invite dialog, if the info dialog is closed.
|
||||
*/
|
||||
async open() {
|
||||
if (await this.isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.participant.getParticipantsPane().clickInvite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PIN for the conference.
|
||||
*/
|
||||
async getPinNumber() {
|
||||
await this.open();
|
||||
|
||||
return (await this.getValueAfterColon(CONFERENCE_ID)).replace(/[# ]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to get values after colons. The invite dialog lists conference specific information
|
||||
* after a label, followed by a colon.
|
||||
*
|
||||
* @param className
|
||||
* @private
|
||||
*/
|
||||
private async getValueAfterColon(className: string) {
|
||||
const elem = this.participant.driver.$(`.${className}`);
|
||||
|
||||
await elem.waitForExist({ timeout: 5000 });
|
||||
|
||||
const fullText = await elem.getText();
|
||||
|
||||
this.participant.log(`Extracted text in invite dialog: ${fullText}`);
|
||||
|
||||
return fullText.split(':')[1].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the meeting url displayed in the dialog.
|
||||
*/
|
||||
async getMeetingURL() {
|
||||
const elem = this.participant.driver.$(`.${CONFERENCE_URL}`);
|
||||
|
||||
await elem.waitForExist();
|
||||
|
||||
return (await elem.getText())?.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the dialog to be open or closed.
|
||||
* @param reverse
|
||||
*/
|
||||
async waitTillOpen(reverse = false) {
|
||||
await this.participant.driver.waitUntil(
|
||||
/* eslint-disable no-extra-parens */
|
||||
async () => (reverse ? !await this.isOpen() : await this.isOpen()),
|
||||
{
|
||||
timeout: 2_000,
|
||||
timeoutMsg: `invite dialog did not ${reverse ? 'close' : 'open'}`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string that contains the dial in number for the current conference.
|
||||
*/
|
||||
getDialInNumber() {
|
||||
return this.getValueAfterColon(PHONE_NUMBER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the link to open a page to show all available dial in numbers.
|
||||
*/
|
||||
async openDialInNumbersPage() {
|
||||
const moreNumbers = this.participant.driver.$(`.${MORE_NUMBERS}`);
|
||||
|
||||
await moreNumbers.waitForExist();
|
||||
await moreNumbers.waitForClickable();
|
||||
await moreNumbers.click();
|
||||
}
|
||||
}
|
||||
80
tests/pageobjects/LargeVideo.ts
Normal file
80
tests/pageobjects/LargeVideo.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
/**
|
||||
* The large video.
|
||||
*/
|
||||
export default class LargeVideo extends BasePageObject {
|
||||
/**
|
||||
* Returns the elapsed time at which video has been playing.
|
||||
*
|
||||
* @return {number} - The current play time of the video element.
|
||||
*/
|
||||
async getPlaytime() {
|
||||
return this.participant.driver.$('#largeVideo').getProperty('currentTime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits 5s for the large video to switch to passed endpoint id.
|
||||
*
|
||||
* @param {string} endpointId - The endpoint.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
waitForSwitchTo(endpointId: string): Promise<void> {
|
||||
return this.participant.driver.waitUntil(async () => endpointId === await this.getResource(), {
|
||||
timeout: 5_000,
|
||||
timeoutMsg: `expected large video to switch to ${endpointId} for 5s`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets avatar SRC attribute for the one displayed on large video.
|
||||
*/
|
||||
async getAvatar() {
|
||||
const avatar = this.participant.driver.$('//img[@id="dominantSpeakerAvatar"]');
|
||||
|
||||
return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns resource part of the JID of the user who is currently displayed in the large video area.
|
||||
*/
|
||||
getResource() {
|
||||
return this.participant.execute(() => APP?.UI?.getLargeVideoID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source of the large video currently shown.
|
||||
*/
|
||||
getId() {
|
||||
return this.participant.execute(() => document.getElementById('largeVideo')?.srcObject?.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the large video is playing or not.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
assertPlaying() {
|
||||
let lastTime: number;
|
||||
|
||||
return this.participant.driver.waitUntil(async () => {
|
||||
const currentTime = parseFloat(await this.getPlaytime());
|
||||
|
||||
if (typeof lastTime === 'undefined') {
|
||||
lastTime = currentTime;
|
||||
}
|
||||
if (currentTime > lastTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
lastTime = currentTime;
|
||||
|
||||
return false;
|
||||
}, {
|
||||
timeout: 5_500,
|
||||
interval: 500,
|
||||
timeoutMsg:
|
||||
`Expected large video for participant ${this.participant.name} to play but it didn't for more than 5s`
|
||||
});
|
||||
}
|
||||
}
|
||||
30
tests/pageobjects/LobbyScreen.ts
Normal file
30
tests/pageobjects/LobbyScreen.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import PreMeetingScreen from './PreMeetingScreen';
|
||||
|
||||
const DISPLAY_NAME_TEST_ID = 'lobby.nameField';
|
||||
const JOIN_BUTTON_TEST_ID = 'lobby.knockButton';
|
||||
|
||||
/**
|
||||
* Page object for the Lobby screen.
|
||||
*/
|
||||
export default class LobbyScreen extends PreMeetingScreen {
|
||||
/**
|
||||
* Returns the join button element.
|
||||
*/
|
||||
getJoinButton(): ChainablePromiseElement {
|
||||
return this.participant.driver.$(`[data-testid="${JOIN_BUTTON_TEST_ID}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name input element.
|
||||
*/
|
||||
getDisplayNameInput(): ChainablePromiseElement {
|
||||
return this.participant.driver.$(`[data-testid="${DISPLAY_NAME_TEST_ID}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for lobby screen to load.
|
||||
*/
|
||||
waitForLoading(): Promise<void> {
|
||||
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 6000 });
|
||||
}
|
||||
}
|
||||
285
tests/pageobjects/Notifications.ts
Normal file
285
tests/pageobjects/Notifications.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
|
||||
const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
|
||||
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
|
||||
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
|
||||
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
|
||||
const LOBBY_ACCESS_DENIED_TEST_ID = 'lobby.joinRejectedMessage';
|
||||
const LOBBY_ENABLED_TEST_ID = 'lobby.notificationLobbyEnabled';
|
||||
const LOBBY_KNOCKING_PARTICIPANT_NOTIFICATION_XPATH
|
||||
= '//div[@data-testid="notify.participantWantsToJoin"]/div/div/span';
|
||||
const LOBBY_NOTIFICATIONS_TITLE_TEST_ID = 'lobby.notificationTitle';
|
||||
const LOBBY_PARTICIPANT_ACCESS_DENIED_TEST_ID = 'lobby.notificationLobbyAccessDenied';
|
||||
const LOBBY_PARTICIPANT_ACCESS_GRANTED_TEST_ID = 'lobby.notificationLobbyAccessGranted';
|
||||
const LOBBY_PARTICIPANT_ADMIT_TEST_ID = 'participantsPane.actions.admit';
|
||||
const LOBBY_PARTICIPANT_REJECT_TEST_ID = 'participantsPane.actions.reject';
|
||||
const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
|
||||
const REENABLE_SELF_VIEW_CLOSE_NOTIFICATION = 'notify.selfViewTitle-dismiss';
|
||||
const REENABLE_SELF_VIEW_NOTIFICATION_ID = 'notify.selfViewTitle';
|
||||
const YOU_ARE_MUTED_TEST_ID = 'notify.mutedTitle';
|
||||
|
||||
export const MAX_USERS_TEST_ID = 'dialog.maxUsersLimitReached';
|
||||
export const TOKEN_AUTH_FAILED_TEST_ID = 'dialog.tokenAuthFailed';
|
||||
export const TOKEN_AUTH_FAILED_TITLE_TEST_ID = 'dialog.tokenAuthFailedTitle';
|
||||
|
||||
/**
|
||||
* Gathers all notifications logic in the UI and obtaining those.
|
||||
*/
|
||||
export default class Notifications extends BasePageObject {
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the muted notification.
|
||||
*/
|
||||
async closeAVModerationMutedNotification(skipNonExisting = false) {
|
||||
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the ask to unmute notification.
|
||||
*/
|
||||
async closeAskToUnmuteNotification(skipNonExisting = false) {
|
||||
return this.closeNotification(ASK_TO_UNMUTE_NOTIFICATION_ID, skipNonExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses any join notifications.
|
||||
*/
|
||||
dismissAnyJoinNotification() {
|
||||
return Promise.allSettled(
|
||||
[ `${JOIN_ONE_TEST_ID}-dismiss`, `${JOIN_TWO_TEST_ID}-dismiss`, `${JOIN_MULTIPLE_TEST_ID}-dismiss` ]
|
||||
.map(id => this.participant.driver.$(`#${id}"]`).click()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the self view notification to be displayed.
|
||||
*/
|
||||
async waitForReEnableSelfViewNotification() {
|
||||
const el
|
||||
= this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_NOTIFICATION_ID}"]`);
|
||||
|
||||
await el.waitForExist({ timeout: 2000 });
|
||||
await el.waitForDisplayed();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the self view notification.
|
||||
*/
|
||||
closeReEnableSelfViewNotification() {
|
||||
return this.participant.driver.$(`div[data-testid="${REENABLE_SELF_VIEW_CLOSE_NOTIFICATION}"]`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification on participants page when Lobby is being enabled or disabled.
|
||||
*/
|
||||
getLobbyEnabledText() {
|
||||
return this.waitForNotificationText(LOBBY_ENABLED_TEST_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a specific lobby notification.
|
||||
* @param testId
|
||||
* @param skipNonExisting
|
||||
* @private
|
||||
*/
|
||||
private async closeNotification(testId: string, skipNonExisting = false) {
|
||||
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
|
||||
|
||||
if (skipNonExisting && !await notification.isExisting()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await notification.waitForExist();
|
||||
await notification.waitForStable();
|
||||
|
||||
const closeButton = notification.$('#close-notification');
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a specific lobby notification.
|
||||
* @param testId
|
||||
* @private
|
||||
*/
|
||||
private async closeLobbyNotification(testId: string) {
|
||||
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
|
||||
|
||||
await notification.waitForExist();
|
||||
await notification.waitForStable();
|
||||
|
||||
const closeButton
|
||||
= this.participant.driver.$(`[data-testid="${LOBBY_NOTIFICATIONS_TITLE_TEST_ID}"]`)
|
||||
.$('#close-notification');
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the notification.
|
||||
*/
|
||||
closeLobbyEnabled() {
|
||||
return this.closeLobbyNotification(LOBBY_ENABLED_TEST_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the knocking participant (the only one) that is displayed on the notification.
|
||||
*/
|
||||
async getKnockingParticipantName() {
|
||||
const knockingParticipantNotification
|
||||
= this.participant.driver.$('//div[@data-testid="notify.participantWantsToJoin"]/div/div/span');
|
||||
|
||||
await knockingParticipantNotification.waitForDisplayed({
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Knocking participant notification not displayed'
|
||||
});
|
||||
|
||||
return await knockingParticipantNotification.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Admits the last knocking participant (it is the only one).
|
||||
*/
|
||||
async allowLobbyParticipant() {
|
||||
const admitButton
|
||||
= this.participant.driver.$(`[data-testid="${LOBBY_PARTICIPANT_ADMIT_TEST_ID}"]`);
|
||||
|
||||
await admitButton.waitForExist();
|
||||
await admitButton.waitForClickable();
|
||||
await admitButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification that someone's access was approved.
|
||||
*/
|
||||
getLobbyParticipantAccessGranted() {
|
||||
return this.waitForNotificationText(LOBBY_PARTICIPANT_ACCESS_GRANTED_TEST_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the notification.
|
||||
*/
|
||||
closeLobbyParticipantAccessGranted() {
|
||||
return this.closeLobbyNotification(LOBBY_PARTICIPANT_ACCESS_GRANTED_TEST_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until a notifications with the given testId is found and returns its text.
|
||||
* @return the notification text.
|
||||
*/
|
||||
private async waitForNotificationText(testId: string) {
|
||||
const notificationElement = this.participant.driver.$(`[data-testid="${testId}"]`);
|
||||
|
||||
await notificationElement.waitForExist({ timeout: 2_000 });
|
||||
|
||||
return await notificationElement.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text from the notification with the given testId, if the notification exists (otherwise returns
|
||||
* undefined).
|
||||
* @param testId
|
||||
* @return the notification text.
|
||||
*/
|
||||
async getNotificationText(testId: string) {
|
||||
const notificationElement = this.participant.driver.$(`[data-testid="${testId}"]`);
|
||||
|
||||
if (await notificationElement.isExisting()) {
|
||||
return await notificationElement.getText();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the last knocking participant (it is the only one).
|
||||
*/
|
||||
async rejectLobbyParticipant() {
|
||||
const admitButton
|
||||
= this.participant.driver.$(`[data-testid="${LOBBY_PARTICIPANT_REJECT_TEST_ID}"]`);
|
||||
|
||||
await admitButton.waitForExist();
|
||||
await admitButton.waitForClickable();
|
||||
await admitButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification test that someone's access was denied.
|
||||
*/
|
||||
getLobbyParticipantAccessDenied() {
|
||||
return this.waitForNotificationText(LOBBY_PARTICIPANT_ACCESS_DENIED_TEST_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the notification.
|
||||
*/
|
||||
closeLobbyParticipantAccessDenied() {
|
||||
return this.closeLobbyNotification(LOBBY_PARTICIPANT_ACCESS_DENIED_TEST_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the notification for access denied for entering the lobby is shown.
|
||||
*/
|
||||
async waitForLobbyAccessDeniedNotification() {
|
||||
const displayNameEl
|
||||
= this.participant.driver.$(`div[data-testid="${LOBBY_ACCESS_DENIED_TEST_ID}"]`);
|
||||
|
||||
await displayNameEl.waitForExist({ timeout: 2000 });
|
||||
await displayNameEl.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait 3 seconds for the knocking participants to disappear and return true or will return false.
|
||||
* @return <tt>true</tt> if the knocking participants list was not displayed.
|
||||
*/
|
||||
waitForHideOfKnockingParticipants() {
|
||||
return this.participant.driver.$(LOBBY_KNOCKING_PARTICIPANT_NOTIFICATION_XPATH)
|
||||
.waitForDisplayed({
|
||||
timeout: 3000,
|
||||
reverse: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes local notification, for the participant that was denied.
|
||||
*/
|
||||
async closeLocalLobbyAccessDenied() {
|
||||
await this.participant.driver.$('[data-testid="lobby.joinRejectedMessage"').waitForExist();
|
||||
|
||||
const dismissButton
|
||||
= this.participant.driver.$('[data-testid="lobby.joinRejectedTitle-dismiss"]');
|
||||
|
||||
await dismissButton.moveTo();
|
||||
await dismissButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the `you are muted` notification.
|
||||
*/
|
||||
async closeYouAreMutedNotification() {
|
||||
return this.closeNotification(YOU_ARE_MUTED_TEST_ID, true);
|
||||
}
|
||||
}
|
||||
287
tests/pageobjects/ParticipantsPane.ts
Normal file
287
tests/pageobjects/ParticipantsPane.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { ChainablePromiseElement } from 'webdriverio';
|
||||
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
import AVModerationMenu from './AVModerationMenu';
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
/**
|
||||
* ID of the closed/hidden participants pane
|
||||
*/
|
||||
const PARTICIPANTS_PANE = 'participants-pane';
|
||||
|
||||
const INVITE = 'Invite someone';
|
||||
|
||||
/**
|
||||
* Represents the participants pane from the UI.
|
||||
*/
|
||||
export default class ParticipantsPane extends BasePageObject {
|
||||
/**
|
||||
* Gets the audio video moderation menu.
|
||||
*/
|
||||
getAVModerationMenu() {
|
||||
return new AVModerationMenu(this.participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pane is open.
|
||||
*/
|
||||
isOpen() {
|
||||
return this.participant.driver.$(`#${PARTICIPANTS_PANE}`).isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "participants" toolbar button to open the participants pane.
|
||||
*/
|
||||
async open() {
|
||||
await this.participant.getToolbar().clickParticipantsPaneButton();
|
||||
|
||||
const pane = this.participant.driver.$(`#${PARTICIPANTS_PANE}`);
|
||||
|
||||
await pane.waitForExist();
|
||||
await pane.waitForStable();
|
||||
await pane.waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the "participants" toolbar button to close the participants pane.
|
||||
*/
|
||||
async close() {
|
||||
await this.participant.getToolbar().clickCloseParticipantsPaneButton();
|
||||
|
||||
await this.participant.driver.$(`#${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that {@code participant} shows or doesn't show the video mute icon for the conference participant
|
||||
* identified by {@code testee}.
|
||||
*
|
||||
* @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
|
||||
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
|
||||
* otherwise, it will assert its presence.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async assertVideoMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
|
||||
const isOpen = await this.isOpen();
|
||||
|
||||
if (!isOpen) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const id = `participant-item-${await testee.getEndpointId()}`;
|
||||
const mutedIconXPath
|
||||
= `//div[@id='${id}']//div[contains(@class, 'indicators')]//*[local-name()='svg' and @id='videoMuted']`;
|
||||
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Video mute icon is${reverse ? '' : ' not'} displayed for ${testee.name} at ${
|
||||
this.participant.name} side.`
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that {@code participant} shows or doesn't show the video mute icon for the conference participant
|
||||
* identified by {@code testee}.
|
||||
*
|
||||
* @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
|
||||
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
|
||||
* otherwise, it will assert its presence.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
|
||||
const isOpen = await this.isOpen();
|
||||
|
||||
if (!isOpen) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const id = `participant-item-${await testee.getEndpointId()}`;
|
||||
const mutedIconXPath
|
||||
= `//div[@id='${id}']//div[contains(@class, 'indicators')]//*[local-name()='svg' and @id='audioMuted']`;
|
||||
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name} at ${
|
||||
this.participant.name} side.`
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
await this.selectParticipant(participantToUnmute);
|
||||
await this.openParticipantContextMenu(participantToUnmute);
|
||||
|
||||
const unmuteButton = this.participant.driver
|
||||
.$(`[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();
|
||||
|
||||
await this.selectParticipant(participantToUnmute);
|
||||
if (fromContextMenu) {
|
||||
await this.openParticipantContextMenu(participantToUnmute);
|
||||
}
|
||||
|
||||
const unmuteButton = this.participant.driver
|
||||
.$(`[data-testid="unmute-audio-${participantId}"]`);
|
||||
|
||||
await unmuteButton.waitForExist();
|
||||
await unmuteButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open context menu for given participant.
|
||||
*/
|
||||
async selectParticipant(participant: Participant) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
|
||||
|
||||
await participantItem.waitForExist();
|
||||
await participantItem.waitForStable();
|
||||
await participantItem.waitForDisplayed();
|
||||
await participantItem.moveTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open context menu for given participant.
|
||||
*/
|
||||
async openParticipantContextMenu(participant: Participant) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the invite button.
|
||||
*/
|
||||
async clickInvite() {
|
||||
if (!await this.isOpen()) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
const inviteButton = this.participant.driver.$(`aria/${INVITE}`);
|
||||
|
||||
await inviteButton.waitForDisplayed();
|
||||
await inviteButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the participant by name.
|
||||
* @param name - The name to look for.
|
||||
* @private
|
||||
*/
|
||||
private async findLobbyParticipantByName(name: string): Promise<ChainablePromiseElement> {
|
||||
return this.participant.driver.$$('//div[@id="lobby-list"]//div[starts-with(@id, "participant-item-")]')
|
||||
.find(async participant => (await participant.getText()).includes(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to click on the approve button and fails if it cannot be clicked.
|
||||
* @param participantNameToAdmit - the name of the participant to admit.
|
||||
*/
|
||||
async admitLobbyParticipant(participantNameToAdmit: string) {
|
||||
const participantToAdmit = await this.findLobbyParticipantByName(participantNameToAdmit);
|
||||
|
||||
await participantToAdmit.moveTo();
|
||||
|
||||
const participantIdToAdmit = (await participantToAdmit.getAttribute('id'))
|
||||
.substring('participant-item-'.length);
|
||||
const admitButton = this.participant.driver
|
||||
.$(`[data-testid="admit-${participantIdToAdmit}"]`);
|
||||
|
||||
await admitButton.waitForExist();
|
||||
await admitButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to click on the reject button and fails if it cannot be clicked.
|
||||
* @param participantNameToReject - the name of the participant for this {@link ParticipantsPane} to reject.
|
||||
*/
|
||||
async rejectLobbyParticipant(participantNameToReject: string) {
|
||||
const participantToReject
|
||||
= await this.findLobbyParticipantByName(participantNameToReject);
|
||||
|
||||
await participantToReject.moveTo();
|
||||
|
||||
const participantIdToReject = (await participantToReject.getAttribute('id'))
|
||||
.substring('participant-item-'.length);
|
||||
|
||||
const moreOptionsButton
|
||||
= this.participant.driver.$(`aria/More moderation options ${participantNameToReject}`);
|
||||
|
||||
await moreOptionsButton.click();
|
||||
|
||||
const rejectButton = this.participant.driver
|
||||
.$(`[data-testid="reject-${participantIdToReject}"]`);
|
||||
|
||||
await rejectButton.waitForExist();
|
||||
await rejectButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
50
tests/pageobjects/PasswordDialog.ts
Normal file
50
tests/pageobjects/PasswordDialog.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const INPUT_KEY_XPATH = '//input[@name="lockKey"]';
|
||||
|
||||
/**
|
||||
* Represents the password dialog in a particular participant.
|
||||
*/
|
||||
export default class PasswordDialog extends BaseDialog {
|
||||
/**
|
||||
* Waiting for the dialog to appear.
|
||||
*/
|
||||
async waitForDialog() {
|
||||
const input = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await input.waitForExist({
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Password dialog not found'
|
||||
});
|
||||
await input.waitForDisplayed();
|
||||
await input.waitForStable();
|
||||
}
|
||||
|
||||
async isOpen() {
|
||||
const input = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
try {
|
||||
await input.isExisting();
|
||||
|
||||
return await input.isDisplayed();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a password and submits the dialog.
|
||||
* @param password
|
||||
*/
|
||||
async submitPassword(password: string) {
|
||||
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await passwordInput.waitForExist();
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
await this.participant.driver.keys(password);
|
||||
|
||||
await this.clickOkButton();
|
||||
}
|
||||
}
|
||||
55
tests/pageobjects/PreJoinScreen.ts
Normal file
55
tests/pageobjects/PreJoinScreen.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import PreMeetingScreen from './PreMeetingScreen';
|
||||
|
||||
const DISPLAY_NAME_ID = 'premeeting-name-input';
|
||||
const ERROR_ON_JOIN = 'prejoin.errorMessage';
|
||||
const JOIN_BUTTON_TEST_ID = 'prejoin.joinMeeting';
|
||||
const JOIN_WITHOUT_AUDIO = 'prejoin.joinWithoutAudio';
|
||||
const OPTIONS_BUTTON = 'prejoin.joinOptions';
|
||||
|
||||
/**
|
||||
* Page object for the PreJoin screen.
|
||||
*/
|
||||
export default class PreJoinScreen extends PreMeetingScreen {
|
||||
/**
|
||||
* Returns the join button element.
|
||||
*/
|
||||
getJoinButton(): ChainablePromiseElement {
|
||||
return this.participant.driver.$(`[data-testid="${JOIN_BUTTON_TEST_ID}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name input element.
|
||||
*/
|
||||
getDisplayNameInput(): ChainablePromiseElement {
|
||||
return this.participant.driver.$(`#${DISPLAY_NAME_ID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for pre join screen to load.
|
||||
*/
|
||||
waitForLoading(): Promise<void> {
|
||||
return this.participant.driver.$('[data-testid="prejoin.screen"]')
|
||||
.waitForDisplayed({ timeout: 3000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message displayed on the prejoin screen.
|
||||
*/
|
||||
getErrorOnJoin() {
|
||||
return this.participant.driver.$(`[data-testid="${ERROR_ON_JOIN}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the join without audio button element.
|
||||
*/
|
||||
getJoinWithoutAudioButton() {
|
||||
return this.participant.driver.$(`[data-testid="${JOIN_WITHOUT_AUDIO}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the join options button element.
|
||||
*/
|
||||
getJoinOptions() {
|
||||
return this.participant.driver.$(`[data-testid="${OPTIONS_BUTTON}"]`);
|
||||
}
|
||||
}
|
||||
90
tests/pageobjects/PreMeetingScreen.ts
Normal file
90
tests/pageobjects/PreMeetingScreen.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
const PASSWORD_BUTTON_TEST_ID = 'lobby.enterPasswordButton';
|
||||
|
||||
/**
|
||||
* Page object for the PreMeeting screen, common stuff between pre-join and lobby screens.
|
||||
*/
|
||||
export default abstract class PreMeetingScreen extends BasePageObject {
|
||||
/**
|
||||
* Waits for pre join or lobby screen to load.
|
||||
*/
|
||||
abstract waitForLoading(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns the display name input element.
|
||||
*/
|
||||
abstract getDisplayNameInput(): WebdriverIO.Element;
|
||||
|
||||
/**
|
||||
* Returns the join button element.
|
||||
*/
|
||||
abstract getJoinButton(): WebdriverIO.Element;
|
||||
|
||||
/**
|
||||
* Interacts with the view to enter a display name.
|
||||
*/
|
||||
async enterDisplayName(displayName: string) {
|
||||
const displayNameInput = this.getDisplayNameInput();
|
||||
|
||||
await displayNameInput.click();
|
||||
|
||||
// element.clear does not always work, make sure we delete the content
|
||||
await displayNameInput.clearValue();
|
||||
|
||||
await this.participant.driver.keys(displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks internally whether lobby room is joined.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
waitToJoinLobby(): Promise<void> {
|
||||
return this.participant.driver.waitUntil(
|
||||
() => this.isLobbyRoomJoined(),
|
||||
{
|
||||
timeout: 6_000, // 6 seconds
|
||||
timeoutMsg: `Timeout waiting to join lobby for ${this.participant.name}`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks internally whether lobby room is joined.
|
||||
*/
|
||||
isLobbyRoomJoined() {
|
||||
return this.participant.execute(
|
||||
() => APP?.conference?._room?.room?.getLobby()?.lobbyRoom?.joined === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password button element.
|
||||
*/
|
||||
getPasswordButton() {
|
||||
return this.participant.driver.$(`[data-testid="${PASSWORD_BUTTON_TEST_ID}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interacts with the view to enter a password.
|
||||
*/
|
||||
async enterPassword(password: string) {
|
||||
const passwordButton = this.getPasswordButton();
|
||||
|
||||
await passwordButton.moveTo();
|
||||
await passwordButton.click();
|
||||
|
||||
const passwordInput = this.participant.driver.$('[data-testid="lobby.password"]');
|
||||
|
||||
await passwordInput.waitForDisplayed();
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
await this.participant.driver.keys(password);
|
||||
|
||||
const joinButton = this.participant.driver.$('[data-testid="lobby.passwordJoinButton"]');
|
||||
|
||||
await joinButton.waitForDisplayed();
|
||||
await joinButton.click();
|
||||
}
|
||||
}
|
||||
134
tests/pageobjects/SecurityDialog.ts
Normal file
134
tests/pageobjects/SecurityDialog.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const ADD_PASSWORD_LINK = 'add-password';
|
||||
const ADD_PASSWORD_FIELD = 'info-password-input';
|
||||
const DIALOG_CONTAINER = 'security-dialog';
|
||||
const LOCAL_LOCK = 'info-password-local';
|
||||
const REMOTE_LOCK = 'info-password-remote';
|
||||
const REMOVE_PASSWORD = 'remove-password';
|
||||
|
||||
/**
|
||||
* Page object for the security dialog.
|
||||
*/
|
||||
export default class SecurityDialog extends BaseDialog {
|
||||
/**
|
||||
* Waits for the settings dialog to be visible.
|
||||
*/
|
||||
waitForDisplay() {
|
||||
return this.participant.driver.$(`.${DIALOG_CONTAINER}`).waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the switch that can be used to detect lobby state or change lobby state.
|
||||
* @private
|
||||
*/
|
||||
private getLobbySwitch() {
|
||||
return this.participant.driver.$('#lobby-section-switch');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns is the lobby enabled.
|
||||
*/
|
||||
isLobbyEnabled() {
|
||||
return this.getLobbySwitch().isSelected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the lobby option from the security dialog.
|
||||
*/
|
||||
async toggleLobby() {
|
||||
const lobbySwitch = this.getLobbySwitch();
|
||||
|
||||
await lobbySwitch.moveTo();
|
||||
await lobbySwitch.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether lobby section is present in the UI.
|
||||
*/
|
||||
isLobbySectionPresent() {
|
||||
return this.getLobbySwitch().isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the lobby to be enabled or disabled.
|
||||
* @param reverse
|
||||
*/
|
||||
waitForLobbyEnabled(reverse = false) {
|
||||
const lobbySwitch = this.getLobbySwitch();
|
||||
|
||||
return this.participant.driver.waitUntil(
|
||||
async () => await lobbySwitch.isSelected() !== reverse,
|
||||
{
|
||||
timeout: 5_000, // 30 seconds
|
||||
timeoutMsg: `Timeout waiting for lobby being ${reverse ? 'disabled' : 'enabled'} for ${
|
||||
this.participant.name}.`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current conference is locked with a locally set password.
|
||||
*
|
||||
* @return {@code true} if the conference is displayed as locked locally in
|
||||
* the security dialog, {@code false} otherwise.
|
||||
*/
|
||||
private isLockedLocally() {
|
||||
return this.participant.driver.$(`.${LOCAL_LOCK}`).isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current conference is locked with a locally set password.
|
||||
*
|
||||
* @return {@code true} if the conference is displayed as locked remotely
|
||||
* in the security dialog, {@code false} otherwise.
|
||||
*/
|
||||
private isLockedRemotely() {
|
||||
return this.participant.driver.$(`.${REMOTE_LOCK}`).isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current conference is locked based on the security dialog's
|
||||
* display state.
|
||||
*
|
||||
* @return {@code true} if the conference is displayed as locked in the
|
||||
* security dialog, {@code false} otherwise.
|
||||
*/
|
||||
async isLocked() {
|
||||
return await this.isLockedLocally() || await this.isLockedRemotely();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a password on the current conference to lock it.
|
||||
*
|
||||
* @param password - The password to use to lock the conference.
|
||||
*/
|
||||
async addPassword(password: string) {
|
||||
const addPasswordLink = this.participant.driver.$(`.${ADD_PASSWORD_LINK}`);
|
||||
|
||||
await addPasswordLink.waitForClickable();
|
||||
await addPasswordLink.click();
|
||||
|
||||
const passwordEntry = this.participant.driver.$(`#${ADD_PASSWORD_FIELD}`);
|
||||
|
||||
await passwordEntry.waitForDisplayed();
|
||||
await passwordEntry.click();
|
||||
|
||||
await this.participant.driver.keys(password);
|
||||
await this.participant.driver.$('button=Add').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the password from the current conference through the security dialog, if a password is set.
|
||||
*/
|
||||
async removePassword() {
|
||||
if (!await this.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removePassword = this.participant.driver.$(`.${REMOVE_PASSWORD}`);
|
||||
|
||||
await removePassword.waitForClickable();
|
||||
await removePassword.click();
|
||||
}
|
||||
}
|
||||
156
tests/pageobjects/SettingsDialog.ts
Normal file
156
tests/pageobjects/SettingsDialog.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const EMAIL_FIELD = '#setEmail';
|
||||
const FOLLOW_ME_CHECKBOX = '//input[@name="follow-me"]';
|
||||
const HIDE_SELF_VIEW_CHECKBOX = '//input[@name="hide-self-view"]';
|
||||
const SETTINGS_DIALOG_CONTENT = '.settings-pane';
|
||||
const START_AUDIO_MUTED_CHECKBOX = '//input[@name="start-audio-muted"]';
|
||||
const START_VIDEO_MUTED_CHECKBOX = '//input[@name="start-video-muted"]';
|
||||
const X_PATH_MODERATOR_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Moderator"]';
|
||||
const X_PATH_MORE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="General"]';
|
||||
const X_PATH_PROFILE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Profile"]';
|
||||
|
||||
/**
|
||||
* The settings dialog.
|
||||
*/
|
||||
export default class SettingsDialog extends BaseDialog {
|
||||
/**
|
||||
* Waits for the settings dialog to be visible.
|
||||
*/
|
||||
waitForDisplay() {
|
||||
return this.participant.driver.$(SETTINGS_DIALOG_CONTENT).waitForDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a specific tab in the settings dialog.
|
||||
* @param xpath
|
||||
* @private
|
||||
*/
|
||||
private async openTab(xpath: string) {
|
||||
const elem = this.participant.driver.$(xpath);
|
||||
|
||||
await elem.waitForClickable();
|
||||
await elem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the Profile tab to be displayed.
|
||||
*/
|
||||
openProfileTab() {
|
||||
return this.openTab(X_PATH_PROFILE_TAB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the More tab to be displayed.
|
||||
*/
|
||||
openMoreTab() {
|
||||
return this.openTab(X_PATH_MORE_TAB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the moderator tab to be displayed.
|
||||
*/
|
||||
openModeratorTab() {
|
||||
return this.openTab(X_PATH_MODERATOR_TAB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters the passed in email into the email field.
|
||||
* @param email
|
||||
*/
|
||||
async setEmail(email: string) {
|
||||
await this.openProfileTab();
|
||||
|
||||
await this.participant.driver.$(EMAIL_FIELD).setValue(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the participant's email displayed in the settings dialog.
|
||||
*/
|
||||
async getEmail() {
|
||||
await this.openProfileTab();
|
||||
|
||||
return await this.participant.driver.$(EMAIL_FIELD).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the OK button on the settings dialog to close the dialog and save any changes made.
|
||||
*/
|
||||
submit() {
|
||||
return this.clickOkButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the start audio muted feature to enabled/disabled.
|
||||
* @param {boolean} enable - true for enabled and false for disabled.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setStartAudioMuted(enable: boolean) {
|
||||
await this.openModeratorTab();
|
||||
|
||||
await this.setCheckbox(START_AUDIO_MUTED_CHECKBOX, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the start video muted feature to enabled/disabled.
|
||||
* @param {boolean} enable - true for enabled and false for disabled.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setStartVideoMuted(enable: boolean) {
|
||||
await this.openModeratorTab();
|
||||
|
||||
await this.setCheckbox(START_VIDEO_MUTED_CHECKBOX, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state checked/selected of a checkbox in the settings dialog.
|
||||
*/
|
||||
async setHideSelfView(hideSelfView: boolean) {
|
||||
await this.openMoreTab();
|
||||
|
||||
await this.setCheckbox(HIDE_SELF_VIEW_CHECKBOX, hideSelfView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the follow me feature to enabled/disabled.
|
||||
* @param enable
|
||||
*/
|
||||
async setFollowMe(enable: boolean) {
|
||||
await this.openModeratorTab();
|
||||
|
||||
await this.setCheckbox(FOLLOW_ME_CHECKBOX, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the follow me checkbox is displayed in the settings dialog.
|
||||
*/
|
||||
async isFollowMeDisplayed() {
|
||||
const elem = this.participant.driver.$(X_PATH_MODERATOR_TAB);
|
||||
|
||||
if (!await elem.isExisting()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.openModeratorTab();
|
||||
|
||||
return await this.participant.driver.$$(FOLLOW_ME_CHECKBOX).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of a checkbox.
|
||||
* @param selector
|
||||
* @param enable
|
||||
* @private
|
||||
*/
|
||||
private async setCheckbox(selector: string, enable: boolean) {
|
||||
const checkbox = this.participant.driver.$(selector);
|
||||
|
||||
await checkbox.waitForExist();
|
||||
|
||||
if (enable !== await checkbox.isSelected()) {
|
||||
// we show a div with svg and text after the input and those elements grab the click
|
||||
// so we need to click on the parent element
|
||||
await this.participant.driver.$(`${selector}//ancestor::div[1]`).click();
|
||||
}
|
||||
}
|
||||
}
|
||||
313
tests/pageobjects/Toolbar.ts
Normal file
313
tests/pageobjects/Toolbar.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
const AUDIO_MUTE = 'Mute microphone';
|
||||
const AUDIO_UNMUTE = 'Unmute microphone';
|
||||
const CHAT = 'Open chat';
|
||||
const CLOSE_CHAT = 'Close chat';
|
||||
const CLOSE_PARTICIPANTS_PANE = 'Close participants pane';
|
||||
const DESKTOP = 'Start sharing your screen';
|
||||
const HANGUP = 'Leave the meeting';
|
||||
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 SECURITY = 'Security options';
|
||||
const SETTINGS = 'Open settings';
|
||||
const STOP_DESKTOP = 'Stop sharing your screen';
|
||||
const ENTER_TILE_VIEW_BUTTON = 'Enter tile view';
|
||||
const EXIT_TILE_VIEW_BUTTON = 'Exit tile view';
|
||||
const VIDEO_QUALITY = 'Manage video quality';
|
||||
const VIDEO_MUTE = 'Stop camera';
|
||||
const VIDEO_UNMUTE = 'Start camera';
|
||||
|
||||
/**
|
||||
* The toolbar elements.
|
||||
*/
|
||||
export default class Toolbar extends BasePageObject {
|
||||
/**
|
||||
* Returns the button.
|
||||
*
|
||||
* @param {string} accessibilityCSSSelector - The selector to find the button.
|
||||
* @returns {WebdriverIO.Element} The button.
|
||||
* @private
|
||||
*/
|
||||
private getButton(accessibilityCSSSelector: string) {
|
||||
return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio mute button.
|
||||
*/
|
||||
get audioMuteBtn() {
|
||||
return this.getButton(AUDIO_MUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio unmute button.
|
||||
*/
|
||||
get audioUnMuteBtn() {
|
||||
return this.getButton(AUDIO_UNMUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks audio mute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickAudioMuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Audio Mute Button');
|
||||
|
||||
return this.audioMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks audio unmute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickAudioUnmuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Audio Unmute Button');
|
||||
|
||||
return this.audioUnMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The video mute button.
|
||||
*/
|
||||
get videoMuteBtn() {
|
||||
return this.getButton(VIDEO_MUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The video unmute button.
|
||||
*/
|
||||
get videoUnMuteBtn() {
|
||||
return this.getButton(VIDEO_UNMUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks video mute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickVideoMuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Video Mute Button');
|
||||
|
||||
return this.videoMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks video unmute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickVideoUnmuteButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Video Unmute Button');
|
||||
|
||||
return this.videoUnMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks Participants pane button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickCloseParticipantsPaneButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Close Participants pane Button');
|
||||
|
||||
return this.getButton(CLOSE_PARTICIPANTS_PANE).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks Participants pane button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clickParticipantsPaneButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Participants pane Button');
|
||||
|
||||
// Special case for participants pane button, as it contains the number of participants and its label
|
||||
// is changing
|
||||
return this.participant.driver.$(`[aria-label^="${PARTICIPANTS}"]`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the video quality toolbar button which opens the
|
||||
* dialog for adjusting max-received video quality.
|
||||
*/
|
||||
clickVideoQualityButton(): Promise<void> {
|
||||
return this.clickButtonInOverflowMenu(VIDEO_QUALITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the profile toolbar button which opens or closes the profile panel.
|
||||
*/
|
||||
clickProfileButton(): Promise<void> {
|
||||
return this.clickButtonInOverflowMenu(PROFILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the raise hand button that enables participants will to speak.
|
||||
*/
|
||||
clickRaiseHandButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Raise hand Button');
|
||||
|
||||
return this.getButton(RAISE_HAND).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the chat button that opens chat panel.
|
||||
*/
|
||||
clickChatButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Chat Button');
|
||||
|
||||
return this.getButton(CHAT).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the chat button that closes chat panel.
|
||||
*/
|
||||
clickCloseChatButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Close Chat Button');
|
||||
|
||||
return this.getButton(CLOSE_CHAT).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the desktop sharing button that starts desktop sharing.
|
||||
*/
|
||||
clickDesktopSharingButton() {
|
||||
return this.getButton(DESKTOP).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the desktop sharing button to stop it.
|
||||
*/
|
||||
clickStopDesktopSharingButton() {
|
||||
return this.getButton(STOP_DESKTOP).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the tile view button which enables tile layout.
|
||||
*/
|
||||
clickEnterTileViewButton() {
|
||||
return this.getButton(ENTER_TILE_VIEW_BUTTON).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the tile view button which exits tile layout.
|
||||
*/
|
||||
clickExitTileViewButton() {
|
||||
return this.getButton(EXIT_TILE_VIEW_BUTTON).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the hangup button that ends the conference.
|
||||
*/
|
||||
clickHangupButton(): Promise<void> {
|
||||
this.participant.log('Clicking on: Hangup Button');
|
||||
|
||||
return this.getButton(HANGUP).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the security toolbar button which opens the security panel.
|
||||
*/
|
||||
clickSecurityButton() {
|
||||
return this.clickButtonInOverflowMenu(SECURITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the settings toolbar button which opens or closes the settings panel.
|
||||
*/
|
||||
clickSettingsButton() {
|
||||
return this.clickButtonInOverflowMenu(SETTINGS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the overflow menu is open and clicks on a specified button.
|
||||
* @param accessibilityLabel The accessibility label of the button to be clicked.
|
||||
* @private
|
||||
*/
|
||||
private async clickButtonInOverflowMenu(accessibilityLabel: string) {
|
||||
await this.openOverflowMenu();
|
||||
|
||||
// sometimes the overflow button tooltip is over the last entry in the menu,
|
||||
// so let's move focus away before clicking the button
|
||||
await this.participant.driver.$('#overflow-context-menu').moveTo();
|
||||
|
||||
this.participant.log(`Clicking on: ${accessibilityLabel}`);
|
||||
await this.getButton(accessibilityLabel).click();
|
||||
|
||||
await this.closeOverflowMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the overflow menu is open and visible.
|
||||
* @private
|
||||
*/
|
||||
private async isOverflowMenuOpen() {
|
||||
return await this.participant.driver.$$(`aria/${OVERFLOW_MENU}`).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the overflow toolbar button which opens or closes the overflow menu.
|
||||
* @private
|
||||
*/
|
||||
private clickOverflowButton(): Promise<void> {
|
||||
return this.getButton(OVERFLOW).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the overflow menu is displayed.
|
||||
* @private
|
||||
*/
|
||||
private async openOverflowMenu() {
|
||||
if (await this.isOverflowMenuOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.clickOverflowButton();
|
||||
|
||||
await this.waitForOverFlowMenu(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the overflow menu is not displayed.
|
||||
* @private
|
||||
*/
|
||||
async closeOverflowMenu() {
|
||||
if (!await this.isOverflowMenuOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.clickOverflowButton();
|
||||
|
||||
await this.waitForOverFlowMenu(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the overflow menu to be visible or hidden.
|
||||
* @param visible
|
||||
* @private
|
||||
*/
|
||||
private waitForOverFlowMenu(visible: boolean) {
|
||||
return this.getButton(OVERFLOW_MENU).waitForDisplayed({
|
||||
reverse: !visible,
|
||||
timeout: 3000,
|
||||
timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the participant's avatar image element located in the toolbar.
|
||||
*/
|
||||
async getProfileImage() {
|
||||
await this.openOverflowMenu();
|
||||
|
||||
const elem = this.participant.driver.$(`[aria-label^="${PROFILE}"] img`);
|
||||
|
||||
return await elem.isExisting() ? await elem.getAttribute('src') : null;
|
||||
}
|
||||
}
|
||||
42
tests/pageobjects/VideoQualityDialog.ts
Normal file
42
tests/pageobjects/VideoQualityDialog.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Key } from 'webdriverio';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const VIDEO_QUALITY_SLIDER_CLASS = 'custom-slider';
|
||||
|
||||
/**
|
||||
* The video quality dialog.
|
||||
*/
|
||||
export default class VideoQualityDialog extends BaseDialog {
|
||||
/**
|
||||
* Opens the video quality dialog and sets the video quality to the minimum or maximum definition.
|
||||
* @param audioOnly - Whether to set the video quality to audio only (minimum).
|
||||
* @private
|
||||
*/
|
||||
async setVideoQuality(audioOnly: boolean) {
|
||||
await this.participant.getToolbar().clickVideoQualityButton();
|
||||
|
||||
const videoQualitySlider = this.participant.driver.$(`.${VIDEO_QUALITY_SLIDER_CLASS}`);
|
||||
|
||||
const audioOnlySliderValue = parseInt(await videoQualitySlider.getAttribute('min'), 10);
|
||||
|
||||
const maxDefinitionSliderValue = parseInt(await videoQualitySlider.getAttribute('max'), 10);
|
||||
const activeValue = parseInt(await videoQualitySlider.getAttribute('value'), 10);
|
||||
|
||||
const targetValue = audioOnly ? audioOnlySliderValue : maxDefinitionSliderValue;
|
||||
const distanceToTargetValue = targetValue - activeValue;
|
||||
const keyDirection = distanceToTargetValue > 0 ? Key.ArrowRight : Key.ArrowLeft;
|
||||
|
||||
// we need to click the element to activate it so it will receive the keys
|
||||
await videoQualitySlider.click();
|
||||
|
||||
// Move the slider to the target value.
|
||||
for (let i = 0; i < Math.abs(distanceToTargetValue); i++) {
|
||||
|
||||
await this.participant.driver.keys(keyDirection);
|
||||
}
|
||||
|
||||
// Close the video quality dialog.
|
||||
await this.clickCloseButton();
|
||||
}
|
||||
}
|
||||
63
tests/pageobjects/Visitors.ts
Normal file
63
tests/pageobjects/Visitors.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import BasePageObject from './BasePageObject';
|
||||
|
||||
/**
|
||||
* Page object for the visitors elements in moderator and visitor page.
|
||||
*/
|
||||
export default class Visitors extends BasePageObject {
|
||||
/**
|
||||
* Returns the visitors dialog element if any.
|
||||
*/
|
||||
hasVisitorsDialog() {
|
||||
return this.participant.driver.$('aria/Joining meeting');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visitors count shown in the conference info (subject) area.
|
||||
*/
|
||||
getVisitorsCount() {
|
||||
return this.participant.driver.$('#visitorsCountLabel').getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visitors count shown in the Participants pane.
|
||||
*/
|
||||
async getVisitorsHeaderFromParticipantsPane() {
|
||||
const participantsPane = this.participant.getParticipantsPane();
|
||||
const isOpen = await participantsPane.isOpen();
|
||||
|
||||
if (!isOpen) {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
return this.participant.driver.$('#visitor-list-header').getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the visitors queue UI is shown.
|
||||
*/
|
||||
isVisitorsQueueUIShown() {
|
||||
return this.participant.driver.$('#visitors-waiting-queue').isDisplayed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the knocking participant (the only one) that is displayed on the notification.
|
||||
*/
|
||||
async getWaitingVisitorsInQueue() {
|
||||
const goLiveNotification
|
||||
= this.participant.driver.$('//div[@data-testid="notify.waitingVisitors"]');
|
||||
|
||||
await goLiveNotification.waitForDisplayed({
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Go live notification not displayed'
|
||||
});
|
||||
|
||||
return await goLiveNotification.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the go live button in the visitors notification.
|
||||
*/
|
||||
async goLive() {
|
||||
return this.participant.driver.$('//button[@data-testid="participantsPane.actions.goLive"]').click();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user