mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
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:
@@ -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=
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}.`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ export default class Toolbar extends BasePageObject {
|
||||
await this.getButton(CLOSE_PARTICIPANTS_PANE).click();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clicks Participants pane button.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
93
tests/specs/3way/followMe.spec.ts
Normal file
93
tests/specs/3way/followMe.spec.ts
Normal 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'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
87
tests/specs/alone/invite.spec.ts
Normal file
87
tests/specs/alone/invite.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user