feat(tests): Adds follow-me and invite dialog test. (#15476)

* feat(tests): Adds follow-me test.

* feat(tests): Adds invite dialog test.

* squash: fix lint.
This commit is contained in:
Дамян Минков
2025-01-10 16:47:25 -06:00
committed by GitHub
parent 1edf88e744
commit c21f84c15a
11 changed files with 377 additions and 19 deletions

View File

@@ -30,3 +30,6 @@
#WEBHOOKS_PROXY_URL=
# A shared secret to authenticate the webhook proxy connection
#WEBHOOKS_PROXY_SHARED_SECRET=
# A rest URL to be used by dial-in tests to invite jigasi to the conference
#DIAL_IN_REST_URL=

View File

@@ -238,11 +238,22 @@ export class Participant {
async () => await this.driver.execute(() => document.readyState === 'complete'),
{
timeout: 30_000, // 30 seconds
timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
}
);
}
/**
* Waits for the tile view to display.
*/
async waitForTileViewDisplay(reverse = false) {
await this.driver.$('//div[@id="videoconference_page" and contains(@class, "tile-view")]').waitForDisplayed({
reverse,
timeout: 10_000,
timeoutMsg: `Tile view did not display in 10s for ${this.name}`
});
}
/**
* Checks if the participant is in the meeting.
*/
@@ -284,7 +295,7 @@ export class Participant {
() => this.isInMuc(),
{
timeout: 10_000, // 10 seconds
timeoutMsg: 'Timeout waiting to join muc.'
timeoutMsg: `Timeout waiting to join muc for ${this.name}`
}
);
}
@@ -300,7 +311,7 @@ export class Participant {
return driver.waitUntil(async () =>
await driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: 'expected ICE to be connected for 15s'
timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
});
}
@@ -309,7 +320,8 @@ export class Participant {
*
* @returns {Promise<void>}
*/
async waitForSendReceiveData(timeout = 15_000, msg = 'expected to receive/send data in 15s'): Promise<void> {
async waitForSendReceiveData(
timeout = 15_000, msg = `expected to receive/send data in 15s for ${this.name}`): Promise<void> {
const driver = this.driver;
return driver.waitUntil(async () =>
@@ -340,7 +352,7 @@ export class Participant {
return driver.waitUntil(async () =>
await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
timeout: 15_000,
timeoutMsg: 'expected remote streams in 15s'
timeoutMsg: `expected remote streams in 15s for ${this.name}`
});
}
@@ -357,7 +369,7 @@ export class Participant {
return driver.waitUntil(async () =>
await driver.execute(count => APP.conference.listMembers().length === count, number), {
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s`
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
});
}
@@ -531,6 +543,13 @@ export class Participant {
return await this.driver.execute(() => APP.UI.getLargeVideoID());
}
/**
* Returns the source of the large video currently shown.
*/
async getLargeVideoId() {
return await this.driver.execute('return document.getElementById("largeVideo").srcObject.id');
}
/**
* Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
* There are 3 options for avatar:

View File

@@ -53,6 +53,28 @@ export default class Filmstrip extends BasePageObject {
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.driver.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.
*/
async getLocalVideoId() {
return await this.participant.driver.execute(
'return document.getElementById("localVideo_container").srcObject.id');
}
/**
* Pins a participant by clicking on their thumbnail.
* @param participant The participant.
@@ -162,4 +184,30 @@ export default class Filmstrip extends BasePageObject {
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
*/
async assertRemoteVideosHidden(reverse = false) {
await 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}.`
}
);
}
}

View File

@@ -1,7 +1,10 @@
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.
@@ -31,7 +34,18 @@ export default class InviteDialog extends BaseDialog {
async getPinNumber() {
await this.open();
const elem = this.participant.driver.$(`.${CONFERENCE_ID}`);
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 });
@@ -39,6 +53,50 @@ export default class InviteDialog extends BaseDialog {
this.participant.log(`Extracted text in invite dialog: ${fullText}`);
return fullText.split(':')[1].trim().replace(/[# ]/g, '');
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.
*/
async getDialInNumber() {
return await 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();
}
}

View File

@@ -1,8 +1,10 @@
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 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"]';
@@ -37,12 +39,19 @@ export default class SettingsDialog extends BaseDialog {
}
/**
* Selects the Profile tab to be displayed.
* Selects the More tab to be displayed.
*/
async openMoreTab() {
await this.openTab(X_PATH_MORE_TAB);
}
/**
* Selects the moderator tab to be displayed.
*/
async openModeratorTab() {
await this.openTab(X_PATH_MODERATOR_TAB);
}
/**
* Enters the passed in email into the email field.
* @param email
@@ -75,14 +84,49 @@ export default class SettingsDialog extends BaseDialog {
async setHideSelfView(hideSelfView: boolean) {
await this.openMoreTab();
const checkbox = this.participant.driver.$(HIDE_SELF_VIEW_CHECKBOX);
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 (hideSelfView !== await checkbox.isSelected()) {
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.$(`${HIDE_SELF_VIEW_CHECKBOX}//ancestor::div[1]`).click();
await this.participant.driver.$(`${selector}//ancestor::div[1]`).click();
}
}
}

View File

@@ -113,7 +113,6 @@ export default class Toolbar extends BasePageObject {
await this.getButton(CLOSE_PARTICIPANTS_PANE).click();
}
/**
* Clicks Participants pane button.
*

View File

@@ -1,7 +1,7 @@
import process from 'node:process';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Fake Dial-In - ', () => {
it('join participant', async () => {
@@ -17,8 +17,7 @@ describe('Fake Dial-In - ', () => {
await ensureOneParticipant(ctx);
// check dial-in is enabled, so skip
if (await ctx.p1.driver.execute(() => Boolean(
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc))) {
if (await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
});

View File

@@ -0,0 +1,93 @@
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
describe('Follow Me - ', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants(ctx);
const { p1 } = ctx;
await p1.getToolbar().clickSettingsButton();
const settings = p1.getSettingsDialog();
await settings.waitForDisplay();
await settings.setFollowMe(true);
await settings.submit();
});
it('follow me checkbox visible only for moderators', async () => {
const { p2 } = ctx;
if (!await p2.isModerator()) {
await p2.getToolbar().clickSettingsButton();
const settings = p2.getSettingsDialog();
await settings.waitForDisplay();
expect(await settings.isFollowMeDisplayed()).toBe(false);
await settings.clickCloseButton();
}
});
it('filmstrip commands', async () => {
const { p1, p2 } = ctx;
const p1Filmstrip = p1.getFilmstrip();
const p2Filmstrip = p2.getFilmstrip();
await p1Filmstrip.toggle();
await p1Filmstrip.assertRemoteVideosHidden();
await p2Filmstrip.assertRemoteVideosHidden();
});
it('tile view', async () => {
await ensureThreeParticipants(ctx);
const { p1, p2, p3 } = ctx;
await p1.waitForTileViewDisplay();
await p1.getToolbar().clickExitTileViewButton();
await Promise.all([
p1.waitForTileViewDisplay(true),
p2.waitForTileViewDisplay(true),
p3.waitForTileViewDisplay(true)
]);
await p1.getToolbar().clickEnterTileViewButton();
await Promise.all([
p1.waitForTileViewDisplay(),
p2.waitForTileViewDisplay(),
p3.waitForTileViewDisplay()
]);
});
it('next on stage', async () => {
const { p1, p2, p3 } = ctx;
await p1.getFilmstrip().pinParticipant(p2);
const p2Filmstrip = p2.getFilmstrip();
const localVideoId = await p2Filmstrip.getLocalVideoId();
await p2.driver.waitUntil(
async () => await localVideoId === await p2.getLargeVideoId(),
{
timeout: 5_000,
timeoutMsg: 'The pinned participant is not displayed on stage for p2'
});
const p2VideoIdOnp3 = await p3.getFilmstrip().getRemoteVideoId(await p2.getEndpointId());
await p3.driver.waitUntil(
async () => p2VideoIdOnp3 === await p3.getLargeVideoId(),
{
timeout: 5_000,
timeoutMsg: 'The pinned participant is not displayed on stage for p3'
});
});
});

View File

@@ -2,7 +2,7 @@ import https from 'node:https';
import process from 'node:process';
import { ensureOneParticipant } from '../../helpers/participants';
import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Dial-In - ', () => {
it('join participant', async () => {
@@ -16,8 +16,7 @@ describe('Dial-In - ', () => {
await ensureOneParticipant(ctx);
// check dial-in is enabled
if (!await ctx.p1.driver.execute(() => Boolean(
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc))) {
if (!await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
});

View File

@@ -0,0 +1,87 @@
import { ensureOneParticipant } from '../../helpers/participants';
import { isDialInEnabled } from '../helpers/DialIn';
describe('Invite - ', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);
});
it('url displayed', async () => {
const { p1 } = ctx;
const inviteDialog = p1.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
const driverUrl = await p1.driver.getUrl();
expect(driverUrl.includes(await inviteDialog.getMeetingURL())).toBe(true);
await inviteDialog.clickCloseButton();
await inviteDialog.waitTillOpen(true);
});
it('dial-in displayed', async () => {
const { p1 } = ctx;
if (!await isDialInEnabled(p1)) {
return;
}
const inviteDialog = p1.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
});
it('view more numbers', async () => {
const { p1 } = ctx;
if (!await isDialInEnabled(p1)) {
return;
}
const inviteDialog = p1.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
const windows = await p1.driver.getWindowHandles();
expect(windows.length).toBe(1);
const meetingWindow = windows[0];
const displayedNumber = await inviteDialog.getDialInNumber();
const displayedPin = await inviteDialog.getPinNumber();
await inviteDialog.openDialInNumbersPage();
const newWindow = (await p1.driver.getWindowHandles()).filter(w => w !== meetingWindow);
expect(newWindow.length).toBe(1);
const moreNumbersWindow = newWindow[0];
await p1.driver.switchWindow(moreNumbersWindow);
await browser.pause(5000);
await p1.driver.$('.dial-in-numbers-list').waitForExist();
const conferenceIdMessage = p1.driver.$('//div[contains(@class, "pinLabel")]');
expect((await conferenceIdMessage.getText()).replace(/ /g, '').includes(displayedPin)).toBe(true);
const numbers = p1.driver.$$('.dial-in-number');
const nums = await numbers.filter(
async el => (await el.getText()).trim() === displayedNumber);
expect(nums.length).toBe(1);
});
});

View File

@@ -37,3 +37,12 @@ export async function cleanup(participant: Participant) {
await participant.getFilmstrip().kickParticipant(jigasiEndpointId);
}
}
/**
* Checks if the dial-in is enabled.
* @param participant
*/
export async function isDialInEnabled(participant: Participant) {
return await participant.driver.execute(() => Boolean(
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc));
}