This commit is contained in:
86
tests/specs/2way/audioOnly.spec.ts
Normal file
86
tests/specs/2way/audioOnly.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Audio only', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
/**
|
||||
* Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.
|
||||
*/
|
||||
it('set and check', () => setAudioOnlyAndCheck(true));
|
||||
|
||||
/**
|
||||
* Verifies that participant1 sees avatars for itself and other participants.
|
||||
*/
|
||||
it('avatars check', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.driver.$('//div[@id="dominantSpeaker"]').waitForDisplayed();
|
||||
|
||||
// Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
|
||||
await p1.assertThumbnailShowsAvatar(p1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Disables audio only mode and verifies that both participants see p1 as not video muted.
|
||||
*/
|
||||
it('disable and check', () => setAudioOnlyAndCheck(false));
|
||||
|
||||
/**
|
||||
* Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1
|
||||
* as video muted.
|
||||
*/
|
||||
it('mute video, set twice and check muted', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
// Mute video on participant1.
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await verifyVideoMute(true);
|
||||
|
||||
// Enable audio-only mode.
|
||||
await setAudioOnlyAndCheck(true);
|
||||
|
||||
// Disable audio-only mode.
|
||||
await p1.getVideoQualityDialog().setVideoQuality(false);
|
||||
|
||||
// p1 should stay muted since it was muted before audio-only was enabled.
|
||||
await verifyVideoMute(true);
|
||||
});
|
||||
|
||||
it('unmute video and check not muted', async () => {
|
||||
// Unmute video on participant1.
|
||||
await ctx.p1.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await verifyVideoMute(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
|
||||
* p2 participant sees a video mute state for the former.
|
||||
* @param enable
|
||||
*/
|
||||
async function setAudioOnlyAndCheck(enable: boolean) {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getVideoQualityDialog().setVideoQuality(enable);
|
||||
|
||||
await verifyVideoMute(enable);
|
||||
|
||||
await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
|
||||
.waitForDisplayed({ reverse: !enable });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that p1 and p2 see p1 as video muted or not.
|
||||
* @param muted
|
||||
*/
|
||||
async function verifyVideoMute(muted: boolean) {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// Verify the observer sees the testee in the desired muted state.
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
|
||||
|
||||
// Verify the testee sees itself in the desired muted state.
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
|
||||
}
|
||||
41
tests/specs/2way/displayName.spec.ts
Normal file
41
tests/specs/2way/displayName.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('DisplayName', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants({ skipDisplayName: true }));
|
||||
|
||||
it('check change', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// default remote display name
|
||||
const defaultDisplayName = await p1.execute(() => config.defaultRemoteDisplayName);
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
|
||||
// Checks whether default display names are set and shown, when both sides still miss the display name.
|
||||
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EndpointId)).toBe(defaultDisplayName);
|
||||
expect(await p2.getFilmstrip().getRemoteDisplayName(p1EndpointId)).toBe(defaultDisplayName);
|
||||
|
||||
const randomName = `Name${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
|
||||
await p2.setLocalDisplayName(randomName);
|
||||
expect(await p2.getLocalDisplayName()).toBe(randomName);
|
||||
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EndpointId)).toBe(randomName);
|
||||
});
|
||||
|
||||
it('check persistence', async () => {
|
||||
const { p2 } = ctx;
|
||||
const randomName = `Name${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
|
||||
await p2.setLocalDisplayName(randomName);
|
||||
|
||||
expect(await p2.getLocalDisplayName()).toBe(randomName);
|
||||
|
||||
await p2.hangup();
|
||||
|
||||
await ensureTwoParticipants({
|
||||
skipDisplayName: true
|
||||
});
|
||||
|
||||
expect(await p2.getLocalDisplayName()).toBe(randomName);
|
||||
});
|
||||
});
|
||||
20
tests/specs/2way/endConference.spec.ts
Normal file
20
tests/specs/2way/endConference.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('End Conference', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('hangup call and check', async () => {
|
||||
const { p1 } = ctx;
|
||||
const url = await p1.driver.getUrl();
|
||||
|
||||
await p1.getToolbar().clickHangupButton();
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1.driver.getUrl() !== url,
|
||||
{
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'p1 did not navigate away from the conference'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
52
tests/specs/2way/fakeDialInAudio.spec.ts
Normal file
52
tests/specs/2way/fakeDialInAudio.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
|
||||
|
||||
describe('Fake Dial-In', () => {
|
||||
it('join participant', async () => {
|
||||
// we execute fake dial in only if the real dial in is not enabled
|
||||
|
||||
// check rest url is not configured
|
||||
if (process.env.DIAL_IN_REST_URL) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
// check dial-in is enabled, so skip
|
||||
if (await isDialInEnabled(ctx.p1)) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('open invite dialog', async () => {
|
||||
await ctx.p1.getInviteDialog().open();
|
||||
|
||||
await ctx.p1.getInviteDialog().clickCloseButton();
|
||||
});
|
||||
|
||||
it('invite second participant', async () => {
|
||||
if (!await ctx.p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureTwoParticipants();
|
||||
});
|
||||
|
||||
it('wait for audio from second participant', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (!await p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await cleanup(p1);
|
||||
});
|
||||
});
|
||||
42
tests/specs/2way/grantModerator.spec.ts
Normal file
42
tests/specs/2way/grantModerator.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Grant moderator', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureOneParticipant();
|
||||
|
||||
if (await ctx.p1.execute(() => typeof APP.conference._room.grantOwner !== 'function')) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureTwoParticipants();
|
||||
});
|
||||
|
||||
it('grant moderator and validate', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (await p2.isModerator()) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.getFilmstrip().grantModerator(p2);
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.isModerator(),
|
||||
{
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'p2 did not become moderator'
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
});
|
||||
44
tests/specs/2way/kick.spec.ts
Normal file
44
tests/specs/2way/kick.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Kick', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
if (!await ctx.p1.isModerator()) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('kick and check', () => kickParticipant2AndCheck());
|
||||
|
||||
it('kick p2p and check', async () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await kickParticipant2AndCheck();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Kicks the second participant and checks that the participant is removed from the conference and that dialog is open.
|
||||
*/
|
||||
async function kickParticipant2AndCheck() {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.getFilmstrip().kickParticipant(await p2.getEndpointId());
|
||||
|
||||
await p1.waitForParticipants(0);
|
||||
|
||||
// check that the kicked participant sees the kick reason dialog
|
||||
// let's wait for this to appear at least 2 seconds
|
||||
await p2.driver.waitUntil(
|
||||
async () => p2.isLeaveReasonDialogOpen(), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'No leave reason dialog shown for p2'
|
||||
});
|
||||
}
|
||||
189
tests/specs/2way/lockRoom.spec.ts
Normal file
189
tests/specs/2way/lockRoom.spec.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
|
||||
import type SecurityDialog from '../../pageobjects/SecurityDialog';
|
||||
|
||||
let roomKey: string;
|
||||
|
||||
/**
|
||||
* 1. Lock the room (make sure the image changes to locked)
|
||||
* 2. Join with a second browser/tab
|
||||
* 3. Make sure we are required to enter a password.
|
||||
* (Also make sure the padlock is locked)
|
||||
* 4. Enter wrong password, make sure we are not joined in the room
|
||||
* 5. Unlock the room (Make sure the padlock is unlocked)
|
||||
* 6. Join again and make sure we are not asked for a password and that
|
||||
* the padlock is unlocked.
|
||||
*/
|
||||
describe('Lock Room', () => {
|
||||
it('joining the meeting', () => ensureOneParticipant());
|
||||
|
||||
it('locks the room', () => participant1LockRoom());
|
||||
|
||||
it('enter participant in locked room', async () => {
|
||||
// first enter wrong pin then correct one
|
||||
await joinSecondParticipant({
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
// wait for password prompt
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
|
||||
|
||||
// give sometime to the password prompt to disappear and send the password
|
||||
await p2.driver.pause(500);
|
||||
|
||||
// wait for password prompt
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(roomKey);
|
||||
|
||||
await p2.waitToJoinMUC();
|
||||
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, true);
|
||||
});
|
||||
|
||||
it('unlock room', async () => {
|
||||
// Unlock room. Check whether room is still locked. Click remove and check whether it is unlocked.
|
||||
await ctx.p2.hangup();
|
||||
|
||||
await participant1UnlockRoom();
|
||||
});
|
||||
|
||||
it('enter participant in unlocked room', async () => {
|
||||
// Just enter the room and check that is not locked.
|
||||
// if we fail to unlock the room this one will detect it
|
||||
// as participant will fail joining
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
|
||||
await p2SecurityDialog.clickCloseButton();
|
||||
});
|
||||
|
||||
it('update locked state while participants in room', async () => {
|
||||
// Both participants are in unlocked room, lock it and see whether the
|
||||
// change is reflected on the second participant icon.
|
||||
await participant1LockRoom();
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, true);
|
||||
|
||||
await participant1UnlockRoom();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
});
|
||||
it('unlock after participant enter wrong password', async () => {
|
||||
// P1 locks the room. Participant tries to enter using wrong password.
|
||||
// P1 unlocks the room and Participant submits the password prompt with no password entered and
|
||||
// should enter of unlocked room.
|
||||
await ctx.p2.hangup();
|
||||
await participant1LockRoom();
|
||||
await joinSecondParticipant({
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
// wait for password prompt
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
|
||||
|
||||
// give sometime to the password prompt to disappear and send the password
|
||||
await p2.driver.pause(500);
|
||||
|
||||
// wait for password prompt
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
|
||||
await participant1UnlockRoom();
|
||||
|
||||
await p2PasswordDialog.clickOkButton();
|
||||
await p2.waitToJoinMUC();
|
||||
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Participant1 locks the room.
|
||||
*/
|
||||
async function participant1LockRoom() {
|
||||
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.addPassword(roomKey);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, true);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Participant1 unlocks the room.
|
||||
*/
|
||||
async function participant1UnlockRoom() {
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await p1SecurityDialog.removePassword();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the room to be locked or unlocked.
|
||||
* @param securityDialog
|
||||
* @param locked
|
||||
*/
|
||||
function waitForRoomLockState(securityDialog: SecurityDialog, locked: boolean) {
|
||||
return securityDialog.participant.driver.waitUntil(
|
||||
async () => await securityDialog.isLocked() === locked,
|
||||
{
|
||||
timeout: 3_000, // 3 seconds
|
||||
timeoutMsg: `Timeout waiting for the room to unlock for ${securityDialog.participant.name}.`
|
||||
}
|
||||
);
|
||||
}
|
||||
138
tests/specs/2way/mute.spect.ts
Normal file
138
tests/specs/2way/mute.spect.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureOneParticipant,
|
||||
ensureTwoParticipants,
|
||||
joinSecondParticipant,
|
||||
muteAudioAndCheck,
|
||||
unmuteAudioAndCheck,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('Mute', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('mute p1 and check', () => toggleMuteAndCheck(ctx.p1, ctx.p2, true));
|
||||
|
||||
it('unmute p1 and check', () => toggleMuteAndCheck(ctx.p1, ctx.p2, false));
|
||||
|
||||
it('mute p2 and check', () => toggleMuteAndCheck(ctx.p2, ctx.p1, true));
|
||||
|
||||
it('unmute p2 and check', () => toggleMuteAndCheck(ctx.p2, ctx.p1, false));
|
||||
|
||||
it('p1 mutes p2 and check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
|
||||
// and now check whether second participant is muted
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
});
|
||||
|
||||
it('p2 unmute after p1 mute and check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await unmuteAudioAndCheck(p2, p1);
|
||||
});
|
||||
|
||||
it('p1 mutes before p2 joins', async () => {
|
||||
await ctx.p2.hangup();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
|
||||
await toggleMuteAndCheck(p1, p2, false);
|
||||
});
|
||||
|
||||
it('mute before join and screen share after in p2p', () => muteP1BeforeP2JoinsAndScreenshare(true));
|
||||
|
||||
it('mute before join and screen share after with jvb', () => muteP1BeforeP2JoinsAndScreenshare(false));
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the mute state of a specific Meet conference participant and
|
||||
* verifies that a specific other Meet conference participants sees a
|
||||
* specific mute state for the former.
|
||||
* @param testee The participant whose mute state is to be toggled.
|
||||
* @param observer The participant to verify the mute state of {@code testee}.
|
||||
* @param muted the mute state of {@code testee} expected to be observed by {@code observer}.
|
||||
*/
|
||||
async function toggleMuteAndCheck(
|
||||
testee: Participant,
|
||||
observer: Participant,
|
||||
muted: boolean) {
|
||||
if (muted) {
|
||||
await muteAudioAndCheck(testee, observer);
|
||||
} else {
|
||||
await unmuteAudioAndCheck(testee, observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Video mutes participant1 before participant2 joins and checks if participant1 can share or unmute video
|
||||
* and that media is being received on participant2 in both the cases.
|
||||
*
|
||||
* @param p2p whether to enable p2p or not.
|
||||
*/
|
||||
async function muteP1BeforeP2JoinsAndScreenshare(p2p: boolean) {
|
||||
await Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup() ]);
|
||||
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: p2p
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await joinSecondParticipant({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: p2p
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
if (p2p) {
|
||||
await p2.waitForP2PIceConnected();
|
||||
} else {
|
||||
await p2.waitForIceConnected();
|
||||
}
|
||||
|
||||
await p2.waitForSendMedia();
|
||||
|
||||
// Check if p1 appears video muted on p2.
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
// Start desktop share.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
|
||||
// we need to pass the id of the fake participant we use for the screensharing
|
||||
await p2.waitForRemoteVideo(`${await p1.getEndpointId()}-v1`);
|
||||
|
||||
// Stop desktop share and unmute video and check for video again.
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
await p2.waitForRemoteVideo(await p1.getEndpointId());
|
||||
}
|
||||
125
tests/specs/2way/preJoin.spec.ts
Normal file
125
tests/specs/2way/preJoin.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ensureOneParticipant, joinFirstParticipant, joinSecondParticipant } from '../../helpers/participants';
|
||||
|
||||
describe('PreJoin', () => {
|
||||
it('display name required', async () => {
|
||||
await joinFirstParticipant({
|
||||
configOverwrite: {
|
||||
prejoinConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
requireDisplayName: true
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
|
||||
|
||||
await p1PreJoinScreen.waitForLoading();
|
||||
|
||||
const joinButton = p1PreJoinScreen.getJoinButton();
|
||||
|
||||
await joinButton.waitForDisplayed();
|
||||
await joinButton.click();
|
||||
|
||||
const error = p1PreJoinScreen.getErrorOnJoin();
|
||||
|
||||
await error.waitForDisplayed();
|
||||
|
||||
await ctx.p1.hangup();
|
||||
});
|
||||
|
||||
it('without lobby', async () => {
|
||||
await joinFirstParticipant({
|
||||
configOverwrite: {
|
||||
prejoinConfig: {
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
|
||||
|
||||
await p1PreJoinScreen.waitForLoading();
|
||||
|
||||
const joinButton = p1PreJoinScreen.getJoinButton();
|
||||
|
||||
await joinButton.waitForDisplayed();
|
||||
|
||||
await ctx.p1.hangup();
|
||||
});
|
||||
|
||||
it('without audio', async () => {
|
||||
await joinFirstParticipant({
|
||||
configOverwrite: {
|
||||
prejoinConfig: {
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
const p1PreJoinScreen = p1.getPreJoinScreen();
|
||||
|
||||
await p1PreJoinScreen.waitForLoading();
|
||||
|
||||
await p1PreJoinScreen.getJoinOptions().click();
|
||||
|
||||
const joinWithoutAudioBtn = p1PreJoinScreen.getJoinWithoutAudioButton();
|
||||
|
||||
await joinWithoutAudioBtn.waitForClickable();
|
||||
await joinWithoutAudioBtn.click();
|
||||
|
||||
await p1.waitToJoinMUC();
|
||||
|
||||
await p1.driver.$('//div[contains(@class, "audio-preview")]//div[contains(@class, "toolbox-icon") '
|
||||
+ 'and contains(@class, "toggled") and contains(@class, "disabled")]')
|
||||
.waitForDisplayed();
|
||||
|
||||
await ctx.p1.hangup();
|
||||
});
|
||||
|
||||
it('with lobby', async () => {
|
||||
await ensureOneParticipant();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLobbyEnabled()).toBe(false);
|
||||
|
||||
await p1SecurityDialog.toggleLobby();
|
||||
await p1SecurityDialog.waitForLobbyEnabled();
|
||||
|
||||
await joinSecondParticipant({
|
||||
configOverwrite: {
|
||||
prejoinConfig: {
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const p1PreJoinScreen = ctx.p2.getPreJoinScreen();
|
||||
|
||||
await p1PreJoinScreen.waitForLoading();
|
||||
|
||||
const joinButton = p1PreJoinScreen.getJoinButton();
|
||||
|
||||
await joinButton.waitForDisplayed();
|
||||
|
||||
});
|
||||
});
|
||||
68
tests/specs/2way/selfView.spec.ts
Normal file
68
tests/specs/2way/selfView.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Self view', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('hide from menu', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await checkSelfViewHidden(p1, false);
|
||||
|
||||
await p1.getFilmstrip().hideSelfView();
|
||||
|
||||
await checkSelfViewHidden(p1, true, true);
|
||||
|
||||
await p1.getToolbar().clickEnterTileViewButton();
|
||||
|
||||
await checkSelfViewHidden(p1, true);
|
||||
});
|
||||
|
||||
it('show from settings', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await toggleSelfViewFromSettings(p1, false);
|
||||
|
||||
await checkSelfViewHidden(p1, false);
|
||||
});
|
||||
|
||||
it('hide from settings', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await toggleSelfViewFromSettings(p1, true);
|
||||
await checkSelfViewHidden(p1, true, true);
|
||||
});
|
||||
|
||||
it('check in alone meeting', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await checkSelfViewHidden(p1, true);
|
||||
await p2.hangup();
|
||||
await checkSelfViewHidden(p1, true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the self view option from the settings dialog.
|
||||
*/
|
||||
async function toggleSelfViewFromSettings(participant: Participant, hide: boolean) {
|
||||
await participant.getToolbar().clickSettingsButton();
|
||||
|
||||
const settings = participant.getSettingsDialog();
|
||||
|
||||
await settings.waitForDisplay();
|
||||
await settings.setHideSelfView(hide);
|
||||
await settings.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the local self view is displayed or not.
|
||||
*/
|
||||
async function checkSelfViewHidden(participant: Participant, hidden: boolean, checkNotification = false) {
|
||||
if (checkNotification) {
|
||||
await participant.getNotifications().waitForReEnableSelfViewNotification();
|
||||
await participant.getNotifications().closeReEnableSelfViewNotification();
|
||||
}
|
||||
|
||||
await participant.getFilmstrip().assertSelfViewIsHidden(hidden);
|
||||
}
|
||||
29
tests/specs/2way/singlePort.spec.ts
Normal file
29
tests/specs/2way/singlePort.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Single port', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('test', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
const port1 = await getRemotePort(p1);
|
||||
const port2 = await getRemotePort(p2);
|
||||
|
||||
expect(Number.isInteger(port1)).toBe(true);
|
||||
expect(Number.isInteger(port2)).toBe(true);
|
||||
expect(port1).toBe(port2);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the remote port of the participant.
|
||||
* @param participant
|
||||
*/
|
||||
async function getRemotePort(participant: Participant) {
|
||||
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.ip);
|
||||
|
||||
const parts = data.split(':');
|
||||
|
||||
return parts.length > 1 ? parseInt(parts[1], 10) : '';
|
||||
}
|
||||
41
tests/specs/2way/stopVideo.spec.ts
Normal file
41
tests/specs/2way/stopVideo.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ensureTwoParticipants, muteVideoAndCheck, unmuteVideoAndCheck } from '../../helpers/participants';
|
||||
|
||||
describe('Stop video', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('stop video and check', () => muteVideoAndCheck(ctx.p1, ctx.p2));
|
||||
|
||||
it('start video and check', () => unmuteVideoAndCheck(ctx.p1, ctx.p2));
|
||||
|
||||
it('start video and check stream', async () => {
|
||||
await muteVideoAndCheck(ctx.p1, ctx.p2);
|
||||
|
||||
// now participant2 should be on large video
|
||||
const largeVideoId = await ctx.p1.getLargeVideo().getId();
|
||||
|
||||
await unmuteVideoAndCheck(ctx.p1, ctx.p2);
|
||||
|
||||
// check if video stream from second participant is still on large video
|
||||
expect(largeVideoId).toBe(await ctx.p1.getLargeVideo().getId());
|
||||
});
|
||||
|
||||
it('stop video on participant and check', () => muteVideoAndCheck(ctx.p2, ctx.p1));
|
||||
|
||||
it('start video on participant and check', () => unmuteVideoAndCheck(ctx.p2, ctx.p1));
|
||||
|
||||
it('stop video on before second joins', async () => {
|
||||
await ctx.p2.hangup();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
});
|
||||
});
|
||||
32
tests/specs/2way/subject.spec.ts
Normal file
32
tests/specs/2way/subject.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
const MY_TEST_SUBJECT = 'My Test Subject';
|
||||
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
|
||||
|
||||
describe('Subject', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
subject: MY_TEST_SUBJECT
|
||||
}
|
||||
}));
|
||||
|
||||
it('check', async () => {
|
||||
await checkSubject(ctx.p1, MY_TEST_SUBJECT);
|
||||
await checkSubject(ctx.p2, MY_TEST_SUBJECT);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check was subject set.
|
||||
*
|
||||
* @param participant
|
||||
* @param subject
|
||||
*/
|
||||
async function checkSubject(participant: Participant, subject: string) {
|
||||
const localTile = participant.driver.$(SUBJECT_XPATH);
|
||||
|
||||
await localTile.moveTo();
|
||||
|
||||
expect(await localTile.getText()).toBe(subject);
|
||||
}
|
||||
39
tests/specs/2way/switchVideo.spec.ts
Normal file
39
tests/specs/2way/switchVideo.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('SwitchVideo', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('p1 click on local', () => ctx.p1.getFilmstrip().pinParticipant(ctx.p1));
|
||||
|
||||
it('p1 click on remote', async () => {
|
||||
await closeToolbarMenu();
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.getFilmstrip().pinParticipant(p2);
|
||||
});
|
||||
|
||||
it('p1 unpin remote', () => ctx.p1.getFilmstrip().unpinParticipant(ctx.p2));
|
||||
|
||||
it('p2 pin remote', () => ctx.p2.getFilmstrip().pinParticipant(ctx.p1));
|
||||
|
||||
it('p2 unpin remote', () => ctx.p2.getFilmstrip().unpinParticipant(ctx.p1));
|
||||
|
||||
it('p2 click on local', () => ctx.p2.getFilmstrip().pinParticipant(ctx.p2));
|
||||
|
||||
it('p2 click on remote', async () => {
|
||||
await closeToolbarMenu();
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p2.getFilmstrip().pinParticipant(p1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Closes the overflow menu on both participants.
|
||||
*/
|
||||
async function closeToolbarMenu() {
|
||||
await ctx.p1.getToolbar().closeOverflowMenu();
|
||||
await ctx.p2.getToolbar().closeOverflowMenu();
|
||||
}
|
||||
26
tests/specs/2way/udp.spec.ts
Normal file
26
tests/specs/2way/udp.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('UDP', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
it('check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// just in case wait 1500, this is the interval we use for `config.pcStatsInterval`
|
||||
await p1.driver.pause(1500);
|
||||
|
||||
expect(await getProtocol(p1)).toBe('udp');
|
||||
expect(await getProtocol(p2)).toBe('udp');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the remote port of the participant.
|
||||
* @param participant
|
||||
*/
|
||||
async function getProtocol(participant: Participant) {
|
||||
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.type);
|
||||
|
||||
return data.toLowerCase();
|
||||
}
|
||||
41
tests/specs/2way/urlNormalisation.spec.ts
Normal file
41
tests/specs/2way/urlNormalisation.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { multiremotebrowser } from '@wdio/globals';
|
||||
|
||||
import { config } from '../../helpers/TestsConfig';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('URL Normalisation', () => {
|
||||
it('joining the meeting', async () => {
|
||||
|
||||
// if we are running with token this becomes ugly to match the URL
|
||||
if (config.jwt.preconfiguredToken) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// a hack to extract the baseUrl that the test will use
|
||||
const baseUrl = multiremotebrowser.getInstance('p1').options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('baseUrl is not set');
|
||||
}
|
||||
|
||||
await ensureTwoParticipants({
|
||||
forceTenant: 'tenant@example.com',
|
||||
roomName: `${ctx.roomName}@example.com`
|
||||
});
|
||||
});
|
||||
|
||||
it('check', async () => {
|
||||
const currentUrlStr = await ctx.p1.driver.getUrl();
|
||||
const currentUrl = new URL(currentUrlStr);
|
||||
const path = currentUrl.pathname;
|
||||
|
||||
const parts = path.split('/');
|
||||
|
||||
expect(parts[1]).toBe('tenantexample.com');
|
||||
|
||||
// @ts-ignore
|
||||
expect(parts[2]).toBe(`${ctx.roomName}example.com`);
|
||||
});
|
||||
});
|
||||
91
tests/specs/3way/activeSpeaker.spec.ts
Normal file
91
tests/specs/3way/activeSpeaker.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { ensureThreeParticipants, muteAudioAndCheck } from '../../helpers/participants';
|
||||
|
||||
describe('ActiveSpeaker', () => {
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
await muteAudioAndCheck(p2, p1);
|
||||
await muteAudioAndCheck(p3, p1);
|
||||
|
||||
// participant1 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(p1, p2, p3);
|
||||
|
||||
// participant3 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(p3, p2, p1);
|
||||
|
||||
// participant2 becomes active speaker - check from participant1's perspective
|
||||
await testActiveSpeaker(p2, p1, p3);
|
||||
|
||||
// check the displayed speakers, there should be only one speaker
|
||||
await assertOneDominantSpeaker(p1);
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tries to make given participant an active speaker by unmuting it.
|
||||
* Verifies from {@code participant2}'s perspective that the active speaker
|
||||
* has been displayed on the large video area. Mutes him back.
|
||||
*
|
||||
* @param {Participant} activeSpeaker - <tt>Participant</tt> instance of the participant who will be tested as an
|
||||
* active speaker.
|
||||
* @param {Participant} otherParticipant1 - <tt>Participant</tt> of the participant who will be observing and verifying
|
||||
* active speaker change.
|
||||
* @param {Participant} otherParticipant2 - Used only to print some debugging info.
|
||||
*/
|
||||
async function testActiveSpeaker(
|
||||
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
|
||||
activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
|
||||
|
||||
const speakerEndpoint = await activeSpeaker.getEndpointId();
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Unmuting in testActiveSpeaker');
|
||||
|
||||
// Unmute
|
||||
await activeSpeaker.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// just a debug print to go in logs
|
||||
otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
|
||||
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
|
||||
|
||||
// Verify that the user is now an active speaker from otherParticipant1's perspective
|
||||
const otherParticipant1Driver = otherParticipant1.driver;
|
||||
|
||||
await otherParticipant1Driver.waitUntil(
|
||||
async () => await otherParticipant1.getLargeVideo().getResource() === speakerEndpoint,
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: 'Active speaker not displayed on large video.'
|
||||
});
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Muting in testActiveSpeaker');
|
||||
|
||||
// Mute back again
|
||||
await activeSpeaker.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// just a debug print to go in logs
|
||||
otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
|
||||
await otherParticipant1.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the number of small videos with the dominant speaker
|
||||
* indicator displayed equals 1.
|
||||
*
|
||||
* @param {Participant} participant - The participant to check.
|
||||
*/
|
||||
async function assertOneDominantSpeaker(participant: Participant) {
|
||||
expect(await participant.driver.$$(
|
||||
'//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
|
||||
}
|
||||
297
tests/specs/3way/audioVideoModeration.spec.ts
Normal file
297
tests/specs/3way/audioVideoModeration.spec.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { config } from '../../helpers/TestsConfig';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants, ensureTwoParticipants,
|
||||
hangupAllParticipants,
|
||||
unmuteAudioAndCheck,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('AVModeration', () => {
|
||||
|
||||
it('check for moderators', async () => {
|
||||
// if all 3 participants are moderators, skip this test
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()
|
||||
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('check audio enable/disable', async () => {
|
||||
const { p1, p3 } = ctx;
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// Here we want to try unmuting and check that we are still muted.
|
||||
await tryToAudioUnmuteAndCheck(p3, p1);
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await unmuteAudioAndCheck(p3, p1);
|
||||
});
|
||||
|
||||
it('check video enable/disable', async () => {
|
||||
const { p1, p3 } = ctx;
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// Here we want to try unmuting and check that we are still muted.
|
||||
await tryToVideoUnmuteAndCheck(p3, p1);
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await unmuteVideoAndCheck(p3, p1);
|
||||
});
|
||||
|
||||
it('unmute by moderator', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await unmuteByModerator(p1, p3, true, true);
|
||||
|
||||
// moderation is stopped at this point, make sure participants 1 & 2 are also unmuted,
|
||||
// participant3 was unmuted by unmuteByModerator
|
||||
await unmuteAudioAndCheck(p2, p1);
|
||||
await unmuteVideoAndCheck(p2, p1);
|
||||
|
||||
// make sure p1 is not muted after turning on and then off the AV moderation
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
|
||||
});
|
||||
|
||||
it('hangup and change moderator', async () => {
|
||||
// no moderator switching if jaas is available.
|
||||
if (config.iframe.usesJaas) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([ ctx.p2.hangup(), ctx.p3.hangup() ]);
|
||||
|
||||
await ensureThreeParticipants();
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p2.getToolbar().clickRaiseHandButton();
|
||||
await p3.getToolbar().clickRaiseHandButton();
|
||||
|
||||
await p1.hangup();
|
||||
|
||||
// we don't use ensureThreeParticipants to avoid all meeting join checks
|
||||
// all participants are muted and checks for media will fail
|
||||
await ensureOneParticipant();
|
||||
|
||||
// After p1 re-joins either p2 or p3 is promoted to moderator. They should still be muted.
|
||||
const isP2Moderator = await p2.isModerator();
|
||||
const moderator = isP2Moderator ? p2 : p3;
|
||||
const nonModerator = isP2Moderator ? p3 : p2;
|
||||
const moderatorParticipantsPane = moderator.getParticipantsPane();
|
||||
const nonModeratorParticipantsPane = nonModerator.getParticipantsPane();
|
||||
|
||||
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
|
||||
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
|
||||
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(nonModerator, p1);
|
||||
await unmuteVideoAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
});
|
||||
it('grant moderator', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await p1.getFilmstrip().grantModerator(p3);
|
||||
|
||||
await p3.driver.waitUntil(
|
||||
() => p3.isModerator(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: `${p3.name} is not moderator`
|
||||
});
|
||||
|
||||
await unmuteByModerator(p3, p2, false, true);
|
||||
});
|
||||
it('ask to unmute', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// mute p2
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// ask p2 to unmute
|
||||
await p1.getParticipantsPane().askToUnmute(p2, true);
|
||||
|
||||
await p2.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await p1.getParticipantsPane().close();
|
||||
});
|
||||
it('remove from whitelist', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await unmuteByModerator(p1, p2, true, false);
|
||||
|
||||
// p1 mute audio on p2 and check
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
|
||||
// we try to unmute and test it that it was still muted
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
|
||||
// stop video and check
|
||||
await p1.getFilmstrip().muteVideo(p2);
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
await tryToVideoUnmuteAndCheck(p2, p1);
|
||||
});
|
||||
it('join moderated', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
const p1ParticipantsPane = ctx.p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.clickContextMenuButton();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await p1ParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
// join with second participant and check
|
||||
await ensureTwoParticipants({
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p2.getNotifications().closeYouAreMutedNotification();
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
await tryToVideoUnmuteAndCheck(p2, p1);
|
||||
|
||||
// asked to unmute and check
|
||||
await unmuteByModerator(p1, p2, false, false);
|
||||
|
||||
// mute and check
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
|
||||
await tryToAudioUnmuteAndCheck(p2, p1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks a user can unmute after being asked by moderator.
|
||||
* @param moderator - The participant that is moderator.
|
||||
* @param participant - The participant being asked to unmute.
|
||||
* @param turnOnModeration - if we want to turn on moderation before testing (when it is currently off).
|
||||
* @param stopModeration - true if moderation to be stopped when done.
|
||||
*/
|
||||
async function unmuteByModerator(
|
||||
moderator: Participant,
|
||||
participant: Participant,
|
||||
turnOnModeration: boolean,
|
||||
stopModeration: boolean) {
|
||||
const moderatorParticipantsPane = moderator.getParticipantsPane();
|
||||
|
||||
if (turnOnModeration) {
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStartAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStartVideoModeration();
|
||||
|
||||
await moderatorParticipantsPane.close();
|
||||
}
|
||||
|
||||
// raise hand to speak
|
||||
await participant.getToolbar().clickRaiseHandButton();
|
||||
await moderator.getNotifications().waitForRaisedHandNotification();
|
||||
|
||||
// ask participant to unmute
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await moderatorParticipantsPane.askToUnmute(participant, false);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(participant, moderator);
|
||||
await unmuteVideoAndCheck(participant, moderator);
|
||||
|
||||
if (stopModeration) {
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopAudioModeration();
|
||||
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
|
||||
|
||||
await moderatorParticipantsPane.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of moderation, tries to audio unmute but stays muted.
|
||||
* Checks locally and remotely that this is still the case.
|
||||
* @param participant
|
||||
* @param observer
|
||||
*/
|
||||
async function tryToAudioUnmuteAndCheck(participant: Participant, observer: Participant) {
|
||||
// try to audio unmute and check
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Check local audio muted icon state
|
||||
await participant.getFilmstrip().assertAudioMuteIconIsDisplayed(participant);
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of moderation, tries to video unmute but stays muted.
|
||||
* Checks locally and remotely that this is still the case.
|
||||
* @param participant
|
||||
* @param observer
|
||||
*/
|
||||
async function tryToVideoUnmuteAndCheck(participant: Participant, observer: Participant) {
|
||||
// try to video unmute and check
|
||||
await participant.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
// Check local audio muted icon state
|
||||
await participant.getParticipantsPane().assertVideoMuteIconIsDisplayed(participant);
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(participant);
|
||||
}
|
||||
212
tests/specs/3way/avatars.spec.ts
Normal file
212
tests/specs/3way/avatars.spec.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import {
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
const EMAIL = 'support@jitsi.org';
|
||||
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
|
||||
|
||||
describe('Avatar', () => {
|
||||
it('setup the meeting', () =>
|
||||
ensureTwoParticipants({
|
||||
skipDisplayName: true
|
||||
})
|
||||
);
|
||||
|
||||
it('change and check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// check default avatar for p1 on p2
|
||||
await p2.assertDefaultAvatarExist(p1);
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
const settings = p1.getSettingsDialog();
|
||||
|
||||
await settings.waitForDisplay();
|
||||
await settings.setEmail(EMAIL);
|
||||
await settings.submit();
|
||||
|
||||
// check if the local avatar in the toolbar menu has changed
|
||||
await p1.driver.waitUntil(
|
||||
async () => (await p1.getToolbar().getProfileImage())?.includes(HASH), {
|
||||
timeout: 3000, // give more time for the initial download of the image
|
||||
timeoutMsg: 'Avatar has not changed for p1'
|
||||
});
|
||||
|
||||
// check if the avatar in the local thumbnail has changed
|
||||
expect(await p1.getLocalVideoAvatar()).toContain(HASH);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
async () => (await p2.getFilmstrip().getAvatar(p1EndpointId))?.includes(HASH), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Avatar has not changed for p1 on p2'
|
||||
});
|
||||
|
||||
// check if the avatar in the large video has changed
|
||||
expect(await p2.getLargeVideo().getAvatar()).toContain(HASH);
|
||||
|
||||
// we check whether the default avatar of participant2 is displayed on both sides
|
||||
await p1.assertDefaultAvatarExist(p2);
|
||||
await p2.assertDefaultAvatarExist(p2);
|
||||
|
||||
// the problem on FF where we can send keys to the input field,
|
||||
// and the m from the text can mute the call, check whether we are muted
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
});
|
||||
|
||||
it('when video muted', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await ctx.p2.hangup();
|
||||
|
||||
// Mute p1's video
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => (await p1.getLargeVideo().getAvatar())?.includes(HASH), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
const p1LargeSrc = await p1.getLargeVideo().getAvatar();
|
||||
const p1ThumbSrc = await p1.getLocalVideoAvatar();
|
||||
|
||||
// Check if avatar on large video is the same as on local thumbnail
|
||||
expect(p1ThumbSrc).toBe(p1LargeSrc);
|
||||
|
||||
// Join p2
|
||||
await ensureTwoParticipants({
|
||||
skipDisplayName: true
|
||||
});
|
||||
const { p2 } = ctx;
|
||||
|
||||
// Verify that p1 is muted from the perspective of p2
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p2.getFilmstrip().pinParticipant(p1);
|
||||
|
||||
// Check if p1's avatar is on large video now
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getLargeVideo().getAvatar() === p1LargeSrc, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'Avatar on large video did not change'
|
||||
});
|
||||
|
||||
// p1 pins p2's video
|
||||
await p1.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
// Check if avatar is displayed on p1's local video thumbnail
|
||||
await p1.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
|
||||
// Unmute - now local avatar should be hidden and local video displayed
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
|
||||
await p1.asserLocalThumbnailShowsVideo();
|
||||
|
||||
// Now both p1 and p2 have video muted
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await p2.getToolbar().clickVideoMuteButton();
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
// Start the third participant
|
||||
await ensureThreeParticipants({
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// When the first participant is FF because of their audio mic feed it will never become dominant speaker
|
||||
// and no audio track will be received by the third participant and video is muted,
|
||||
// that's why we need to do a different check that expects any track just from p2
|
||||
if (p1.driver.isFirefox) {
|
||||
await Promise.all([ p2.waitForRemoteStreams(1), p3.waitForRemoteStreams(1) ]);
|
||||
} else {
|
||||
await Promise.all([ p2.waitForRemoteStreams(2), p3.waitForRemoteStreams(2) ]);
|
||||
}
|
||||
|
||||
// Pin local video and verify avatars are displayed
|
||||
await p3.getFilmstrip().pinParticipant(p3);
|
||||
|
||||
await p3.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p2, false, true);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
|
||||
expect(await p3.getFilmstrip().getAvatar(p1EndpointId)).toBe(p1ThumbSrc);
|
||||
|
||||
// Click on p1's video
|
||||
await p3.getFilmstrip().pinParticipant(p1);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.getLargeVideo().getResource() === p1EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${p1.name}`
|
||||
});
|
||||
|
||||
await p3.assertDisplayNameVisibleOnStage(
|
||||
await p3.getFilmstrip().getRemoteDisplayName(p1EndpointId));
|
||||
|
||||
// p2 has the default avatar
|
||||
await p3.assertThumbnailShowsAvatar(p2, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p3, true);
|
||||
|
||||
// Click on p2's video
|
||||
await p3.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
// The avatar should be on large video and display name instead of an avatar, local video displayed
|
||||
await p3.driver.waitUntil(
|
||||
async () => await p3.getLargeVideo().getResource() === p2EndpointId, {
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Large video did not switch to ${p2.name}`
|
||||
});
|
||||
|
||||
await p3.assertDisplayNameVisibleOnStage(
|
||||
await p3.getFilmstrip().getRemoteDisplayName(p2EndpointId)
|
||||
);
|
||||
|
||||
await p3.assertThumbnailShowsAvatar(p1, false, false, true);
|
||||
await p3.assertThumbnailShowsAvatar(p3, true);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Unmute p1's and p2's videos
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
});
|
||||
|
||||
it('email persistence', async () => {
|
||||
let { p1 } = ctx;
|
||||
|
||||
if (p1.driver.isFirefox) {
|
||||
// strangely this test when FF is involved, missing source mapping from jvb
|
||||
// and fails with an error of: expected number of remote streams:1 in 15s for participant1
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
|
||||
await p1.hangup();
|
||||
|
||||
await ensureTwoParticipants({
|
||||
skipDisplayName: true
|
||||
});
|
||||
p1 = ctx.p1;
|
||||
|
||||
await p1.getToolbar().clickProfileButton();
|
||||
|
||||
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
|
||||
});
|
||||
});
|
||||
481
tests/specs/3way/breakoutRooms.spec.ts
Normal file
481
tests/specs/3way/breakoutRooms.spec.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
import type { ChainablePromiseElement } from 'webdriverio';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import {
|
||||
checkSubject,
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
const MAIN_ROOM_NAME = 'Main room';
|
||||
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
|
||||
const LIST_ITEM_CONTAINER = 'list-item-container';
|
||||
|
||||
describe('BreakoutRooms', () => {
|
||||
it('check support', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
if (!await ctx.p1.isBreakoutRoomsSupported()) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('add breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be no breakout rooms initially, list is sent with a small delay
|
||||
await p1.driver.pause(2000);
|
||||
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
|
||||
|
||||
// add one breakout room
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room added for p1'
|
||||
});
|
||||
|
||||
|
||||
// second participant should also see one breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room seen by p2'
|
||||
});
|
||||
});
|
||||
|
||||
it('join breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be one breakout room
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room seen by p1'
|
||||
});
|
||||
|
||||
const roomsList = await p1BreakoutRooms.getRooms();
|
||||
|
||||
expect(roomsList.length).toBe(1);
|
||||
|
||||
// join the room
|
||||
await roomsList[0].joinRoom();
|
||||
|
||||
// the participant should see the main room as the only breakout room
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].name === MAIN_ROOM_NAME;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 did not join breakout room'
|
||||
});
|
||||
|
||||
// the second participant should see one participant in the breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p2.getBreakoutRooms().getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
|
||||
});
|
||||
});
|
||||
|
||||
it('leave breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// leave room
|
||||
await p1BreakoutRooms.leaveBreakoutRoom();
|
||||
|
||||
// there should be one breakout room and that should not be the main room
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].name !== MAIN_ROOM_NAME;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 did not leave breakout room'
|
||||
});
|
||||
|
||||
// the second participant should see no participants in the breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p2.getBreakoutRooms().getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 0;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P2 is seeing p1 in the breakout room'
|
||||
});
|
||||
});
|
||||
|
||||
it('remove breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// remove the room
|
||||
await (await p1BreakoutRooms.getRooms())[0].removeRoom();
|
||||
|
||||
// there should be no breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Breakout room was not removed for p1'
|
||||
});
|
||||
|
||||
// the second participant should also see no breakout rooms
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Breakout room was not removed for p2'
|
||||
});
|
||||
});
|
||||
|
||||
it('auto assign', async () => {
|
||||
await ensureThreeParticipants();
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// create two rooms
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
|
||||
// there should be two breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Breakout room was not created by p1'
|
||||
});
|
||||
|
||||
// auto assign participants to rooms
|
||||
await p1BreakoutRooms.autoAssignToBreakoutRooms();
|
||||
|
||||
// each room should have one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1 && list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
|
||||
});
|
||||
|
||||
// the second participant should see one participant in the main room
|
||||
const p2BreakoutRooms = p2.getBreakoutRooms();
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p2BreakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p2BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1 && list[1].participantCount === 1
|
||||
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P2 is not seeing p1 in the main room'
|
||||
});
|
||||
});
|
||||
|
||||
it('close breakout room', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be two non-empty breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1 && list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P1 is not seeing two breakout rooms'
|
||||
});
|
||||
|
||||
// close the first room
|
||||
await (await p1BreakoutRooms.getRooms())[0].closeRoom();
|
||||
|
||||
// there should be two rooms and first one should be empty
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 0 || list[1].participantCount === 0;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 is not seeing an empty breakout room'
|
||||
});
|
||||
|
||||
// there should be two participants in the main room, either p2 or p3 got moved to the main room
|
||||
const checkParticipants = (p: Participant) =>
|
||||
p.driver.waitUntil(
|
||||
async () => {
|
||||
const isInBreakoutRoom = await p.isInBreakoutRoom();
|
||||
const breakoutRooms = p.getBreakoutRooms();
|
||||
|
||||
if (isInBreakoutRoom) {
|
||||
if (await breakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await breakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list.every(r => { // eslint-disable-line arrow-body-style
|
||||
return r.name === MAIN_ROOM_NAME ? r.participantCount === 2 : r.participantCount === 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (await breakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await breakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount + list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
|
||||
});
|
||||
|
||||
await checkParticipants(p2);
|
||||
await checkParticipants(p3);
|
||||
});
|
||||
|
||||
it('send participants to breakout room', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
// because the participants rejoin so fast, the meeting is not properly ended,
|
||||
// so the previous breakout rooms would still be there.
|
||||
// To avoid this issue we use a different meeting
|
||||
// Respect room name suffix as it is important in multi-shard testing
|
||||
ctx.roomName += `new-${ctx.roomName}`;
|
||||
|
||||
await ensureTwoParticipants();
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be no breakout rooms
|
||||
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
|
||||
|
||||
// add one breakout room
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
|
||||
// there should be one empty room
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1
|
||||
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room added for p1'
|
||||
});
|
||||
|
||||
// send the second participant to the first breakout room
|
||||
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, (await p1BreakoutRooms.getRooms())[0].name);
|
||||
|
||||
// there should be one room with one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
});
|
||||
});
|
||||
|
||||
it('collapse breakout room', async () => {
|
||||
const { p1 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be one breakout room with one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
});
|
||||
|
||||
// get id of the breakout room participant
|
||||
const breakoutList = p1.driver.$(`#${BREAKOUT_ROOMS_LIST_ID}`);
|
||||
const breakoutRoomItem = await breakoutList.$$(`.${LIST_ITEM_CONTAINER}`).find(
|
||||
async el => {
|
||||
const id = await el.getAttribute('id');
|
||||
|
||||
return id !== '' && id !== null;
|
||||
}) as ChainablePromiseElement;
|
||||
|
||||
const pId = await breakoutRoomItem.getAttribute('id');
|
||||
const breakoutParticipant = p1.driver.$(`//div[@id="${pId}"]`);
|
||||
|
||||
expect(await breakoutParticipant.isDisplayed()).toBe(true);
|
||||
|
||||
// collapse the first
|
||||
await (await p1BreakoutRooms.getRooms())[0].collapse();
|
||||
|
||||
// the participant should not be visible
|
||||
expect(await breakoutParticipant.isDisplayed()).toBe(false);
|
||||
|
||||
// the collapsed room should still have one participant
|
||||
expect((await p1BreakoutRooms.getRooms())[0].participantCount).toBe(1);
|
||||
});
|
||||
|
||||
it('rename breakout room', async () => {
|
||||
const myNewRoomName = `breakout-${crypto.randomUUID()}`;
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// let's rename breakout room and see it in local and remote
|
||||
await (await p1BreakoutRooms.getRooms())[0].renameRoom(myNewRoomName);
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].name === myNewRoomName;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'The breakout room was not renamed for p1'
|
||||
});
|
||||
|
||||
await checkSubject(p2, myNewRoomName);
|
||||
|
||||
const p2BreakoutRooms = p2.getBreakoutRooms();
|
||||
|
||||
// leave room
|
||||
await p2BreakoutRooms.leaveBreakoutRoom();
|
||||
|
||||
// there should be one empty room
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 0;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'The breakout room not found or not empty for p1'
|
||||
});
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p2BreakoutRooms.getRooms();
|
||||
|
||||
return list?.length === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'The breakout room not seen by p2'
|
||||
});
|
||||
|
||||
expect((await p2BreakoutRooms.getRooms())[0].name).toBe(myNewRoomName);
|
||||
|
||||
// send the second participant to the first breakout room
|
||||
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);
|
||||
|
||||
// there should be one room with one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'The breakout room was not rename for p1'
|
||||
});
|
||||
|
||||
await checkSubject(p2, myNewRoomName);
|
||||
});
|
||||
});
|
||||
128
tests/specs/3way/codecSelection.spec.ts
Normal file
128
tests/specs/3way/codecSelection.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('Codec selection', () => {
|
||||
it('asymmetric codecs', async () => {
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'VP8', 'VP9', 'AV1' ]
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// Check if media is playing on both endpoints.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
// Check if p1 is sending VP9 and p2 is sending VP8 as per their codec preferences.
|
||||
// Except on Firefox because it doesn't support VP9 encode.
|
||||
if (p1.driver.isFirefox) {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
} else {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
}
|
||||
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
});
|
||||
|
||||
it('asymmetric codecs with AV1', async () => {
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Check if media is playing on p3.
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
const majorVersion = parseInt(p1.driver.capabilities.browserVersion || '0', 10);
|
||||
|
||||
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
|
||||
// Except on Firefox because it doesn't support VP9 encode.
|
||||
if (p1.driver.isFirefox) {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
} else {
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
}
|
||||
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
|
||||
// If there is a Firefox ep in the call, all other eps will switch to VP9.
|
||||
if (p1.driver.isFirefox && majorVersion < 136) {
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
} else {
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('codec switch over', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ]
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// Disable this test on Firefox because it doesn't support VP9 encode.
|
||||
if (p1.driver.isFirefox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if p1 and p2 are encoding in VP9 which is the default codec.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
|
||||
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
videoQuality: {
|
||||
codecPreferenceOrder: [ 'VP8' ]
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p3 } = ctx;
|
||||
|
||||
// Check if all three participants are encoding in VP8 now.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Check of p1 and p2 have switched to VP9.
|
||||
await p1.driver.waitUntil(
|
||||
() => p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'p1 did not switch back to VP9'
|
||||
}
|
||||
);
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'p1 did not switch back to VP9'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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.getLargeVideo().getId(),
|
||||
{
|
||||
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.getLargeVideo().getId(),
|
||||
{
|
||||
timeout: 5_000,
|
||||
timeoutMsg: 'The pinned participant is not displayed on stage for p3'
|
||||
});
|
||||
});
|
||||
});
|
||||
503
tests/specs/3way/lobby.spec.ts
Normal file
503
tests/specs/3way/lobby.spec.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
import { P1, P3, Participant } from '../../helpers/Participant';
|
||||
import { config } from '../../helpers/TestsConfig';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
import type { IJoinOptions } from '../../helpers/types';
|
||||
import type PreMeetingScreen from '../../pageobjects/PreMeetingScreen';
|
||||
|
||||
describe('Lobby', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureOneParticipant();
|
||||
|
||||
if (!await ctx.p1.execute(() => APP.conference._room.isLobbySupported())) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('enable', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
await enableLobby();
|
||||
});
|
||||
|
||||
it('entering in lobby and approve', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await enterLobby(p1, true);
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
await p1.getNotifications().allowLobbyParticipant();
|
||||
|
||||
const notificationText = await p2.getNotifications().getLobbyParticipantAccessGranted();
|
||||
|
||||
expect(notificationText.includes(P1)).toBe(true);
|
||||
expect(notificationText.includes(P3)).toBe(true);
|
||||
|
||||
await p2.getNotifications().closeLobbyParticipantAccessGranted();
|
||||
|
||||
// ensure 3 participants in the call will check for the third one that muc is joined, ice connected,
|
||||
// media is being receiving and there are two remote streams
|
||||
await p3.waitToJoinMUC();
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForSendReceiveData();
|
||||
await p3.waitForRemoteStreams(2);
|
||||
|
||||
// now check third one display name in the room, is the one set in the prejoin screen
|
||||
const name = await p1.getFilmstrip().getRemoteDisplayName(await p3.getEndpointId());
|
||||
|
||||
expect(name).toBe(P3);
|
||||
|
||||
await p3.hangup();
|
||||
});
|
||||
|
||||
it('entering in lobby and deny', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// the first time tests is executed we need to enter display name,
|
||||
// for next execution that will be locally stored
|
||||
await enterLobby(p1, false);
|
||||
|
||||
// moderator rejects access
|
||||
await p1.getNotifications().rejectLobbyParticipant();
|
||||
|
||||
// deny notification on 2nd participant
|
||||
const notificationText = await p2.getNotifications().getLobbyParticipantAccessDenied();
|
||||
|
||||
expect(notificationText.includes(P1)).toBe(true);
|
||||
expect(notificationText.includes(P3)).toBe(true);
|
||||
|
||||
await p2.getNotifications().closeLobbyParticipantAccessDenied();
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// check the denied one is out of lobby, sees the notification about it
|
||||
await p3.getNotifications().waitForLobbyAccessDeniedNotification();
|
||||
|
||||
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
|
||||
|
||||
await p3.hangup();
|
||||
});
|
||||
|
||||
|
||||
it('approve from participants pane', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
const knockingParticipant = await enterLobby(p1, false);
|
||||
|
||||
// moderator allows access
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.open();
|
||||
await p1ParticipantsPane.admitLobbyParticipant(knockingParticipant);
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// ensure 3 participants in the call will check for the third one that muc is joined, ice connected,
|
||||
// media is being receiving and there are two remote streams
|
||||
await p3.waitToJoinMUC();
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForSendReceiveData();
|
||||
await p3.waitForRemoteStreams(2);
|
||||
|
||||
// now check third one display name in the room, is the one set in the prejoin screen
|
||||
// now check third one display name in the room, is the one set in the prejoin screen
|
||||
const name = await p1.getFilmstrip().getRemoteDisplayName(await p3.getEndpointId());
|
||||
|
||||
expect(name).toBe(P3);
|
||||
|
||||
await p3.hangup();
|
||||
});
|
||||
|
||||
it('reject from participants pane', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
const knockingParticipant = await enterLobby(p1, false);
|
||||
|
||||
// moderator rejects access
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.open();
|
||||
await p1ParticipantsPane.rejectLobbyParticipant(knockingParticipant);
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// check the denied one is out of lobby, sees the notification about it
|
||||
// The third participant should see a warning that his access to the room was denied
|
||||
await p3.getNotifications().waitForLobbyAccessDeniedNotification();
|
||||
|
||||
// check Lobby room not left
|
||||
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
|
||||
|
||||
await p3.hangup();
|
||||
});
|
||||
|
||||
it('lobby user leave', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await enterLobby(p1, false);
|
||||
|
||||
await ctx.p3.hangup();
|
||||
|
||||
// check that moderator (participant 1) no longer sees notification about participant in lobby
|
||||
await p1.getNotifications().waitForHideOfKnockingParticipants();
|
||||
});
|
||||
|
||||
it('conference ended in lobby', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await enterLobby(p1, false);
|
||||
|
||||
await p1.hangup();
|
||||
await p2.hangup();
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
await p3.driver.$('.dialog.leaveReason').isExisting();
|
||||
|
||||
await p3.driver.waitUntil(
|
||||
async () => !await p3.getLobbyScreen().isLobbyRoomJoined(),
|
||||
{
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'p2 did not leave lobby'
|
||||
}
|
||||
);
|
||||
|
||||
await p3.hangup();
|
||||
});
|
||||
|
||||
it('disable while participant in lobby', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await enableLobby();
|
||||
await enterLobby(p1);
|
||||
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await p1SecurityDialog.toggleLobby();
|
||||
await p1SecurityDialog.waitForLobbyEnabled(true);
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
await p3.waitToJoinMUC();
|
||||
|
||||
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
|
||||
});
|
||||
|
||||
it('change of moderators in lobby', async () => {
|
||||
// no moderator switching if jaas is available.
|
||||
if (config.iframe.usesJaas) {
|
||||
return;
|
||||
}
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// hanging up the first one, which is moderator and second one should be
|
||||
await p1.hangup();
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.isModerator(),
|
||||
{
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'p2 is not moderator after p1 leaves'
|
||||
}
|
||||
);
|
||||
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await p2SecurityDialog.toggleLobby();
|
||||
await p2SecurityDialog.waitForLobbyEnabled();
|
||||
|
||||
// here the important check is whether the moderator sees the knocking participant
|
||||
await enterLobby(p2, false);
|
||||
});
|
||||
|
||||
it('shared password', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await enableLobby();
|
||||
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(false);
|
||||
|
||||
const roomPasscode = String(Math.trunc(Math.random() * 1_000_000));
|
||||
|
||||
await p1SecurityDialog.addPassword(roomPasscode);
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
() => p1SecurityDialog.isLocked(),
|
||||
{
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'room did not lock for p1'
|
||||
}
|
||||
);
|
||||
|
||||
await enterLobby(p1, false);
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// now fill in password
|
||||
const lobbyScreen = p3.getLobbyScreen();
|
||||
|
||||
await lobbyScreen.enterPassword(roomPasscode);
|
||||
|
||||
await p3.waitToJoinMUC();
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForSendReceiveData();
|
||||
});
|
||||
|
||||
it('enable with more than two participants', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
|
||||
await enableLobby();
|
||||
|
||||
// we need to check remote participants as isInMuc has not changed its value as
|
||||
// the bug is triggered by presence with status 322 which is not handled correctly
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p1.waitForRemoteStreams(2);
|
||||
await p2.waitForRemoteStreams(2);
|
||||
await p3.waitForRemoteStreams(2);
|
||||
});
|
||||
|
||||
it('moderator leaves while lobby enabled', async () => {
|
||||
// no moderator switching if jaas is available.
|
||||
if (config.iframe.usesJaas) {
|
||||
return;
|
||||
}
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p3.hangup();
|
||||
await p1.hangup();
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.isModerator(),
|
||||
{
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'p2 is not moderator after p1 leaves'
|
||||
}
|
||||
);
|
||||
|
||||
const lobbyScreen = p2.getLobbyScreen();
|
||||
|
||||
expect(await lobbyScreen.isLobbyRoomJoined()).toBe(true);
|
||||
});
|
||||
|
||||
it('reject and approve in pre-join', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureTwoParticipants();
|
||||
await enableLobby();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
const knockingParticipant = await enterLobby(p1, true, true);
|
||||
|
||||
// moderator rejects access
|
||||
const p1ParticipantsPane = p1.getParticipantsPane();
|
||||
|
||||
await p1ParticipantsPane.open();
|
||||
await p1ParticipantsPane.rejectLobbyParticipant(knockingParticipant);
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// check the denied one is out of lobby, sees the notification about it
|
||||
// The third participant should see a warning that his access to the room was denied
|
||||
await p3.getNotifications().waitForLobbyAccessDeniedNotification();
|
||||
|
||||
// check Lobby room left
|
||||
expect(await p3.getLobbyScreen().isLobbyRoomJoined()).toBe(false);
|
||||
|
||||
// try again entering the lobby with the third one and approve it
|
||||
// check that everything is fine in the meeting
|
||||
await p3.getNotifications().closeLocalLobbyAccessDenied();
|
||||
|
||||
// let's retry to enter the lobby and approve this time
|
||||
const lobbyScreen = p3.getPreJoinScreen();
|
||||
|
||||
// click join button
|
||||
await lobbyScreen.getJoinButton().click();
|
||||
await lobbyScreen.waitToJoinLobby();
|
||||
|
||||
// check that moderator (participant 1) sees notification about participant in lobby
|
||||
const name = await p1.getNotifications().getKnockingParticipantName();
|
||||
|
||||
expect(name).toBe(P3);
|
||||
expect(await lobbyScreen.isLobbyRoomJoined()).toBe(true);
|
||||
|
||||
await p1ParticipantsPane.open();
|
||||
await p1ParticipantsPane.admitLobbyParticipant(knockingParticipant);
|
||||
await p1ParticipantsPane.close();
|
||||
|
||||
await p3.waitForParticipants(2);
|
||||
await p3.waitForRemoteStreams(2);
|
||||
|
||||
expect(await p3.getFilmstrip().countVisibleThumbnails()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enable lobby and check that it is enabled.
|
||||
*/
|
||||
async function enableLobby() {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLobbyEnabled()).toBe(false);
|
||||
|
||||
await p1SecurityDialog.toggleLobby();
|
||||
await p1SecurityDialog.waitForLobbyEnabled();
|
||||
|
||||
expect((await p2.getNotifications().getLobbyEnabledText()).includes(p1.name)).toBe(true);
|
||||
|
||||
await p2.getNotifications().closeLobbyEnabled();
|
||||
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
// lobby is visible to moderators only, this depends on whether deployment is all moderators or not
|
||||
if (await p2.isModerator()) {
|
||||
await p2SecurityDialog.waitForLobbyEnabled();
|
||||
} else {
|
||||
expect(await p2SecurityDialog.isLobbySectionPresent()).toBe(false);
|
||||
}
|
||||
|
||||
// let's close the security dialog, or we will not be able to click
|
||||
// on popups for allow/deny participants
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
await p2SecurityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that lobby is enabled for the room we will try to join.
|
||||
* Lobby UI is shown, enter display name and join.
|
||||
* Checks Lobby UI and also that when joining the moderator sees corresponding notifications.
|
||||
*
|
||||
* @param participant The participant that is moderator in the meeting.
|
||||
* @param enterDisplayName whether to enter display name. We need to enter display name only the first time when
|
||||
* a participant sees the lobby screen, next time visiting the page display name will be pre-filled
|
||||
* from local storage.
|
||||
* @param usePreJoin
|
||||
* @return the participant name knocking.
|
||||
*/
|
||||
async function enterLobby(participant: Participant, enterDisplayName = false, usePreJoin = false) {
|
||||
const options: IJoinOptions = { };
|
||||
|
||||
if (usePreJoin) {
|
||||
options.configOverwrite = {
|
||||
prejoinConfig: {
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await ensureThreeParticipants({
|
||||
...options,
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
let screen: PreMeetingScreen;
|
||||
|
||||
// WebParticipant participant3 = getParticipant3();
|
||||
// ParentPreMeetingScreen lobbyScreen;
|
||||
if (usePreJoin) {
|
||||
screen = p3.getPreJoinScreen();
|
||||
} else {
|
||||
screen = p3.getLobbyScreen();
|
||||
}
|
||||
|
||||
// participant 3 should be now on pre-join screen
|
||||
await screen.waitForLoading();
|
||||
|
||||
const displayNameInput = screen.getDisplayNameInput();
|
||||
|
||||
// check display name is visible
|
||||
expect(await displayNameInput.isExisting()).toBe(true);
|
||||
expect(await displayNameInput.isDisplayed()).toBe(true);
|
||||
|
||||
const joinButton = screen.getJoinButton();
|
||||
|
||||
expect(await joinButton.isExisting()).toBe(true);
|
||||
|
||||
if (enterDisplayName) {
|
||||
let classes = await joinButton.getAttribute('class');
|
||||
|
||||
if (!usePreJoin) {
|
||||
// check join button is disabled
|
||||
expect(classes.includes('disabled')).toBe(true);
|
||||
}
|
||||
|
||||
// TODO check that password is hidden as the room does not have password
|
||||
// this check needs to be added once the functionality exists
|
||||
|
||||
// enter display name
|
||||
await screen.enterDisplayName(P3);
|
||||
|
||||
// check join button is enabled
|
||||
classes = await joinButton.getAttribute('class');
|
||||
expect(classes.includes('disabled')).toBe(false);
|
||||
}
|
||||
|
||||
// click join button
|
||||
await screen.getJoinButton().click();
|
||||
await screen.waitToJoinLobby();
|
||||
|
||||
// check no join button
|
||||
await p3.driver.waitUntil(
|
||||
async () => !await joinButton.isExisting() || !await joinButton.isDisplayed() || !await joinButton.isEnabled(),
|
||||
{
|
||||
timeout: 2_000,
|
||||
timeoutMsg: 'Join button is still available for p3'
|
||||
});
|
||||
|
||||
// new screen, is password button shown
|
||||
const passwordButton = screen.getPasswordButton();
|
||||
|
||||
expect(await passwordButton.isExisting()).toBe(true);
|
||||
expect(await passwordButton.isEnabled()).toBe(true);
|
||||
|
||||
// check that moderator (participant 1) sees notification about participant in lobby
|
||||
const name = await participant.getNotifications().getKnockingParticipantName();
|
||||
|
||||
expect(name).toBe(P3);
|
||||
expect(await screen.isLobbyRoomJoined()).toBe(true);
|
||||
|
||||
return name;
|
||||
}
|
||||
81
tests/specs/3way/oneOnOne.spec.ts
Normal file
81
tests/specs/3way/oneOnOne.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import {
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
const ONE_ON_ONE_CONFIG_OVERRIDES = {
|
||||
configOverwrite: {
|
||||
disable1On1Mode: false,
|
||||
toolbarConfig: {
|
||||
timeout: 500,
|
||||
alwaysVisible: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('OneOnOne', () => {
|
||||
it('filmstrip hidden in 1on1', async () => {
|
||||
await ensureTwoParticipants(ONE_ON_ONE_CONFIG_OVERRIDES);
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await configureToolbarsToHideQuickly(p1);
|
||||
await configureToolbarsToHideQuickly(p2);
|
||||
|
||||
await p1.getFilmstrip().verifyRemoteVideosDisplay(false);
|
||||
await p2.getFilmstrip().verifyRemoteVideosDisplay(false);
|
||||
});
|
||||
|
||||
it('filmstrip visible with more than 2', async () => {
|
||||
await ensureThreeParticipants(ONE_ON_ONE_CONFIG_OVERRIDES);
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await configureToolbarsToHideQuickly(p3);
|
||||
|
||||
await p1.getFilmstrip().verifyRemoteVideosDisplay(true);
|
||||
await p2.getFilmstrip().verifyRemoteVideosDisplay(true);
|
||||
await p3.getFilmstrip().verifyRemoteVideosDisplay(true);
|
||||
});
|
||||
|
||||
it('filmstrip display when returning to 1on1', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p2.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
await p1.getFilmstrip().verifyRemoteVideosDisplay(false);
|
||||
await p2.getFilmstrip().verifyRemoteVideosDisplay(true);
|
||||
});
|
||||
|
||||
it('filmstrip visible on self view focus', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getFilmstrip().pinParticipant(p1);
|
||||
await p1.getFilmstrip().verifyRemoteVideosDisplay(true);
|
||||
|
||||
await p1.getFilmstrip().unpinParticipant(p1);
|
||||
await p1.getFilmstrip().verifyRemoteVideosDisplay(false);
|
||||
});
|
||||
|
||||
it('filmstrip hover show videos', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getFilmstrip().hoverOverLocalVideo();
|
||||
|
||||
await p1.getFilmstrip().verifyRemoteVideosDisplay(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Hangs up all participants (p1, p2 and p3)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function configureToolbarsToHideQuickly(participant: Participant): Promise<void> {
|
||||
return participant.execute(() => {
|
||||
APP.UI.dockToolbar(false);
|
||||
APP.UI.showToolbar(250);
|
||||
});
|
||||
}
|
||||
290
tests/specs/3way/startMuted.spec.ts
Normal file
290
tests/specs/3way/startMuted.spec.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureOneParticipant,
|
||||
ensureTwoParticipants,
|
||||
hangupAllParticipants,
|
||||
joinSecondParticipant,
|
||||
joinThirdParticipant,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('StartMuted', () => {
|
||||
it('checkboxes test', async () => {
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
},
|
||||
testing: {
|
||||
testMode: true,
|
||||
debugAudioLevels: true
|
||||
}
|
||||
} };
|
||||
|
||||
await ensureOneParticipant(options);
|
||||
|
||||
const { p1 } = ctx;
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
|
||||
await p1.getToolbar().clickSettingsButton();
|
||||
|
||||
const settingsDialog = p1.getSettingsDialog();
|
||||
|
||||
await settingsDialog.waitForDisplay();
|
||||
|
||||
await settingsDialog.setStartAudioMuted(true);
|
||||
await settingsDialog.setStartVideoMuted(true);
|
||||
await settingsDialog.submit();
|
||||
|
||||
// Check that p1 doesn't get muted.
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
|
||||
await joinSecondParticipant({
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
// Enable screenshare on p1.
|
||||
p1.getToolbar().clickDesktopSharingButton();
|
||||
await checkForScreensharingTile(p1, p1);
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
|
||||
await p2.waitForIceConnected();
|
||||
await p2.waitForReceiveMedia();
|
||||
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p1.waitForAudioMuted(p2, true);
|
||||
await p1.waitForRemoteVideo(p2EndpointId, true);
|
||||
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
await p2.waitForAudioMuted(p1, false);
|
||||
await p2.waitForRemoteVideo(p1EndpointId, false);
|
||||
|
||||
// Check if a remote screenshare tile is created on p2.
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
|
||||
// Enable video on p2 and check if p2 appears unmuted on p1.
|
||||
await Promise.all([
|
||||
p2.getToolbar().clickAudioUnmuteButton(), p2.getToolbar().clickVideoUnmuteButton()
|
||||
]);
|
||||
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
|
||||
|
||||
await p1.waitForAudioMuted(p2, false);
|
||||
await p1.waitForRemoteVideo(p2EndpointId, false);
|
||||
|
||||
// Add a third participant and check p3 is able to receive audio and video from p2.
|
||||
await joinThirdParticipant({
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForReceiveMedia();
|
||||
|
||||
await p3.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
|
||||
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
});
|
||||
|
||||
it('config options test', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
testing: {
|
||||
testMode: true,
|
||||
debugAudioLevels: true
|
||||
},
|
||||
startAudioMuted: 2,
|
||||
startVideoMuted: 2
|
||||
}
|
||||
};
|
||||
|
||||
await ensureOneParticipant(options);
|
||||
await joinSecondParticipant({
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
await p2.waitForIceConnected();
|
||||
await p2.waitForReceiveMedia();
|
||||
|
||||
await joinThirdParticipant({
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForReceiveMedia();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
const p2ID = await p2.getEndpointId();
|
||||
|
||||
p1.log(`Start configOptionsTest, second participant: ${p2ID}`);
|
||||
|
||||
// Participant 3 should be muted, 1 and 2 unmuted.
|
||||
await p3.getFilmstrip().assertAudioMuteIconIsDisplayed(p3);
|
||||
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p3);
|
||||
|
||||
await Promise.all([
|
||||
p1.waitForAudioMuted(p3, true),
|
||||
p2.waitForAudioMuted(p3, true)
|
||||
]);
|
||||
|
||||
await p3.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
|
||||
await p3.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
|
||||
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
|
||||
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
|
||||
|
||||
// Unmute and see if the audio works
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
p1.log('configOptionsTest, unmuted third participant');
|
||||
await p1.waitForAudioMuted(p3, false /* unmuted */);
|
||||
});
|
||||
|
||||
it('startWithVideoMuted=true can unmute', async () => {
|
||||
// Maybe disable if there is FF or Safari participant.
|
||||
|
||||
await hangupAllParticipants();
|
||||
|
||||
// Explicitly enable P2P due to a regression with unmute not updating
|
||||
// large video while in P2P.
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
},
|
||||
startWithVideoMuted: true
|
||||
}
|
||||
};
|
||||
|
||||
await ensureTwoParticipants(options);
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await Promise.all([
|
||||
p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
|
||||
p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
|
||||
]);
|
||||
|
||||
await unmuteVideoAndCheck(p2, p1);
|
||||
await p1.getLargeVideo().assertPlaying();
|
||||
});
|
||||
|
||||
it('startWithAudioMuted=true can unmute', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
testing: {
|
||||
testMode: true,
|
||||
debugAudioLevels: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await ensureTwoParticipants(options);
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, true) ]);
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, false) ]);
|
||||
});
|
||||
|
||||
it('startWithAudioVideoMuted=true can unmute', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: true,
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await ensureOneParticipant(options);
|
||||
await joinSecondParticipant({
|
||||
configOverwrite: {
|
||||
testing: {
|
||||
testMode: true,
|
||||
debugAudioLevels: true
|
||||
},
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p2.waitForIceConnected();
|
||||
await p2.waitForSendMedia();
|
||||
|
||||
await p2.waitForAudioMuted(p1, true);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
// Unmute p1's both audio and video and check on p2.
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p2.waitForAudioMuted(p1, false);
|
||||
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
await p2.getLargeVideo().assertPlaying();
|
||||
});
|
||||
|
||||
|
||||
it('test p2p JVB switch and switch back', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
// Mute p2's video just before p3 joins.
|
||||
await p2.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await joinThirdParticipant({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// Unmute p2 and check if its video is being received by p1 and p3.
|
||||
await unmuteVideoAndCheck(p2, p3);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
|
||||
|
||||
// Mute p2's video just before p3 leaves.
|
||||
await p2.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
await p2.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
// Check if p2's video is playing on p1.
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
|
||||
await p1.getLargeVideo().assertPlaying();
|
||||
});
|
||||
});
|
||||
113
tests/specs/3way/tileView.spec.ts
Normal file
113
tests/specs/3way/tileView.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
/**
|
||||
* The CSS selector for local video when outside of tile view. It should
|
||||
* be in a container separate from remote videos so remote videos can
|
||||
* scroll while local video stays docked.
|
||||
*/
|
||||
const FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR = '#filmstripLocalVideo #localVideoContainer';
|
||||
|
||||
/**
|
||||
* The CSS selector for local video tile view is enabled. It should display
|
||||
* at the end of all the other remote videos, as the last tile.
|
||||
*/
|
||||
const TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR = '.remote-videos #localVideoContainer';
|
||||
|
||||
describe('TileView', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
// TODO: implements etherpad check
|
||||
|
||||
it('pinning exits', async () => {
|
||||
await enterTileView();
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.getFilmstrip().pinParticipant(p2);
|
||||
|
||||
await p1.waitForTileViewDisplay(true);
|
||||
});
|
||||
|
||||
it('local video display', async () => {
|
||||
await enterTileView();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.driver.$(TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({ timeout: 3000 });
|
||||
await p1.driver.$(FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({
|
||||
timeout: 3000,
|
||||
reverse: true
|
||||
});
|
||||
});
|
||||
|
||||
it('can exit', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickExitTileViewButton();
|
||||
await p1.waitForTileViewDisplay(true);
|
||||
});
|
||||
|
||||
it('local video display independently from remote', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.driver.$(TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({
|
||||
timeout: 3000,
|
||||
reverse: true
|
||||
});
|
||||
await p1.driver.$(FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({ timeout: 3000 });
|
||||
});
|
||||
|
||||
it('lastN', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
if (p1.driver.isFirefox) {
|
||||
// Firefox does not support external audio file as input.
|
||||
// Not testing as second participant cannot be dominant speaker.
|
||||
return;
|
||||
}
|
||||
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
channelLastN: 1,
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
// one inactive icon should appear in few seconds
|
||||
await p3.waitForNinjaIcon();
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
|
||||
await p3.waitForRemoteVideo(p1EpId);
|
||||
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
await p3.waitForNinjaIcon(p2EpId);
|
||||
|
||||
// no video for participant 2
|
||||
await p3.waitForRemoteVideo(p2EpId, true);
|
||||
|
||||
// mute audio for participant 1
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// unmute audio for participant 2
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
await p3.waitForDominantSpeaker(p2EpId);
|
||||
|
||||
// check video of participant 2 should be received
|
||||
await p3.waitForRemoteVideo(p2EpId);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Attempts to enter tile view and verifies tile view has been entered.
|
||||
*/
|
||||
async function enterTileView() {
|
||||
await ctx.p1.getToolbar().clickEnterTileViewButton();
|
||||
await ctx.p1.waitForTileViewDisplay();
|
||||
}
|
||||
316
tests/specs/4way/desktopSharing.spec.ts
Normal file
316
tests/specs/4way/desktopSharing.spec.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { SET_AUDIO_ONLY } from '../../../react/features/base/audio-only/actionTypes';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureFourParticipants,
|
||||
ensureOneParticipant,
|
||||
ensureThreeParticipants,
|
||||
ensureTwoParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('Desktop sharing', () => {
|
||||
it('start', async () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Check if a remote screen share tile is created on p1.
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
|
||||
// Check if a local screen share tile is created on p2.
|
||||
await checkForScreensharingTile(p2, p2);
|
||||
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
});
|
||||
|
||||
it('stop', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p2.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
// Check if the local screen share thumbnail disappears on p2.
|
||||
await checkForScreensharingTile(p2, p2, true);
|
||||
|
||||
// Check if the remote screen share thumbnail disappears on p1.
|
||||
await checkForScreensharingTile(p1, p2, true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensures screen share is still visible when the call switches from p2p to jvb connection.
|
||||
*/
|
||||
it('p2p to jvb switch', async () => {
|
||||
await ctx.p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Check if a remote screen share tile is created on all participants.
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
await checkForScreensharingTile(p2, p2);
|
||||
await checkForScreensharingTile(p2, p2);
|
||||
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure screen share is still visible when the call switches from jvb to p2p and back.
|
||||
*/
|
||||
it('p2p to jvb switch and back', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Check if a remote screen share tile is created on p1 and p2 after switching back to p2p.
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
await checkForScreensharingTile(p2, p2);
|
||||
|
||||
// The video should be playing.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
// Start desktop share on p1.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Check if a new tile for p1's screen share is created on both p1 and p2.
|
||||
await checkForScreensharingTile(p1, p1);
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
|
||||
await ensureThreeParticipants();
|
||||
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
await checkForScreensharingTile(p2, p3);
|
||||
|
||||
// The large video should be playing on p3.
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that screen share is still visible in jvb connection when share is toggled while the users are
|
||||
* in p2p mode, i.e., share is restarted when user is in p2p mode and then the call switches over to jvb mode.
|
||||
*/
|
||||
it('stop screen sharing and back', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Stop share on both p1 and p2.
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
await p2.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Start share on both p1 and p2.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Check if p1 and p2 can see each other's shares in p2p.
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
|
||||
// Add p3 back to the conference and check if p1 and p2's shares are visible on p3.
|
||||
await ensureThreeParticipants();
|
||||
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
await checkForScreensharingTile(p2, p3);
|
||||
|
||||
// The large video should be playing on p3.
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensures screen share is visible when a muted screen share track is added to the conference, i.e.,
|
||||
* users starts and stops the share before anyone else joins the call.
|
||||
* The call switches to jvb and then back to p2p.
|
||||
*/
|
||||
it('screen sharing toggle before others join', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1 } = ctx;
|
||||
|
||||
// p1 starts share when alone in the call.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
await checkForScreensharingTile(p1, p1);
|
||||
|
||||
// p1 stops share.
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
// Call switches to jvb.
|
||||
await ensureThreeParticipants();
|
||||
const { p2, p3 } = ctx;
|
||||
|
||||
// p1 starts share again when call switches to jvb.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Check p2 and p3 are able to see p1's share.
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
// p3 leaves the call.
|
||||
await p3.hangup();
|
||||
|
||||
// Make sure p2 see's p1's share after the call switches back to p2p.
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
// p2 starts share when in p2p.
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Makes sure p2's share is visible on p1.
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* A case where a non-dominant speaker is sharing screen for a participant in low bandwidth mode
|
||||
* where only a screen share can be received. A bug fixed in jvb 0c5dd91b where the video was not received.
|
||||
*/
|
||||
it('audio only and non dominant screen share', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureOneParticipant();
|
||||
const { p1 } = ctx;
|
||||
|
||||
// a workaround to directly set audio only mode without going through the rest of the settings in the UI
|
||||
await p1.execute(type => {
|
||||
APP?.store?.dispatch({
|
||||
type,
|
||||
audioOnly: true
|
||||
});
|
||||
APP?.conference?.onToggleAudioOnly();
|
||||
}, SET_AUDIO_ONLY);
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await ensureThreeParticipants({
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
const { p2, p3 } = ctx;
|
||||
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
await p3.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await checkForScreensharingTile(p3, p1);
|
||||
await checkForScreensharingTile(p3, p2);
|
||||
|
||||
// the video should be playing
|
||||
await p1.driver.waitUntil(() => p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
|
||||
timeout: 5_000,
|
||||
timeoutMsg: 'expected remote screen share to be on large'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A case where first participant is muted (a&v) and enters low bandwidth mode,
|
||||
* the second one is audio muted only and the one sharing (the third) is dominant speaker.
|
||||
* A problem fixed in jitsi-meet 3657c19e and d6ab0a72.
|
||||
*/
|
||||
it('audio only and dominant screen share', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: true
|
||||
}
|
||||
});
|
||||
const { p1 } = ctx;
|
||||
|
||||
// a workaround to directly set audio only mode without going through the rest of the settings in the UI
|
||||
await p1.execute(type => {
|
||||
APP?.store?.dispatch({
|
||||
type,
|
||||
audioOnly: true
|
||||
});
|
||||
APP?.conference?.onToggleAudioOnly();
|
||||
}, SET_AUDIO_ONLY);
|
||||
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
},
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
await ensureThreeParticipants({
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
const { p2, p3 } = ctx;
|
||||
|
||||
await p3.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await checkForScreensharingTile(p3, p1);
|
||||
await checkForScreensharingTile(p3, p2);
|
||||
|
||||
// The desktop sharing participant should be on large
|
||||
expect(await p1.getLargeVideo().getResource()).toBe(`${await p3.getEndpointId()}-v1`);
|
||||
|
||||
// the video should be playing
|
||||
await p1.driver.waitUntil(() => p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
|
||||
timeout: 5_000,
|
||||
timeoutMsg: 'expected remote screen share to be on large'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test screensharing with lastN. We add p4 with lastN=2 and verify that it receives the expected streams.
|
||||
*/
|
||||
it('with lastN', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p3.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await ensureFourParticipants({
|
||||
configOverwrite: {
|
||||
channelLastN: 2,
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p4 } = ctx;
|
||||
|
||||
// We now have p1, p2, p3, p4.
|
||||
// p3 is screensharing.
|
||||
// p1, p3, p4 are audio muted, so p2 should eventually become dominant speaker.
|
||||
// Participants should display p3 on-stage because it is screensharing.
|
||||
await checkForScreensharingTile(p3, p1);
|
||||
await checkForScreensharingTile(p3, p2);
|
||||
await checkForScreensharingTile(p3, p4);
|
||||
|
||||
// And the video should be playing
|
||||
expect(await p4.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
const p1EndpointId = await p1.getEndpointId();
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
|
||||
// p4 has lastN=2 and has selected p3. With p2 being dominant speaker p4 should eventually
|
||||
// see video for [p3, p2] and p1 as ninja.
|
||||
await p4.waitForNinjaIcon(p1EndpointId);
|
||||
await p4.waitForRemoteVideo(p2EndpointId);
|
||||
|
||||
// Let's switch and check, muting participant 2 and unmuting 1 will leave participant 1 as dominant
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// Participant4 should eventually see video for [p3, p1] and p2 as a ninja.
|
||||
await p4.waitForNinjaIcon(p2EndpointId);
|
||||
await p4.waitForRemoteVideo(p1EndpointId);
|
||||
});
|
||||
});
|
||||
|
||||
73
tests/specs/4way/lastN.spec.ts
Normal file
73
tests/specs/4way/lastN.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ensureFourParticipants, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('lastN', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: true,
|
||||
channelLastN: 1
|
||||
},
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
channelLastN: 1
|
||||
},
|
||||
skipInMeetingChecks: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('checks', async () => {
|
||||
const { p3 } = ctx;
|
||||
const p3Toolbar = p3.getToolbar();
|
||||
|
||||
await p3.waitForSendMedia();
|
||||
|
||||
await ctx.p1.waitForRemoteVideo(await p3.getEndpointId());
|
||||
|
||||
// Mute audio on participant3.
|
||||
await p3Toolbar.clickAudioMuteButton();
|
||||
|
||||
await ensureFourParticipants({
|
||||
configOverwrite: {
|
||||
channelLastN: 1
|
||||
},
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p1, p2, p4 } = ctx;
|
||||
|
||||
await p4.waitForSendReceiveData();
|
||||
|
||||
// Mute audio on p4 and unmute p3.
|
||||
await p4.getToolbar().clickAudioMuteButton();
|
||||
await p3Toolbar.clickAudioUnmuteButton();
|
||||
|
||||
const p4EndpointId = await p4.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// Check if p1 starts receiving video from p3 and p4 shows up as ninja.
|
||||
await p1.waitForNinjaIcon(p4EndpointId);
|
||||
await p1.waitForRemoteVideo(p3EndpointId);
|
||||
|
||||
// At this point, mute video of p3 and others should be receiving p4's video.
|
||||
// Mute p1's video
|
||||
await p3Toolbar.clickVideoMuteButton();
|
||||
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p3);
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p3);
|
||||
await p1.waitForRemoteVideo(p4EndpointId);
|
||||
|
||||
// Unmute p3's video and others should switch to receiving p3's video.
|
||||
await p3Toolbar.clickVideoUnmuteButton();
|
||||
await p1.waitForRemoteVideo(p3EndpointId);
|
||||
await p1.waitForNinjaIcon(p4EndpointId);
|
||||
|
||||
// Mute p3's audio and unmute p2's audio. Other endpoints should continue to receive video from p3
|
||||
// even though p2 is the dominant speaker.
|
||||
await p3Toolbar.clickAudioMuteButton();
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForRemoteVideo(p3EndpointId);
|
||||
});
|
||||
});
|
||||
33
tests/specs/alone/chatPanel.spec.ts
Normal file
33
tests/specs/alone/chatPanel.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
describe('Chat Panel', () => {
|
||||
it('join participant', () => ensureOneParticipant());
|
||||
|
||||
it('start closed', async () => {
|
||||
expect(await ctx.p1.getChatPanel().isOpen()).toBe(false);
|
||||
});
|
||||
it('open', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickChatButton();
|
||||
expect(await p1.getChatPanel().isOpen()).toBe(true);
|
||||
});
|
||||
it('use shortcut to close', async () => {
|
||||
const chatPanel = ctx.p1.getChatPanel();
|
||||
|
||||
await chatPanel.pressShortcut();
|
||||
expect(await chatPanel.isOpen()).toBe(false);
|
||||
});
|
||||
it('use shortcut to open', async () => {
|
||||
const chatPanel = ctx.p1.getChatPanel();
|
||||
|
||||
await chatPanel.pressShortcut();
|
||||
expect(await chatPanel.isOpen()).toBe(true);
|
||||
});
|
||||
it('use button to open', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickCloseChatButton();
|
||||
expect(await p1.getChatPanel().isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
59
tests/specs/alone/dialInAudio.spec.ts
Normal file
59
tests/specs/alone/dialInAudio.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import { cleanup, dialIn, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
|
||||
|
||||
describe('Dial-In', () => {
|
||||
it('join participant', async () => {
|
||||
// check rest url is configured
|
||||
if (!process.env.DIAL_IN_REST_URL) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureOneParticipant({ preferGenerateToken: true });
|
||||
|
||||
// check dial-in is enabled
|
||||
if (!await isDialInEnabled(ctx.p1)) {
|
||||
ctx.skipSuiteTests = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('retrieve pin', async () => {
|
||||
let dialInPin: string;
|
||||
|
||||
try {
|
||||
dialInPin = await ctx.p1.getDialInPin();
|
||||
} catch (e) {
|
||||
console.error('dial-in.test.no-pin');
|
||||
ctx.skipSuiteTests = true;
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (dialInPin.length === 0) {
|
||||
console.error('dial-in.test.no-pin');
|
||||
ctx.skipSuiteTests = true;
|
||||
throw new Error('no pin');
|
||||
}
|
||||
|
||||
expect(dialInPin.length >= 8).toBe(true);
|
||||
});
|
||||
|
||||
it('invite dial-in participant', async () => {
|
||||
await dialIn(ctx.p1);
|
||||
});
|
||||
|
||||
it('wait for audio from dial-in participant', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (!await p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await cleanup(p1);
|
||||
});
|
||||
});
|
||||
85
tests/specs/alone/invite.spec.ts
Normal file
85
tests/specs/alone/invite.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import { isDialInEnabled } from '../helpers/DialIn';
|
||||
|
||||
describe('Invite', () => {
|
||||
it('join participant', () => ensureOneParticipant({ preferGenerateToken: true }));
|
||||
|
||||
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(10000);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
46
tests/specs/alone/lockRoomDigitsOnly.spec.ts
Normal file
46
tests/specs/alone/lockRoomDigitsOnly.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
/**
|
||||
* Tests that the digits only password feature works.
|
||||
*
|
||||
* 1. Lock the room with a string (shouldn't work)
|
||||
* 2. Lock the room with a valid numeric password (should work)
|
||||
*/
|
||||
describe('Lock Room with Digits only', () => {
|
||||
it('join participant', () => ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
roomPasswordNumberOfDigits: 5
|
||||
}
|
||||
}));
|
||||
|
||||
it('lock room with digits only', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
expect(await p1.execute(
|
||||
() => APP.store.getState()['features/base/config'].roomPasswordNumberOfDigits === 5)).toBe(true);
|
||||
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(false);
|
||||
|
||||
// Set a non-numeric password.
|
||||
await p1SecurityDialog.addPassword('AAAAA');
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(false);
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await p1SecurityDialog.addPassword('12345');
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
expect(await p1SecurityDialog.isLocked()).toBe(true);
|
||||
});
|
||||
});
|
||||
27
tests/specs/alone/videoLayout.spec.ts
Normal file
27
tests/specs/alone/videoLayout.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
describe('Video Layout', () => {
|
||||
it('join participant', () => ensureOneParticipant());
|
||||
|
||||
it('check', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
const innerWidth = parseInt(await p1.execute('return window.innerWidth'), 10);
|
||||
const innerHeight = parseInt(await p1.execute('return window.innerHeight'), 10);
|
||||
|
||||
const largeVideo = p1.driver.$('//div[@id="largeVideoContainer"]');
|
||||
const filmstrip = p1.driver.$('//div[contains(@class, "filmstrip")]');
|
||||
let filmstripWidth;
|
||||
|
||||
if (!await filmstrip.isExisting() || !await filmstrip.isDisplayed()) {
|
||||
filmstripWidth = 0;
|
||||
} else {
|
||||
filmstripWidth = await filmstrip.getSize('width');
|
||||
}
|
||||
|
||||
const largeVideoSize = await largeVideo.getSize();
|
||||
|
||||
expect((largeVideoSize.width === (innerWidth - filmstripWidth)) || (largeVideoSize.height === innerHeight))
|
||||
.toBe(true);
|
||||
});
|
||||
});
|
||||
90
tests/specs/helpers/DialIn.ts
Normal file
90
tests/specs/helpers/DialIn.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import https from 'node:https';
|
||||
import process from 'node:process';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
|
||||
/**
|
||||
* Helper functions for dial-in related operations.
|
||||
* To be able to create a fake dial-in test that will run most of the logic for the real dial-in test.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Waits for the audio from the dial-in participant.
|
||||
* @param participant
|
||||
*/
|
||||
export async function waitForAudioFromDialInParticipant(participant: Participant) {
|
||||
// waits 15 seconds for the participant to join
|
||||
await participant.waitForParticipants(1, `dial-in.test.jigasi.participant.no.join.for:${
|
||||
ctx.times.restAPIExecutionTS + 15_000} ms.`);
|
||||
|
||||
const joinedTS = performance.now();
|
||||
|
||||
console.log(`dial-in.test.jigasi.participant.join.after:${joinedTS - ctx.times.restAPIExecutionTS}`);
|
||||
|
||||
await participant.waitForIceConnected();
|
||||
await participant.waitForRemoteStreams(1);
|
||||
|
||||
await participant.waitForSendReceiveData(20_000, 'dial-in.test.jigasi.participant.no.audio.after.join');
|
||||
console.log(`dial-in.test.jigasi.participant.received.audio.after.join:${performance.now() - joinedTS} ms.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the dial-in participant by kicking it if the local participant is a moderator.
|
||||
* @param participant
|
||||
*/
|
||||
export async function cleanup(participant: Participant) {
|
||||
// cleanup
|
||||
if (await participant.isModerator()) {
|
||||
const jigasiEndpointId = await participant.execute(() => APP?.conference?.listMembers()[0].getId());
|
||||
|
||||
await participant.getFilmstrip().kickParticipant(jigasiEndpointId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the dial-in is enabled.
|
||||
* @param participant
|
||||
*/
|
||||
export async function isDialInEnabled(participant: Participant) {
|
||||
return await participant.execute(() => Boolean(
|
||||
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the REST API to dial in the participant using the provided pin.
|
||||
* @param participant
|
||||
*/
|
||||
export async function dialIn(participant: Participant) {
|
||||
if (!await participant.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
const dialInPin = await participant.getDialInPin();
|
||||
|
||||
const restUrl = process.env.DIAL_IN_REST_URL?.replace('{0}', dialInPin);
|
||||
|
||||
// we have already checked in the first test that DIAL_IN_REST_URL exist so restUrl cannot be ''
|
||||
const responseData: string = await new Promise((resolve, reject) => {
|
||||
https.get(restUrl || '', res => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
ctx.times.restAPIExecutionTS = performance.now();
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
}).on('error', err => {
|
||||
console.error('dial-in.test.restAPI.request.fail');
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`dial-in.test.call_session_history_id:${JSON.parse(responseData).call_session_history_id}`);
|
||||
console.log(`API response:${responseData}`);
|
||||
}
|
||||
62
tests/specs/helpers/jaas.ts
Normal file
62
tests/specs/helpers/jaas.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { config } from '../../helpers/TestsConfig';
|
||||
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
|
||||
import { IParticipantJoinOptions } from '../../helpers/types';
|
||||
|
||||
export function generateJaasToken(options: ITokenOptions): IToken {
|
||||
if (!config.jaas.enabled) {
|
||||
throw new Error('JaaS is not configured.');
|
||||
}
|
||||
|
||||
// Don't override the keyId and keyPath if they are already set in options, allow tests to set them.
|
||||
return generateToken({
|
||||
...options,
|
||||
keyId: options.keyId || config.jaas.kid,
|
||||
keyPath: options.keyPath || config.jaas.privateKeyPath
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Participant and joins the MUC with the given options. The jaas-specific properties must be set as
|
||||
* environment variables (see env.example and TestsConfig.ts). If no room name is specified, the default room name
|
||||
* from the context is used.
|
||||
*
|
||||
* @param instanceId This is the "name" passed to the Participant, I think it's used to match against one of the
|
||||
* pre-configured browser instances in wdio? It must be one of 'p1', 'p2', 'p3', or 'p4'. TODO: figure out how this
|
||||
* should be used.
|
||||
* @param token the token to use, if any.
|
||||
* @param joinOptions options to use when joining the MUC.
|
||||
* @returns {Promise<Participant>} The Participant that has joined the MUC.
|
||||
*/
|
||||
export async function joinMuc(
|
||||
instanceId: 'p1' | 'p2' | 'p3' | 'p4',
|
||||
token?: IToken,
|
||||
joinOptions?: Partial<IParticipantJoinOptions>): Promise<Participant> {
|
||||
|
||||
if (!config.jaas.enabled) {
|
||||
throw new Error('JaaS is not configured.');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const p = ctx[instanceId] as Participant;
|
||||
|
||||
if (p) {
|
||||
// Load a blank page to make sure the page is reloaded (in case the new participant uses the same URL). Using
|
||||
// 'about:blank' was causing problems in the past, if we notice any issues we can change to "base.html".
|
||||
await p.driver.url('about:blank');
|
||||
}
|
||||
|
||||
const newParticipant = new Participant({
|
||||
name: instanceId,
|
||||
token
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
ctx[instanceId] = newParticipant;
|
||||
|
||||
return await newParticipant.joinConference({
|
||||
...joinOptions,
|
||||
forceTenant: config.jaas.tenant,
|
||||
roomName: joinOptions?.roomName || ctx.roomName,
|
||||
});
|
||||
}
|
||||
198
tests/specs/iframe/chat.spec.ts
Normal file
198
tests/specs/iframe/chat.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { expect } from '@wdio/globals';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
import { fetchJson } from '../../helpers/utils';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Chat', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// let's populate endpoint ids
|
||||
await Promise.all([
|
||||
p1.getEndpointId(),
|
||||
p2.getEndpointId()
|
||||
]);
|
||||
});
|
||||
|
||||
it('send message', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
await p2.getIframeAPI().addEventListener('chatUpdated');
|
||||
await p2.getIframeAPI().addEventListener('incomingMessage');
|
||||
await p1.getIframeAPI().addEventListener('outgoingMessage');
|
||||
|
||||
const testMessage = 'Hello world';
|
||||
|
||||
await p1.getIframeAPI().executeCommand('sendChatMessage', testMessage);
|
||||
|
||||
const chatUpdatedEvent: {
|
||||
isOpen: boolean;
|
||||
unreadCount: number;
|
||||
} = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('chatUpdated'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Chat was not updated'
|
||||
});
|
||||
|
||||
expect(chatUpdatedEvent).toEqual({
|
||||
isOpen: false,
|
||||
unreadCount: 1
|
||||
});
|
||||
|
||||
const incomingMessageEvent: {
|
||||
from: string;
|
||||
message: string;
|
||||
nick: string;
|
||||
privateMessage: boolean;
|
||||
} = await p2.getIframeAPI().getEventResult('incomingMessage');
|
||||
|
||||
expect(incomingMessageEvent).toEqual({
|
||||
from: await p1.getEndpointId(),
|
||||
message: testMessage,
|
||||
nick: p1.name,
|
||||
privateMessage: false
|
||||
});
|
||||
|
||||
const outgoingMessageEvent: {
|
||||
message: string;
|
||||
privateMessage: boolean;
|
||||
} = await p1.getIframeAPI().getEventResult('outgoingMessage');
|
||||
|
||||
expect(outgoingMessageEvent).toEqual({
|
||||
message: testMessage,
|
||||
privateMessage: false
|
||||
});
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('outgoingMessage');
|
||||
await p2.getIframeAPI().clearEventResults('chatUpdated');
|
||||
await p2.getIframeAPI().clearEventResults('incomingMessage');
|
||||
});
|
||||
|
||||
it('toggle chat', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p2.getIframeAPI().executeCommand('toggleChat');
|
||||
|
||||
await testSendGroupMessageWithChatOpen(p1, p2);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('outgoingMessage');
|
||||
await p2.getIframeAPI().clearEventResults('chatUpdated');
|
||||
await p2.getIframeAPI().clearEventResults('incomingMessage');
|
||||
});
|
||||
|
||||
it('private chat', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const testMessage = 'Hello private world!';
|
||||
const p2Id = await p2.getEndpointId();
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('initiatePrivateChat', p2Id);
|
||||
await p1.getIframeAPI().executeCommand('sendChatMessage', testMessage, p2Id);
|
||||
|
||||
const incomingMessageEvent = await p2.driver.waitUntil(
|
||||
() => p2.getIframeAPI().getEventResult('incomingMessage'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Chat was not received'
|
||||
});
|
||||
|
||||
expect(incomingMessageEvent).toEqual({
|
||||
from: p1Id,
|
||||
message: testMessage,
|
||||
nick: p1.name,
|
||||
privateMessage: true
|
||||
});
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('outgoingMessage')).toEqual({
|
||||
message: testMessage,
|
||||
privateMessage: true
|
||||
});
|
||||
|
||||
await p1.getIframeAPI().executeCommand('cancelPrivateChat');
|
||||
|
||||
await p2.getIframeAPI().clearEventResults('chatUpdated');
|
||||
await p2.getIframeAPI().clearEventResults('incomingMessage');
|
||||
|
||||
await testSendGroupMessageWithChatOpen(p1, p2);
|
||||
});
|
||||
|
||||
it('chat upload chat', async () => {
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
if (webhooksProxy) {
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('CHAT_UPLOADED');
|
||||
|
||||
expect('CHAT_UPLOADED').toBe(event.eventType);
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
|
||||
const uploadedChat: any = await fetchJson(event.data.preAuthenticatedLink);
|
||||
|
||||
expect(uploadedChat.messageType).toBe('CHAT');
|
||||
expect(uploadedChat.messages).toBeDefined();
|
||||
expect(uploadedChat.messages.length).toBe(3);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test sending a group message with the chat open.
|
||||
* @param p1
|
||||
* @param p2
|
||||
*/
|
||||
async function testSendGroupMessageWithChatOpen(p1: Participant, p2: Participant) {
|
||||
const testMessage = 'Hello world again';
|
||||
|
||||
await p1.getIframeAPI().executeCommand('sendChatMessage', testMessage);
|
||||
|
||||
const chatUpdatedEvent: {
|
||||
isOpen: boolean;
|
||||
unreadCount: number;
|
||||
} = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('chatUpdated'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Chat was not updated'
|
||||
});
|
||||
|
||||
expect(chatUpdatedEvent).toEqual({
|
||||
isOpen: true,
|
||||
unreadCount: 0
|
||||
});
|
||||
|
||||
const incomingMessageEvent = await p2.driver.waitUntil(
|
||||
() => p2.getIframeAPI().getEventResult('incomingMessage'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Chat was not received'
|
||||
});
|
||||
|
||||
expect(incomingMessageEvent).toEqual({
|
||||
from: await p1.getEndpointId(),
|
||||
message: testMessage,
|
||||
nick: p1.name,
|
||||
privateMessage: false
|
||||
});
|
||||
}
|
||||
214
tests/specs/iframe/invite.spec.ts
Normal file
214
tests/specs/iframe/invite.spec.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import {
|
||||
cleanup,
|
||||
dialIn,
|
||||
isDialInEnabled,
|
||||
waitForAudioFromDialInParticipant
|
||||
} from '../helpers/DialIn';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
const customerId = testsConfig.iframe.customerId;
|
||||
|
||||
describe('Invite iframeAPI', () => {
|
||||
let dialInDisabled: boolean;
|
||||
let dialOutDisabled: boolean;
|
||||
let sipJibriDisabled: boolean;
|
||||
|
||||
it('join participant', async () => {
|
||||
await ensureOneParticipant();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
// check for dial-in dial-out sip-jibri maybe
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dialOutDisabled = Boolean(!await p1.execute(() => config.dialOutAuthUrl));
|
||||
sipJibriDisabled = Boolean(!await p1.execute(() => config.inviteServiceUrl));
|
||||
|
||||
// check dial-in is enabled
|
||||
if (!await isDialInEnabled(ctx.p1) || !process.env.DIAL_IN_REST_URL) {
|
||||
dialInDisabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
it('dial-in', async () => {
|
||||
if (dialInDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { p1 } = ctx;
|
||||
const dialInPin = await p1.getDialInPin();
|
||||
|
||||
expect(dialInPin.length >= 8).toBe(true);
|
||||
|
||||
await dialIn(p1);
|
||||
|
||||
if (!await p1.isInMuc()) {
|
||||
// local participant did not join abort
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await checkDialEvents(p1, 'in', 'DIAL_IN_STARTED', 'DIAL_IN_ENDED');
|
||||
});
|
||||
|
||||
it('dial-out', async () => {
|
||||
if (dialOutDisabled || !process.env.DIAL_OUT_URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
await p1.getIframeAPI().invitePhone(process.env.DIAL_OUT_URL);
|
||||
|
||||
await p1.switchInPage();
|
||||
|
||||
await p1.waitForParticipants(1);
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
await checkDialEvents(p1, 'out', 'DIAL_OUT_STARTED', 'DIAL_OUT_ENDED');
|
||||
});
|
||||
|
||||
it('sip jibri', async () => {
|
||||
if (sipJibriDisabled || !process.env.SIP_JIBRI_DIAL_OUT_URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
await p1.getIframeAPI().inviteSIP(process.env.SIP_JIBRI_DIAL_OUT_URL);
|
||||
|
||||
await p1.switchInPage();
|
||||
|
||||
await p1.waitForParticipants(1);
|
||||
|
||||
await waitForAudioFromDialInParticipant(p1);
|
||||
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
const sipCallOutStartedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
sipAddress: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_STARTED');
|
||||
|
||||
expect('SIP_CALL_OUT_STARTED').toBe(sipCallOutStartedEvent.eventType);
|
||||
expect(sipCallOutStartedEvent.data.sipAddress).toBe(`sip:${process.env.SIP_JIBRI_DIAL_OUT_URL}`);
|
||||
expect(sipCallOutStartedEvent.customerId).toBe(customerId);
|
||||
|
||||
const participantId = sipCallOutStartedEvent.data.participantId;
|
||||
const participantJid = sipCallOutStartedEvent.data.participantJid;
|
||||
const participantFullJid = sipCallOutStartedEvent.data.participantFullJid;
|
||||
|
||||
await cleanup(p1);
|
||||
|
||||
const sipCallOutEndedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
direction: string;
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_ENDED');
|
||||
|
||||
expect('SIP_CALL_OUT_ENDED').toBe(sipCallOutEndedEvent.eventType);
|
||||
expect(sipCallOutEndedEvent.customerId).toBe(customerId);
|
||||
expect(sipCallOutEndedEvent.data.participantFullJid).toBe(participantFullJid);
|
||||
expect(sipCallOutEndedEvent.data.participantId).toBe(participantId);
|
||||
expect(sipCallOutEndedEvent.data.participantJid).toBe(participantJid);
|
||||
} else {
|
||||
await cleanup(p1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks the dial events for a participant and clean up at the end.
|
||||
* @param participant
|
||||
* @param startedEventName
|
||||
* @param endedEventName
|
||||
* @param direction
|
||||
*/
|
||||
async function checkDialEvents(participant: Participant, direction: string, startedEventName: string, endedEventName: string) {
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
const dialInStartedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
direction: string;
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent(startedEventName);
|
||||
|
||||
expect(startedEventName).toBe(dialInStartedEvent.eventType);
|
||||
expect(dialInStartedEvent.data.direction).toBe(direction);
|
||||
expect(dialInStartedEvent.customerId).toBe(customerId);
|
||||
|
||||
const participantId = dialInStartedEvent.data.participantId;
|
||||
const participantJid = dialInStartedEvent.data.participantJid;
|
||||
const participantFullJid = dialInStartedEvent.data.participantFullJid;
|
||||
|
||||
const usageEvent: {
|
||||
customerId: string;
|
||||
data: any;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('USAGE');
|
||||
|
||||
expect('USAGE').toBe(usageEvent.eventType);
|
||||
expect(usageEvent.customerId).toBe(customerId);
|
||||
|
||||
expect(usageEvent.data.some((el: any) =>
|
||||
el.participantId === participantId && el.callDirection === direction)).toBe(true);
|
||||
|
||||
await cleanup(participant);
|
||||
|
||||
const dialInEndedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
direction: string;
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent(endedEventName);
|
||||
|
||||
expect(endedEventName).toBe(dialInEndedEvent.eventType);
|
||||
expect(dialInEndedEvent.customerId).toBe(customerId);
|
||||
expect(dialInEndedEvent.data.participantFullJid).toBe(participantFullJid);
|
||||
expect(dialInEndedEvent.data.participantId).toBe(participantId);
|
||||
expect(dialInEndedEvent.data.participantJid).toBe(participantJid);
|
||||
} else {
|
||||
await cleanup(participant);
|
||||
}
|
||||
}
|
||||
477
tests/specs/iframe/participantsPresence.spec.ts
Normal file
477
tests/specs/iframe/participantsPresence.spec.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { P1, P2, Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests PARTICIPANT_LEFT webhook.
|
||||
*/
|
||||
async function checkParticipantLeftHook(p: Participant, reason: string, checkId = false, conferenceJid: string) {
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_LEFT webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
customerId: string;
|
||||
data: {
|
||||
conference: string;
|
||||
disconnectReason: string;
|
||||
group: string;
|
||||
id: string;
|
||||
isBreakout: boolean;
|
||||
name: string;
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect('PARTICIPANT_LEFT').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.disconnectReason).toBe(reason);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.participantId).toBe(await p.getEndpointId());
|
||||
expect(event.data.name).toBe(p.name);
|
||||
|
||||
if (checkId) {
|
||||
const jwtPayload = p.getToken()?.payload;
|
||||
|
||||
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
|
||||
expect(event.data.group).toBe(jwtPayload?.context?.group);
|
||||
expect(event.customerId).toBe(testsConfig.iframe.customerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Participants presence', () => {
|
||||
let conferenceJid: string = '';
|
||||
|
||||
it('joining the meeting', async () => {
|
||||
// ensure 2 participants one moderator and one guest, we will load both with iframeAPI
|
||||
await ensureTwoParticipants();
|
||||
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// let's populate endpoint ids
|
||||
await Promise.all([
|
||||
p1.getEndpointId(),
|
||||
p2.getEndpointId()
|
||||
]);
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p2.getIframeAPI().getEventResult('isModerator')).toBe(false);
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
|
||||
if (webhooksProxy) {
|
||||
// USAGE webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: [
|
||||
{ participantId: string; }
|
||||
];
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('USAGE');
|
||||
|
||||
expect('USAGE').toBe(event.eventType);
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
expect(event.data.filter(d => d.participantId === p1EpId
|
||||
|| d.participantId === p2EpId).length).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
it('participants info',
|
||||
async () => {
|
||||
const { p1, roomName, webhooksProxy } = ctx;
|
||||
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
|
||||
|
||||
expect(roomsInfo).toBeDefined();
|
||||
expect(roomsInfo.isMainRoom).toBe(true);
|
||||
|
||||
expect(roomsInfo.id).toBeDefined();
|
||||
const { node: roomNode } = parseJid(roomsInfo.id);
|
||||
|
||||
expect(roomNode).toBe(roomName);
|
||||
|
||||
const { node, resource } = parseJid(roomsInfo.jid);
|
||||
|
||||
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
|
||||
expect(node).toBe(roomName);
|
||||
expect(resource).toBe(p1EpId);
|
||||
|
||||
expect(roomsInfo.participants.length).toBe(2);
|
||||
expect(await p1.getIframeAPI().getNumberOfParticipants()).toBe(2);
|
||||
|
||||
if (webhooksProxy) {
|
||||
// ROOM_CREATED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
|
||||
|
||||
expect('ROOM_CREATED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('participants pane', async () => {
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantsPaneToggled');
|
||||
await p1.getIframeAPI().executeCommand('toggleParticipantsPane', true);
|
||||
|
||||
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(true);
|
||||
expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(true);
|
||||
|
||||
await p1.getIframeAPI().executeCommand('toggleParticipantsPane', false);
|
||||
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
|
||||
expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(false);
|
||||
});
|
||||
|
||||
it('grant moderator', async () => {
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('participantRoleChanged');
|
||||
await p2.getIframeAPI().clearEventResults('participantRoleChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
|
||||
|
||||
await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('isModerator'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Moderator role not granted'
|
||||
});
|
||||
|
||||
type RoleChangedEvent = {
|
||||
id: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
const event1: RoleChangedEvent = await p1.driver.waitUntil(
|
||||
() => p1.getIframeAPI().getEventResult('participantRoleChanged'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Role was not update on p1 side'
|
||||
});
|
||||
|
||||
expect(event1?.id).toBe(p2EpId);
|
||||
expect(event1?.role).toBe('moderator');
|
||||
|
||||
const event2: RoleChangedEvent = await p2.driver.waitUntil(
|
||||
() => p2.getIframeAPI().getEventResult('participantRoleChanged'), {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Role was not update on p2 side'
|
||||
});
|
||||
|
||||
expect(event2?.id).toBe(p2EpId);
|
||||
expect(event2?.role).toBe('moderator');
|
||||
|
||||
if (webhooksProxy) {
|
||||
// ROLE_CHANGED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
grantedBy: {
|
||||
participantId: string;
|
||||
};
|
||||
grantedTo: {
|
||||
participantId: string;
|
||||
};
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROLE_CHANGED');
|
||||
|
||||
expect('ROLE_CHANGED').toBe(event.eventType);
|
||||
expect(event.data.role).toBe('moderator');
|
||||
expect(event.data.grantedBy.participantId).toBe(await p1.getEndpointId());
|
||||
expect(event.data.grantedTo.participantId).toBe(await p2.getEndpointId());
|
||||
}
|
||||
});
|
||||
|
||||
it('kick participant', async () => {
|
||||
// we want to join second participant with token, so we can check info in webhook
|
||||
await ctx.p2.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
await ctx.p2.switchToAPI();
|
||||
await ctx.p2.getIframeAPI().executeCommand('hangup');
|
||||
await ctx.p2.driver.waitUntil(() =>
|
||||
ctx.p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 4000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
await ensureTwoParticipants({
|
||||
preferGenerateToken: true
|
||||
});
|
||||
|
||||
const { p1, p2, roomName, webhooksProxy } = ctx;
|
||||
|
||||
webhooksProxy?.clearCache();
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
const p1DisplayName = await p1.getLocalDisplayName();
|
||||
const p2DisplayName = await p2.getLocalDisplayName();
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
|
||||
|
||||
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantKickedOut');
|
||||
await p2.getIframeAPI().addEventListener('participantKickedOut');
|
||||
|
||||
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
|
||||
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
|
||||
|
||||
const eventP1 = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantKickedOut'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'participantKickedOut event not received on p1 side'
|
||||
});
|
||||
const eventP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('participantKickedOut'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'participantKickedOut event not received on p2 side'
|
||||
});
|
||||
|
||||
await checkParticipantLeftHook(p2, 'kicked', true, conferenceJid);
|
||||
|
||||
expect(eventP1).toBeDefined();
|
||||
expect(eventP2).toBeDefined();
|
||||
|
||||
expect(isEqual(eventP1, {
|
||||
kicked: {
|
||||
id: p2EpId,
|
||||
local: false,
|
||||
name: p2DisplayName
|
||||
},
|
||||
kicker: {
|
||||
id: p1EpId,
|
||||
local: true,
|
||||
name: p1DisplayName
|
||||
}
|
||||
})).toBe(true);
|
||||
|
||||
expect(isEqual(eventP2, {
|
||||
kicked: {
|
||||
id: 'local',
|
||||
local: true,
|
||||
name: p2DisplayName
|
||||
},
|
||||
kicker: {
|
||||
id: p1EpId,
|
||||
name: p1DisplayName
|
||||
}
|
||||
})).toBe(true);
|
||||
|
||||
const eventConferenceLeftP2 = await p2.driver.waitUntil(() =>
|
||||
p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 4000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
});
|
||||
|
||||
it('join after kick', async () => {
|
||||
const { p1, webhooksProxy } = ctx;
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantJoined');
|
||||
await p1.getIframeAPI().addEventListener('participantMenuButtonClick');
|
||||
|
||||
webhooksProxy?.clearCache();
|
||||
|
||||
// join again
|
||||
await ensureTwoParticipants();
|
||||
const { p2 } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_JOINED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
moderator: boolean;
|
||||
name: string;
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.moderator).toBe(false);
|
||||
expect(event.data.name).toBe(await p2.getLocalDisplayName());
|
||||
expect(event.data.participantId).toBe(await p2.getEndpointId());
|
||||
expect(event.data.name).toBe(p2.name);
|
||||
}
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
const event = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantJoined'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'participantJoined not received'
|
||||
});
|
||||
|
||||
const p2DisplayName = await p2.getLocalDisplayName();
|
||||
|
||||
expect(event).toBeDefined();
|
||||
expect(event.id).toBe(await p2.getEndpointId());
|
||||
expect(event.displayName).toBe(p2DisplayName);
|
||||
expect(event.formattedDisplayName).toBe(p2DisplayName);
|
||||
|
||||
});
|
||||
|
||||
it('overwrite names', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
const p2EpId = await p2.getEndpointId();
|
||||
|
||||
const newP1Name = P1;
|
||||
const newP2Name = P2;
|
||||
const newNames: ({ id: string; name: string; })[] = [ {
|
||||
id: p2EpId,
|
||||
name: newP2Name
|
||||
}, {
|
||||
id: p1EpId,
|
||||
name: newP1Name
|
||||
} ];
|
||||
|
||||
await p1.getIframeAPI().executeCommand('overwriteNames', newNames);
|
||||
|
||||
await p1.switchInPage();
|
||||
|
||||
expect(await p1.getLocalDisplayName()).toBe(newP1Name);
|
||||
|
||||
expect(await p1.getFilmstrip().getRemoteDisplayName(p2EpId)).toBe(newP2Name);
|
||||
|
||||
});
|
||||
|
||||
it('hangup', async () => {
|
||||
const { p1, p2, roomName } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
|
||||
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
await p2.getIframeAPI().addEventListener('readyToClose');
|
||||
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
const eventConferenceLeftP2 = await p2.driver.waitUntil(() =>
|
||||
p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 4000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(p2, 'left', false, conferenceJid);
|
||||
|
||||
const eventReadyToCloseP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('readyToClose'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'readyToClose not received'
|
||||
});
|
||||
|
||||
expect(eventReadyToCloseP2).toBeDefined();
|
||||
});
|
||||
|
||||
it('dispose conference', async () => {
|
||||
const { p1, roomName, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('videoConferenceLeft');
|
||||
await p1.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
await p1.getIframeAPI().addEventListener('readyToClose');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
const eventConferenceLeft = await p1.driver.waitUntil(() =>
|
||||
p1.getIframeAPI().getEventResult('videoConferenceLeft'), {
|
||||
timeout: 4000,
|
||||
timeoutMsg: 'videoConferenceLeft not received'
|
||||
});
|
||||
|
||||
expect(eventConferenceLeft).toBeDefined();
|
||||
expect(eventConferenceLeft.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(p1, 'left', true, conferenceJid);
|
||||
if (webhooksProxy) {
|
||||
// ROOM_DESTROYED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
expect('ROOM_DESTROYED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
|
||||
const eventReadyToClose = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('readyToClose'), {
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'readyToClose not received'
|
||||
});
|
||||
|
||||
expect(eventReadyToClose).toBeDefined();
|
||||
|
||||
// dispose
|
||||
await p1.getIframeAPI().dispose();
|
||||
|
||||
// check there is no iframe on the page
|
||||
await p1.driver.$('iframe').waitForExist({
|
||||
reverse: true,
|
||||
timeout: 2000,
|
||||
timeoutMsg: 'iframe is still on the page'
|
||||
});
|
||||
});
|
||||
});
|
||||
207
tests/specs/iframe/recording.spec.ts
Normal file
207
tests/specs/iframe/recording.spec.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
const { tenant, customerId } = testsConfig.iframe;
|
||||
|
||||
describe('Recording', () => {
|
||||
let recordingDisabled: boolean;
|
||||
let liveStreamingDisabled: boolean;
|
||||
|
||||
it('join participant', async () => {
|
||||
await ensureOneParticipant();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
// check for dial-in dial-out sip-jibri maybe
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
recordingDisabled = Boolean(!await p1.execute(() => config.recordingService?.enabled));
|
||||
liveStreamingDisabled = Boolean(!await p1.execute(() => config.liveStreaming?.enabled))
|
||||
|| !process.env.YTUBE_TEST_STREAM_KEY;
|
||||
});
|
||||
|
||||
it('start/stop function', async () => {
|
||||
if (recordingDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await testRecordingStarted(true);
|
||||
await testRecordingStopped(true);
|
||||
|
||||
// to avoid limits
|
||||
await ctx.p1.driver.pause(30000);
|
||||
});
|
||||
|
||||
it('start/stop command', async () => {
|
||||
if (recordingDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await testRecordingStarted(false);
|
||||
await testRecordingStopped(false);
|
||||
|
||||
// to avoid limits
|
||||
await ctx.p1.driver.pause(30000);
|
||||
});
|
||||
|
||||
it('start/stop Livestreaming command', async () => {
|
||||
if (liveStreamingDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { p1, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p1.getIframeAPI().addEventListener('recordingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('startRecording', {
|
||||
youtubeBroadcastID: process.env.YTUBE_TEST_BROADCAST_ID,
|
||||
mode: 'stream',
|
||||
youtubeStreamKey: process.env.YTUBE_TEST_STREAM_KEY
|
||||
});
|
||||
|
||||
if (webhooksProxy) {
|
||||
const liveStreamEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_STARTED');
|
||||
|
||||
expect('LIVE_STREAM_STARTED').toBe(liveStreamEvent.eventType);
|
||||
expect(liveStreamEvent.customerId).toBe(customerId);
|
||||
}
|
||||
|
||||
const statusEvent = (await p1.getIframeAPI().getEventResult('recordingStatusChanged'));
|
||||
|
||||
expect(statusEvent.mode).toBe('stream');
|
||||
expect(statusEvent.on).toBe(true);
|
||||
|
||||
if (process.env.YTUBE_TEST_BROADCAST_ID) {
|
||||
const liveStreamUrl = await p1.getIframeAPI().getLivestreamUrl();
|
||||
|
||||
expect(liveStreamUrl.livestreamUrl).toBeDefined();
|
||||
}
|
||||
|
||||
await p1.getIframeAPI().executeCommand('stopRecording', 'stream');
|
||||
|
||||
if (webhooksProxy) {
|
||||
const liveStreamEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_ENDED');
|
||||
|
||||
expect('LIVE_STREAM_ENDED').toBe(liveStreamEvent.eventType);
|
||||
expect(liveStreamEvent.customerId).toBe(customerId);
|
||||
}
|
||||
|
||||
const stoppedStatusEvent = (await p1.getIframeAPI().getEventResult('recordingStatusChanged'));
|
||||
|
||||
expect(stoppedStatusEvent.mode).toBe('stream');
|
||||
expect(stoppedStatusEvent.on).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks if the recording is started.
|
||||
* @param command
|
||||
*/
|
||||
async function testRecordingStarted(command: boolean) {
|
||||
const { p1, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p1.getIframeAPI().addEventListener('recordingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('recordingLinkAvailable');
|
||||
|
||||
if (command) {
|
||||
await p1.getIframeAPI().executeCommand('startRecording', {
|
||||
mode: 'file'
|
||||
});
|
||||
} else {
|
||||
await p1.getIframeAPI().startRecording({
|
||||
mode: 'file'
|
||||
});
|
||||
}
|
||||
|
||||
if (webhooksProxy) {
|
||||
const recordingEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_STARTED');
|
||||
|
||||
expect('RECORDING_STARTED').toBe(recordingEvent.eventType);
|
||||
expect(recordingEvent.customerId).toBe(customerId);
|
||||
|
||||
webhooksProxy?.clearCache();
|
||||
}
|
||||
|
||||
const statusEvent = (await p1.getIframeAPI().getEventResult('recordingStatusChanged'));
|
||||
|
||||
expect(statusEvent.mode).toBe('file');
|
||||
expect(statusEvent.on).toBe(true);
|
||||
|
||||
const linkEvent = (await p1.getIframeAPI().getEventResult('recordingLinkAvailable'));
|
||||
|
||||
expect(linkEvent.link.startsWith('https://')).toBe(true);
|
||||
expect(linkEvent.link.includes(tenant)).toBe(true);
|
||||
expect(linkEvent.ttl > 0).toBe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the recording is stopped.
|
||||
* @param command
|
||||
*/
|
||||
async function testRecordingStopped(command: boolean) {
|
||||
const { p1, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
if (command) {
|
||||
await p1.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
} else {
|
||||
await p1.getIframeAPI().stopRecording('file');
|
||||
}
|
||||
|
||||
if (webhooksProxy) {
|
||||
const liveStreamEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_ENDED');
|
||||
|
||||
expect('RECORDING_ENDED').toBe(liveStreamEvent.eventType);
|
||||
expect(liveStreamEvent.customerId).toBe(customerId);
|
||||
|
||||
const recordingUploadedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
initiatorId: string;
|
||||
participants: Array<string>;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED');
|
||||
|
||||
const jwtPayload = p1.getToken()?.payload;
|
||||
|
||||
expect(recordingUploadedEvent.data.initiatorId).toBe(jwtPayload?.context?.user?.id);
|
||||
expect(recordingUploadedEvent.data.participants.some(
|
||||
// @ts-ignore
|
||||
e => e.id === jwtPayload?.context?.user?.id)).toBe(true);
|
||||
|
||||
webhooksProxy?.clearCache();
|
||||
}
|
||||
|
||||
const statusEvent = (await p1.getIframeAPI().getEventResult('recordingStatusChanged'));
|
||||
|
||||
expect(statusEvent.mode).toBe('file');
|
||||
expect(statusEvent.on).toBe(false);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('recordingStatusChanged');
|
||||
}
|
||||
237
tests/specs/iframe/transcriptions.spec.ts
Normal file
237
tests/specs/iframe/transcriptions.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { expect } from '@wdio/globals';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import type WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Transcriptions', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureOneParticipant();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (await p1.execute(() => config.disableIframeAPI || !config.transcription?.enabled)) {
|
||||
// skip the test if iframeAPI or transcriptions are disabled
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
// let's populate endpoint ids
|
||||
await Promise.all([
|
||||
p1.getEndpointId(),
|
||||
p2.getEndpointId()
|
||||
]);
|
||||
|
||||
await p1.switchToAPI();
|
||||
await p2.switchToAPI();
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
});
|
||||
|
||||
it('toggle subtitles', async () => {
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy);
|
||||
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
// give it some time to process
|
||||
await p1.driver.pause(5000);
|
||||
});
|
||||
|
||||
it('set subtitles on and off', async () => {
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
// we need to clear results or the last one will be used, form the previous time subtitles were on
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy);
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', false);
|
||||
|
||||
// give it some time to process
|
||||
await p1.driver.pause(5000);
|
||||
});
|
||||
|
||||
it('start/stop transcriptions via recording', async () => {
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
// we need to clear results or the last one will be used, form the previous time subtitles were on
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
|
||||
let allTranscriptionStatusChanged: Promise<any>[] = [];
|
||||
|
||||
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received on p1 side'
|
||||
}));
|
||||
allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received on p2 side'
|
||||
}));
|
||||
|
||||
let result = await Promise.allSettled(allTranscriptionStatusChanged);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
|
||||
result.forEach(e => {
|
||||
// @ts-ignore
|
||||
expect(e.value.on).toBe(true);
|
||||
});
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
|
||||
allTranscriptionStatusChanged = [];
|
||||
|
||||
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received on p1 side'
|
||||
}));
|
||||
allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received on p2 side'
|
||||
}));
|
||||
|
||||
result = await Promise.allSettled(allTranscriptionStatusChanged);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
|
||||
result.forEach(e => {
|
||||
// @ts-ignore
|
||||
expect(e.value.on).toBe(false);
|
||||
});
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
// sometimes events are not immediately received,
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
if (webhooksProxy) {
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect('TRANSCRIPTION_UPLOADED').toBe(event.eventType);
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy) {
|
||||
const allTranscripts: Promise<any>[] = [];
|
||||
|
||||
allTranscripts.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p1 side'
|
||||
}));
|
||||
|
||||
allTranscripts.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
|
||||
}));
|
||||
|
||||
if (webhooksProxy) {
|
||||
// TRANSCRIPTION_CHUNK_RECEIVED webhook
|
||||
allTranscripts.push((async () => {
|
||||
const event: {
|
||||
data: {
|
||||
final: string;
|
||||
language: string;
|
||||
messageID: string;
|
||||
participant: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
stable: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
expect('TRANSCRIPTION_CHUNK_RECEIVED').toBe(event.eventType);
|
||||
|
||||
event.data.stable = event.data.final;
|
||||
|
||||
return event;
|
||||
})());
|
||||
}
|
||||
|
||||
const result = await Promise.allSettled(allTranscripts);
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// @ts-ignore
|
||||
const firstEntryData = result[0].value.data;
|
||||
const stable = firstEntryData.stable || firstEntryData.final;
|
||||
const language = firstEntryData.language;
|
||||
const messageID = firstEntryData.messageID;
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
result.map(r => {
|
||||
// @ts-ignore
|
||||
const v = r.value;
|
||||
|
||||
expect(v).toBeDefined();
|
||||
|
||||
return v.data;
|
||||
}).forEach(tr => {
|
||||
const checkTranscripts = stable.includes(tr.stable || tr.final) || (tr.stable || tr.final).includes(stable);
|
||||
|
||||
if (!checkTranscripts) {
|
||||
console.log('received events', JSON.stringify(result));
|
||||
}
|
||||
|
||||
expect(checkTranscripts).toBe(true);
|
||||
expect(tr.language).toBe(language);
|
||||
expect(tr.messageID).toBe(messageID);
|
||||
expect(tr.participant.id).toBe(p1Id);
|
||||
expect(tr.participant.name).toBe(p1.name);
|
||||
});
|
||||
}
|
||||
125
tests/specs/iframe/visitors.spec.ts
Normal file
125
tests/specs/iframe/visitors.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Visitors', () => {
|
||||
it('joining the meeting', async () => {
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true
|
||||
};
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled or visitors are not supported
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.driver.waitUntil(() => p1.execute(() => APP.conference._room.isVisitorsSupported()), {
|
||||
timeout: 2000
|
||||
}).then(async () => {
|
||||
await p1.switchToAPI();
|
||||
}).catch(() => {
|
||||
ctx.skipSuiteTests = true;
|
||||
});
|
||||
});
|
||||
|
||||
it('visitor joins', async () => {
|
||||
await ensureTwoParticipants({
|
||||
preferGenerateToken: true,
|
||||
tokenOptions: { visitor: true },
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
await p2.waitForReceiveMedia(15_000, 'Visitor is not receiving media');
|
||||
await p2.waitForRemoteStreams(1);
|
||||
|
||||
const p2Visitors = p2.getVisitors();
|
||||
const p1Visitors = p1.getVisitors();
|
||||
|
||||
await p2.driver.waitUntil(() => p2Visitors.hasVisitorsDialog(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Missing visitors dialog'
|
||||
});
|
||||
|
||||
expect((await p1Visitors.getVisitorsCount()).trim()).toBe('1');
|
||||
expect((await p1Visitors.getVisitorsHeaderFromParticipantsPane()).trim()).toBe('Viewers 1');
|
||||
|
||||
if (webhooksProxy) {
|
||||
// PARTICIPANT_JOINED webhook
|
||||
// @ts-ignore
|
||||
const event: {
|
||||
customerId: string;
|
||||
data: {
|
||||
avatar: string;
|
||||
email: string;
|
||||
group: string;
|
||||
id: string;
|
||||
name: string;
|
||||
participantJid: string;
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
const jwtPayload = p2.getToken()?.payload;
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.avatar).toBe(jwtPayload.context.user.avatar);
|
||||
expect(event.data.email).toBe(jwtPayload.context.user.email);
|
||||
expect(event.data.id).toBe(jwtPayload.context.user.id);
|
||||
expect(event.data.group).toBe(jwtPayload.context.group);
|
||||
expect(event.data.name).toBe(p2.name);
|
||||
expect(event.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
|
||||
expect(event.data.name).toBe(p2.name);
|
||||
expect(event.data.role).toBe('visitor');
|
||||
expect(event.customerId).toBe(testsConfig.iframe.customerId);
|
||||
|
||||
await p2.switchToAPI();
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
// PARTICIPANT_LEFT webhook
|
||||
// @ts-ignore
|
||||
const eventLeft: {
|
||||
customerId: string;
|
||||
data: {
|
||||
avatar: string;
|
||||
email: string;
|
||||
group: string;
|
||||
id: string;
|
||||
name: string;
|
||||
participantJid: string;
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect('PARTICIPANT_LEFT').toBe(eventLeft.eventType);
|
||||
expect(eventLeft.data.avatar).toBe(jwtPayload.context.user.avatar);
|
||||
expect(eventLeft.data.email).toBe(jwtPayload.context.user.email);
|
||||
expect(eventLeft.data.id).toBe(jwtPayload.context.user.id);
|
||||
expect(eventLeft.data.group).toBe(jwtPayload.context.group);
|
||||
expect(eventLeft.data.name).toBe(p2.name);
|
||||
expect(eventLeft.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
|
||||
expect(eventLeft.data.name).toBe(p2.name);
|
||||
expect(eventLeft.data.role).toBe('visitor');
|
||||
expect(eventLeft.customerId).toBe(testsConfig.iframe.customerId);
|
||||
}
|
||||
});
|
||||
});
|
||||
81
tests/specs/iframe/visitorsLive.spec.ts
Normal file
81
tests/specs/iframe/visitorsLive.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { expect } from '@wdio/globals';
|
||||
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Visitors', () => {
|
||||
it('joining the meeting', async () => {
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true,
|
||||
visitorsLive: false
|
||||
};
|
||||
}
|
||||
|
||||
await ensureOneParticipant();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled or visitors are not supported
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.driver.waitUntil(() => p1.execute(() => APP.conference._room.isVisitorsSupported()), {
|
||||
timeout: 2000
|
||||
}).then(async () => {
|
||||
await p1.switchToAPI();
|
||||
}).catch(() => {
|
||||
ctx.skipSuiteTests = true;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('go live', async () => {
|
||||
await ensureTwoParticipants({
|
||||
preferGenerateToken: true,
|
||||
tokenOptions: { visitor: true },
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p1, p2 } = ctx;
|
||||
const p2Visitors = p2.getVisitors();
|
||||
const p1Visitors = p1.getVisitors();
|
||||
|
||||
await p2.driver.waitUntil(async () => p2Visitors.isVisitorsQueueUIShown(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Missing visitors queue UI'
|
||||
});
|
||||
|
||||
await p1.driver.waitUntil(async () => await p1Visitors.getWaitingVisitorsInQueue()
|
||||
=== 'Viewers waiting in queue: 1', {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'Missing visitors queue count in UI'
|
||||
});
|
||||
|
||||
await p1Visitors.goLive();
|
||||
|
||||
await p2.waitToJoinMUC();
|
||||
await p2.waitForReceiveMedia(15000, 'Visitor is not receiving media');
|
||||
await p2.waitForRemoteStreams(1);
|
||||
|
||||
await p2.driver.waitUntil(() => p2Visitors.hasVisitorsDialog(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Missing visitors dialog'
|
||||
});
|
||||
|
||||
expect((await p1Visitors.getVisitorsCount()).trim()).toBe('1');
|
||||
expect((await p1Visitors.getVisitorsHeaderFromParticipantsPane()).trim()).toBe('Viewers 1');
|
||||
});
|
||||
});
|
||||
101
tests/specs/jaas/joinMuc.spec.ts
Normal file
101
tests/specs/jaas/joinMuc.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { TOKEN_AUTH_FAILED_TEST_ID, TOKEN_AUTH_FAILED_TITLE_TEST_ID } from '../../pageobjects/Notifications';
|
||||
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
describe('XMPP login and MUC join test', () => {
|
||||
it('with a valid token (wildcard room)', async () => {
|
||||
console.log('Joining a MUC with a valid token (wildcard room)');
|
||||
const p = await joinMuc('p1', t({ room: '*' }));
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a valid token (specific room)', async () => {
|
||||
console.log('Joining a MUC with a valid token (specific room)');
|
||||
const p = await joinMuc('p1', t({ room: ctx.roomName }));
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a token with bad signature', async () => {
|
||||
console.log('Joining a MUC with a token with bad signature');
|
||||
const token = t({ room: ctx.roomName });
|
||||
|
||||
token.jwt = token.jwt + 'badSignature';
|
||||
|
||||
const p = await joinMuc('p1', token);
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID)
|
||||
|| await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
it('with an expired token', async () => {
|
||||
console.log('Joining a MUC with an expired token');
|
||||
const p = await joinMuc('p1', t({ exp: '-1m' }));
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('Token is expired');
|
||||
});
|
||||
|
||||
it('with a token using the wrong key ID', async () => {
|
||||
console.log('Joining a MUC with a token using the wrong key ID');
|
||||
const p = await joinMuc('p1', t({ keyId: 'invalid-key-id' }));
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
it('with a token for a different room', async () => {
|
||||
console.log('Joining a MUC with a token for a different room');
|
||||
const p = await joinMuc('p1', t({ room: ctx.roomName + 'different' }));
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
it('with a moderator token', async () => {
|
||||
console.log('Joining a MUC with a moderator token');
|
||||
const p = await joinMuc('p1', t({ moderator: true }));
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(true);
|
||||
});
|
||||
|
||||
// This is dependent on jaas account configuration. All tests under jaas/ expect that "unauthenticated access" is
|
||||
// disabled.
|
||||
it('without a token', async () => {
|
||||
console.log('Joining a MUC without a token');
|
||||
const p = await joinMuc('p1');
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
// it('without sending a conference-request', async () => {
|
||||
// console.log('Joining a MUC without sending a conference-request');
|
||||
// // TODO verify failure
|
||||
// //expect(await joinMuc(ctx.roomName, 'p1', token)).toBe(true);
|
||||
// });
|
||||
});
|
||||
31
tests/specs/jaas/maxOccupants.spec.ts
Normal file
31
tests/specs/jaas/maxOccupants.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('MaxOccupants limit enforcement', () => {
|
||||
it('test maxOccupants limit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
maxOccupants: 2
|
||||
};
|
||||
|
||||
const p1 = await joinMuc('p1', t({ room: ctx.roomName }));
|
||||
const p2 = await joinMuc('p2', t({ room: ctx.roomName }));
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
expect(await p2.isInMuc()).toBe(true);
|
||||
|
||||
// Third participant should be rejected (exceeding maxOccupants), even if it's a moderator
|
||||
let p3 = await joinMuc('p3', t({ room: ctx.roomName, moderator: true }));
|
||||
|
||||
expect(Boolean(await p3.isInMuc())).toBe(false);
|
||||
|
||||
await p1.hangup();
|
||||
p3 = await joinMuc('p3', t({ room: ctx.roomName }));
|
||||
expect(await p3.isInMuc()).toBe(true);
|
||||
});
|
||||
});
|
||||
51
tests/specs/jaas/passcode.spec.ts
Normal file
51
tests/specs/jaas/passcode.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { IToken } from '../../helpers/token';
|
||||
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
const passcode = '1234';
|
||||
|
||||
describe('Setting passcode through settings provisioning', () => {
|
||||
it('With a valid passcode', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
passcode: passcode,
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
// We want to keep the room from getting destroyed, because the visitors queue has a timeout and causes
|
||||
// problems. We could use different rooms instead, but the webhooksProxy is only configured for the default room.
|
||||
await joinWithPassword('p1', t({ room: ctx.roomName }));
|
||||
await joinWithPassword('p2', t({ room: ctx.roomName, moderator: true }));
|
||||
await joinWithPassword('p2', t({ room: ctx.roomName, visitor: true }));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Join a password-protected room. Assert that a password is required, that a wrong password does not work, and that
|
||||
* the correct password does work.
|
||||
*/
|
||||
async function joinWithPassword(instanceId: string, token: IToken) {
|
||||
// @ts-ignore
|
||||
const p = await joinMuc(instanceId, token, ctx.roomName);
|
||||
|
||||
await p.waitForMucJoinedOrError();
|
||||
expect(await p.isInMuc()).toBe(false);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(true);
|
||||
|
||||
await p.getPasswordDialog().submitPassword('wrong password');
|
||||
await p.waitForMucJoinedOrError();
|
||||
expect(await p.isInMuc()).toBe(false);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(true);
|
||||
|
||||
await p.getPasswordDialog().submitPassword(passcode);
|
||||
await p.waitToJoinMUC();
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(false);
|
||||
}
|
||||
|
||||
25
tests/specs/jaas/passcodeInvalid.spec.ts
Normal file
25
tests/specs/jaas/passcodeInvalid.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
// This test is separate from passcode.spec.ts, because it needs to use a different room name, and webhooksProxy is only
|
||||
// setup for the default room name.
|
||||
describe('Setting passcode through settings provisioning', () => {
|
||||
it('With an invalid passcode', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
passcode: 'passcode-must-be-digits-only'
|
||||
};
|
||||
|
||||
const p = await joinMuc('p1', t({ room: ctx.roomName }), ctx.roomName);
|
||||
|
||||
// The settings provisioning contains an invalid passcode, the expected result is that the room is not
|
||||
// configured to require a passcode.
|
||||
await p.waitToJoinMUC();
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
50
tests/specs/jaas/visitors/participantsSoftLimit.spec.ts
Normal file
50
tests/specs/jaas/visitors/participantsSoftLimit.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Visitors triggered by reaching participantsSoftLimit', () => {
|
||||
it('test participantsSoftLimit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
participantsSoftLimit: 2,
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
/// XXX the "name" of the participant MUST match one of the "capabilities" defined in wdio. It's not a "participant", it's an instance configuration!
|
||||
const m = await joinMuc(
|
||||
'p1',
|
||||
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
|
||||
);
|
||||
|
||||
expect(await m.isInMuc()).toBe(true);
|
||||
expect(await m.isModerator()).toBe(true);
|
||||
expect(await m.isVisitor()).toBe(false);
|
||||
console.log('Moderator joined');
|
||||
|
||||
// Joining with a participant token before participantSoftLimit has been reached
|
||||
const p = await joinMuc(
|
||||
'p2',
|
||||
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
|
||||
);
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
expect(await p.isVisitor()).toBe(false);
|
||||
console.log('Participant joined');
|
||||
|
||||
// Joining with a participant token after participantSoftLimit has been reached
|
||||
const v = await joinMuc(
|
||||
'p3',
|
||||
t({ room: ctx.roomName, displayName: 'Visi Tor' })
|
||||
);
|
||||
|
||||
expect(await v.isInMuc()).toBe(true);
|
||||
expect(await v.isModerator()).toBe(false);
|
||||
expect(await v.isVisitor()).toBe(true);
|
||||
console.log('Visitor joined');
|
||||
});
|
||||
});
|
||||
61
tests/specs/jaas/visitors/videoWithSingleSender.spec.ts
Normal file
61
tests/specs/jaas/visitors/videoWithSingleSender.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a case which fails if jitsi-videobridge doesn't properly forward PLIs from visitors.
|
||||
*/
|
||||
describe('Visitor receiving video from a single remote participant', () => {
|
||||
it('joining the meeting', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true,
|
||||
visitorsLive: true,
|
||||
};
|
||||
|
||||
// Force a connection via JVB.
|
||||
const configOverwrite = {
|
||||
p2p: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
const sender = await joinMuc(
|
||||
'p1',
|
||||
t({ room: ctx.roomName, displayName: 'Sender', moderator: true }), {
|
||||
configOverwrite
|
||||
}
|
||||
);
|
||||
const senderEndpointId = await sender.getEndpointId();
|
||||
|
||||
const testVisitor = async function(instanceId: 'p1' | 'p2' | 'p3' | 'p4') {
|
||||
const visitor = await joinMuc(
|
||||
instanceId,
|
||||
t({ room: ctx.roomName, displayName: 'Visitor', visitor: true }), {
|
||||
configOverwrite
|
||||
}
|
||||
);
|
||||
|
||||
await visitor.waitForIceConnected();
|
||||
|
||||
const iceConnected = performance.now();
|
||||
|
||||
await visitor.driver.waitUntil(
|
||||
() => visitor.isRemoteVideoReceivedAndDisplayed(senderEndpointId), {
|
||||
timeout: 10_000,
|
||||
timeoutMsg: `Visitor (${instanceId}) is not receiving video from the sender`
|
||||
});
|
||||
|
||||
const duration = performance.now() - iceConnected;
|
||||
|
||||
console.log(`Video displayed after ${duration} ms after ICE connected (${instanceId})`);
|
||||
};
|
||||
|
||||
await testVisitor('p2');
|
||||
await testVisitor('p3');
|
||||
await testVisitor('p4');
|
||||
});
|
||||
});
|
||||
58
tests/specs/jaas/visitors/visitorTokens.spec.ts
Normal file
58
tests/specs/jaas/visitors/visitorTokens.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Visitors triggered by visitor tokens', () => {
|
||||
it('test visitor tokens', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
const m = await joinMuc(
|
||||
'p1',
|
||||
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
|
||||
);
|
||||
|
||||
expect(await m.isInMuc()).toBe(true);
|
||||
expect(await m.isModerator()).toBe(true);
|
||||
expect(await m.isVisitor()).toBe(false);
|
||||
console.log('Moderator joined');
|
||||
|
||||
// Joining with a participant token before any visitors
|
||||
const p = await joinMuc(
|
||||
'p2',
|
||||
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
|
||||
);
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
expect(await p.isVisitor()).toBe(false);
|
||||
console.log('Participant joined');
|
||||
|
||||
// Joining with a visitor token
|
||||
const v = await joinMuc(
|
||||
'p3',
|
||||
t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
|
||||
);
|
||||
|
||||
expect(await v.isInMuc()).toBe(true);
|
||||
expect(await v.isModerator()).toBe(false);
|
||||
expect(await v.isVisitor()).toBe(true);
|
||||
console.log('Visitor joined');
|
||||
|
||||
// Joining with a participant token after visitors...:mindblown:
|
||||
const v2 = await joinMuc(
|
||||
'p2',
|
||||
t({ room: ctx.roomName, displayName: 'Visi Tor 2' }));
|
||||
|
||||
expect(await v2.isInMuc()).toBe(true);
|
||||
expect(await v2.isModerator()).toBe(false);
|
||||
expect(await v2.isVisitor()).toBe(true);
|
||||
console.log('Visitor2 joined');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user