ref: Refactor tests (#16399)

* ref: Inline enterTileView.
* ref: Refactor tileView, remove tileView.LastN.
    The "last n" cases are not related to tile view and are covered in lastN.spec.ts.
* ref: Remove redundant "skipInMeetingChecks: true".
    skipInMeetingChecks is only used in ensureTwoParticipants, ensureThreeParticipants and ensureFourParticipants.
* ref: Move recording test to jaas/, more refactoring.
* ref: Rename and document switchToAPI() and switchInPage().
* ref: Move the tileView into 2way (temp).
This commit is contained in:
bgrozev
2025-09-03 15:31:43 -05:00
committed by GitHub
parent e39f38f75b
commit 61764273b2
26 changed files with 397 additions and 476 deletions

View File

@@ -27,7 +27,6 @@ import Toolbar from '../pageobjects/Toolbar';
import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
import Visitors from '../pageobjects/Visitors';
import { config as testsConfig } from './TestsConfig';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IToken } from './token';
import { IParticipantJoinOptions, IParticipantOptions } from './types';
@@ -219,8 +218,8 @@ export class Participant {
// @ts-ignore
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${options.roomName}"`;
if (testsConfig.iframe.tenant) {
url = `${url}&tenant="${testsConfig.iframe.tenant}"`;
if (options.tenant) {
url = `${url}&tenant="${options.tenant}"`;
} else if (baseUrl.pathname.length > 1) {
// remove leading slash
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
@@ -235,8 +234,9 @@ export class Participant {
// drop the leading '/' so we can use the tenant if any
url = url.startsWith('/') ? url.substring(1) : url;
if (options.forceTenant) {
url = `/${options.forceTenant}/${url}`;
if (options.tenant && !this._iFrameApi) {
// For the iFrame API the tenant is passed in a different way.
url = `/${options.tenant}/${url}`;
}
await this.driver.url(url);
@@ -244,9 +244,7 @@ export class Participant {
await this.waitForPageToLoad();
if (this._iFrameApi) {
const mainFrame = this.driver.$('iframe');
await this.driver.switchFrame(mainFrame);
await this.switchToIFrame();
}
if (!options.skipWaitToJoin) {
@@ -319,7 +317,7 @@ export class Participant {
/**
* Waits for the tile view to display.
*/
async waitForTileViewDisplay(reverse = false) {
async waitForTileViewDisplayed(reverse = false) {
await this.driver.$('//div[@id="videoconference_page" and contains(@class, "tile-view")]').waitForDisplayed({
reverse,
timeout: 10_000,
@@ -626,19 +624,23 @@ export class Participant {
/**
* Switches to the iframe API context
* Switches to the main frame context (outside the iFrame; where the Jitsi Meet iFrame API is available).
*
* If this Participant was initialized with iFrameApi=false this has no effect, as there aren't any other contexts.
*/
async switchToAPI() {
async switchToMainFrame() {
await this.driver.switchFrame(null);
}
/**
* Switches to the meeting page context.
* Switches to the iFrame context (inside the iFrame; where the Jitsi Meet application runs).
*
* If this Participant was initialized with iFrameApi=false this will result in an error.
*/
switchInPage() {
const mainFrame = this.driver.$('iframe');
async switchToIFrame() {
const iframe = this.driver.$('iframe');
return this.driver.switchFrame(mainFrame);
await this.driver.switchFrame(iframe);
}
/**

View File

@@ -187,7 +187,7 @@ async function joinParticipant( // eslint-disable-line max-params
if (p) {
if (ctx.testProperties.useIFrameApi) {
await p.switchInPage();
await p.switchToIFrame();
}
if (await p.isInMuc()) {
@@ -196,7 +196,7 @@ async function joinParticipant( // eslint-disable-line max-params
if (ctx.testProperties.useIFrameApi) {
// when loading url make sure we are on the top page context or strange errors may occur
await p.switchToAPI();
await p.switchToMainFrame();
}
// Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
@@ -209,16 +209,16 @@ async function joinParticipant( // eslint-disable-line max-params
// @ts-ignore
ctx[participantOptions.name] = newParticipant;
let forceTenant = options?.forceTenant;
let tenant = options?.tenant;
if (options?.preferGenerateToken && !ctx.testProperties.useIFrameApi
&& config.iframe.usesJaas && config.iframe.tenant) {
forceTenant = config.iframe.tenant;
tenant = config.iframe.tenant;
}
return await newParticipant.joinConference({
...options,
forceTenant,
tenant: tenant,
roomName: options?.roomName || ctx.roomName,
});
}

View File

@@ -53,11 +53,6 @@ export type IParticipantJoinOptions = {
*/
configOverwrite?: IConfig;
/**
* An optional tenant to use. If provided the URL is prepended with /$forceTenant
*/
forceTenant?: string;
/** The name of the room to join */
roomName: string;
@@ -71,6 +66,11 @@ export type IParticipantJoinOptions = {
* based on the logic of the test.
*/
skipWaitToJoin?: boolean;
/**
* An optional tenant to use. If provided it overrides the default.
*/
tenant?: string;
};
export type IJoinOptions = {
@@ -80,11 +80,6 @@ export type IJoinOptions = {
*/
configOverwrite?: IConfig;
/**
* An optional tenant to use. If provided the URL is prepended with /$forceTenant
*/
forceTenant?: string;
/**
* When joining the first participant and jwt singing material is available and a provided token
* is available, prefer generating a new token for the first participant.
@@ -116,6 +111,11 @@ export type IJoinOptions = {
*/
skipWaitToJoin?: boolean;
/**
* An optional tenant to use. If provided the default tenant is changed to it.
*/
tenant?: string;
/**
* Options used when generating a token.
*/

View File

@@ -21,8 +21,7 @@ describe('Lock Room', () => {
it('enter participant in locked room', async () => {
// first enter wrong pin then correct one
await joinSecondParticipant({
skipWaitToJoin: true,
skipInMeetingChecks: true
skipWaitToJoin: true
});
const { p2 } = ctx;
@@ -98,8 +97,7 @@ describe('Lock Room', () => {
await ctx.p2.hangup();
await participant1LockRoom();
await joinSecondParticipant({
skipWaitToJoin: true,
skipInMeetingChecks: true
skipWaitToJoin: true
});
const { p2 } = ctx;

View File

@@ -10,8 +10,7 @@ describe('PreJoin', () => {
requireDisplayName: true
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
skipWaitToJoin: true
});
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
@@ -38,8 +37,7 @@ describe('PreJoin', () => {
}
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
skipWaitToJoin: true
});
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
@@ -61,8 +59,7 @@ describe('PreJoin', () => {
}
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
skipWaitToJoin: true
});
const { p1 } = ctx;
@@ -109,8 +106,7 @@ describe('PreJoin', () => {
}
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
skipWaitToJoin: true
});
const p1PreJoinScreen = ctx.p2.getPreJoinScreen();

View File

@@ -0,0 +1,53 @@
import { Participant } from '../../helpers/Participant';
import { 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', () => {
let p1: Participant, p2: Participant;
before('join the meeting', async () => {
await ensureTwoParticipants();
p1 = ctx.p1;
p2 = ctx.p2;
});
it('entering tile view', async () => {
await p1.getToolbar().clickEnterTileViewButton();
await p1.waitForTileViewDisplayed();
});
it('exit tile view by pinning', async () => {
await p1.getFilmstrip().pinParticipant(p2);
await p1.waitForTileViewDisplayed(true);
});
it('local video is displayed in tile view', async () => {
await p1.getToolbar().clickEnterTileViewButton();
await p1.waitForTileViewDisplayed();
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('exit tile view by clicking "exit tile view"', async () => {
await p1.getToolbar().clickExitTileViewButton();
await p1.waitForTileViewDisplayed(true);
});
it('local video display independently from remote', async () => {
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 });
});
});

View File

@@ -21,7 +21,7 @@ describe('URL Normalisation', () => {
}
await ensureTwoParticipants({
forceTenant: 'tenant@example.com',
tenant: 'tenant@example.com',
roomName: `${ctx.roomName}@example.com`
});
});

View File

@@ -47,22 +47,22 @@ describe('Follow Me', () => {
const { p1, p2, p3 } = ctx;
await p1.waitForTileViewDisplay();
await p1.waitForTileViewDisplayed();
await p1.getToolbar().clickExitTileViewButton();
await Promise.all([
p1.waitForTileViewDisplay(true),
p2.waitForTileViewDisplay(true),
p3.waitForTileViewDisplay(true)
p1.waitForTileViewDisplayed(true),
p2.waitForTileViewDisplayed(true),
p3.waitForTileViewDisplayed(true)
]);
await p1.getToolbar().clickEnterTileViewButton();
await Promise.all([
p1.waitForTileViewDisplay(),
p2.waitForTileViewDisplay(),
p3.waitForTileViewDisplay()
p1.waitForTileViewDisplayed(),
p2.waitForTileViewDisplayed(),
p3.waitForTileViewDisplayed()
]);
});

View File

@@ -40,10 +40,7 @@ describe('StartMuted', () => {
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
await joinSecondParticipant({
...options,
skipInMeetingChecks: true
});
await joinSecondParticipant(options);
// Enable screenshare on p1.
p1.getToolbar().clickDesktopSharingButton();
@@ -80,10 +77,7 @@ describe('StartMuted', () => {
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
});
await joinThirdParticipant(options);
const { p3 } = ctx;
@@ -110,20 +104,14 @@ describe('StartMuted', () => {
};
await ensureOneParticipant(options);
await joinSecondParticipant({
...options,
skipInMeetingChecks: true
});
await joinSecondParticipant(options);
const { p2 } = ctx;
await p2.waitForIceConnected();
await p2.waitForReceiveMedia();
await joinThirdParticipant({
...options,
skipInMeetingChecks: true
});
await joinThirdParticipant(options);
const { p3 } = ctx;
@@ -233,8 +221,7 @@ describe('StartMuted', () => {
p2p: {
enabled: true
}
},
skipInMeetingChecks: true
}
});
const { p1, p2 } = ctx;

View File

@@ -1,113 +0,0 @@
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();
}

View File

@@ -1,7 +1,7 @@
import { Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
import { IParticipantJoinOptions } from '../../helpers/types';
import { IParticipantJoinOptions, IParticipantOptions } from '../../helpers/types';
export function generateJaasToken(options: ITokenOptions): IToken {
if (!config.jaas.enabled) {
@@ -21,24 +21,22 @@ export function generateJaasToken(options: ITokenOptions): IToken {
* 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 participantOptions
* @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,
participantOptions?: Partial<IParticipantOptions>,
joinOptions?: Partial<IParticipantJoinOptions>): Promise<Participant> {
const name = participantOptions?.name || 'p1';
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
// @ts-ignore
const p = ctx[instanceId] as Participant;
const p = ctx[name] 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
@@ -47,16 +45,17 @@ export async function joinMuc(
}
const newParticipant = new Participant({
name: instanceId,
token
iFrameApi: participantOptions?.iFrameApi || false,
name,
token: participantOptions?.token
});
// @ts-ignore
ctx[instanceId] = newParticipant;
ctx[name] = newParticipant;
return await newParticipant.joinConference({
...joinOptions,
forceTenant: config.jaas.tenant,
tenant: joinOptions?.tenant || config.jaas.tenant,
roomName: joinOptions?.roomName || ctx.roomName,
});
}

View File

@@ -34,8 +34,8 @@ describe('Chat', () => {
it('send message', async () => {
const { p1, p2 } = ctx;
await p1.switchToAPI();
await p2.switchToAPI();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
await p2.getIframeAPI().addEventListener('chatUpdated');
await p2.getIframeAPI().addEventListener('incomingMessage');

View File

@@ -72,11 +72,11 @@ describe('Invite iframeAPI', () => {
const { p1 } = ctx;
await p1.switchToAPI();
await p1.switchToMainFrame();
await p1.getIframeAPI().invitePhone(process.env.DIAL_OUT_URL);
await p1.switchInPage();
await p1.switchToIFrame();
await p1.waitForParticipants(1);
@@ -92,11 +92,11 @@ describe('Invite iframeAPI', () => {
const { p1 } = ctx;
await p1.switchToAPI();
await p1.switchToMainFrame();
await p1.getIframeAPI().inviteSIP(process.env.SIP_JIBRI_DIAL_OUT_URL);
await p1.switchInPage();
await p1.switchToIFrame();
await p1.waitForParticipants(1);

View File

@@ -73,8 +73,8 @@ describe('Participants presence', () => {
p2.getEndpointId()
]);
await p1.switchToAPI();
await p2.switchToAPI();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
expect(await p2.getIframeAPI().getEventResult('isModerator')).toBe(false);
@@ -148,7 +148,7 @@ describe('Participants presence', () => {
it('participants pane', async () => {
const { p1 } = ctx;
await p1.switchToAPI();
await p1.switchToMainFrame();
expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
@@ -226,7 +226,7 @@ describe('Participants presence', () => {
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.switchToMainFrame();
await ctx.p2.getIframeAPI().executeCommand('hangup');
await ctx.p2.driver.waitUntil(() =>
ctx.p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
@@ -248,8 +248,8 @@ describe('Participants presence', () => {
const p1DisplayName = await p1.getLocalDisplayName();
const p2DisplayName = await p2.getLocalDisplayName();
await p1.switchToAPI();
await p2.switchToAPI();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
@@ -347,7 +347,7 @@ describe('Participants presence', () => {
expect(event.data.name).toBe(p2.name);
}
await p1.switchToAPI();
await p1.switchToMainFrame();
const event = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantJoined'), {
timeout: 2000,
@@ -381,7 +381,7 @@ describe('Participants presence', () => {
await p1.getIframeAPI().executeCommand('overwriteNames', newNames);
await p1.switchInPage();
await p1.switchToIFrame();
expect(await p1.getLocalDisplayName()).toBe(newP1Name);
@@ -392,8 +392,8 @@ describe('Participants presence', () => {
it('hangup', async () => {
const { p1, p2, roomName } = ctx;
await p1.switchToAPI();
await p2.switchToAPI();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
@@ -423,7 +423,7 @@ describe('Participants presence', () => {
it('dispose conference', async () => {
const { p1, roomName, webhooksProxy } = ctx;
await p1.switchToAPI();
await p1.switchToMainFrame();
await p1.getIframeAPI().clearEventResults('videoConferenceLeft');
await p1.getIframeAPI().addEventListener('videoConferenceLeft');

View File

@@ -1,207 +0,0 @@
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');
}

View File

@@ -24,7 +24,7 @@ describe('Transcriptions', () => {
return;
}
await p1.switchToAPI();
await p1.switchToMainFrame();
await ensureTwoParticipants({
configOverwrite: {
@@ -40,8 +40,8 @@ describe('Transcriptions', () => {
p2.getEndpointId()
]);
await p1.switchToAPI();
await p2.switchToAPI();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();

View File

@@ -32,7 +32,7 @@ describe('Visitors', () => {
await p1.driver.waitUntil(() => p1.execute(() => APP.conference._room.isVisitorsSupported()), {
timeout: 2000
}).then(async () => {
await p1.switchToAPI();
await p1.switchToMainFrame();
}).catch(() => {
ctx.skipSuiteTests = true;
});
@@ -91,7 +91,7 @@ describe('Visitors', () => {
expect(event.data.role).toBe('visitor');
expect(event.customerId).toBe(testsConfig.iframe.customerId);
await p2.switchToAPI();
await p2.switchToMainFrame();
await p2.getIframeAPI().executeCommand('hangup');
// PARTICIPANT_LEFT webhook

View File

@@ -34,7 +34,7 @@ describe('Visitors', () => {
await p1.driver.waitUntil(() => p1.execute(() => APP.conference._room.isVisitorsSupported()), {
timeout: 2000
}).then(async () => {
await p1.switchToAPI();
await p1.switchToMainFrame();
}).catch(() => {
ctx.skipSuiteTests = true;
});

View File

@@ -9,7 +9,7 @@ setTestProperties(__filename, {
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: '*' }));
const p = await joinMuc({ token: t({ room: '*' }) });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
@@ -17,7 +17,7 @@ describe('XMPP login and MUC join test', () => {
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 }));
const p = await joinMuc({ token: t({ room: ctx.roomName }) });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
@@ -29,7 +29,7 @@ describe('XMPP login and MUC join test', () => {
token.jwt = token.jwt + 'badSignature';
const p = await joinMuc('p1', token);
const p = await joinMuc({ token });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -41,7 +41,7 @@ describe('XMPP login and MUC join test', () => {
it('with an expired token', async () => {
console.log('Joining a MUC with an expired token');
const p = await joinMuc('p1', t({ exp: '-1m' }));
const p = await joinMuc({ token: t({ exp: '-1m' }) });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -52,7 +52,7 @@ describe('XMPP login and MUC join test', () => {
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' }));
const p = await joinMuc({ token: t({ keyId: 'invalid-key-id' }) });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -63,7 +63,7 @@ describe('XMPP login and MUC join test', () => {
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' }));
const p = await joinMuc({ token: t({ room: ctx.roomName + 'different' }) });
expect(Boolean(await p.isInMuc())).toBe(false);
@@ -74,7 +74,7 @@ describe('XMPP login and MUC join test', () => {
it('with a moderator token', async () => {
console.log('Joining a MUC with a moderator token');
const p = await joinMuc('p1', t({ moderator: true }));
const p = await joinMuc({ token: t({ moderator: true }) });
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(true);
@@ -84,7 +84,7 @@ describe('XMPP login and MUC join test', () => {
// disabled.
it('without a token', async () => {
console.log('Joining a MUC without a token');
const p = await joinMuc('p1');
const p = await joinMuc();
expect(Boolean(await p.isInMuc())).toBe(false);

View File

@@ -13,19 +13,19 @@ describe('MaxOccupants limit enforcement', () => {
maxOccupants: 2
};
const p1 = await joinMuc('p1', t({ room: ctx.roomName }));
const p2 = await joinMuc('p2', t({ room: ctx.roomName }));
const p1 = await joinMuc({ token: t({ room: ctx.roomName }) });
const p2 = await joinMuc({ name: 'p2', token: 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 }));
let p3 = await joinMuc({ name: 'p3', token: t({ room: ctx.roomName, moderator: true }) });
expect(Boolean(await p3.isInMuc())).toBe(false);
await p1.hangup();
p3 = await joinMuc('p3', t({ room: ctx.roomName }));
p3 = await joinMuc({ name: 'p3', token: t({ room: ctx.roomName }) });
expect(await p3.isInMuc()).toBe(true);
});
});

View File

@@ -31,7 +31,7 @@ describe('Setting passcode through settings provisioning', () => {
*/
async function joinWithPassword(instanceId: string, token: IToken) {
// @ts-ignore
const p = await joinMuc(instanceId, token, ctx.roomName);
const p = await joinMuc({ name: instanceId, token }, { roomName: ctx.roomName });
await p.waitForMucJoinedOrError();
expect(await p.isInMuc()).toBe(false);

View File

@@ -14,7 +14,7 @@ describe('Setting passcode through settings provisioning', () => {
passcode: 'passcode-must-be-digits-only'
};
const p = await joinMuc('p1', t({ room: ctx.roomName }), ctx.roomName);
const p = await joinMuc({ token: t({ room: ctx.roomName }) }, { roomName: ctx.roomName });
// The settings provisioning contains an invalid passcode, the expected result is that the room is not
// configured to require a passcode.

View File

@@ -0,0 +1,208 @@
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import WebhookProxy from '../../helpers/WebhookProxy';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
// Note this just for posterity. We don't depend on the framework doing anything for us because of this flag (we
// pass it as a parameter directly)
useIFrameApi: true,
useWebhookProxy: true
});
/**
* Tests the recording and live-streaming functionality of JaaS (including relevant webhooks) exercising the iFrame API
* commands and functions.
* TODO: read flags from config.
* TODO: also assert "this meeting is being recorder" notificaitons are show/played?
*/
describe('Recording and Live Streaming', () => {
const tenant = testsConfig.jaas.tenant;
const customerId = tenant?.replace('vpaas-magic-cookie-', '');
// TODO: read from config
let recordingDisabled: boolean;
// TODO: read from config
let liveStreamingDisabled: boolean;
let p: Participant;
let webhooksProxy: WebhookProxy;
it('setup', async () => {
webhooksProxy = ctx.webhooksProxy;
p = await joinMuc({ iFrameApi: true, token: t({ moderator: true }) }, { roomName: ctx.roomName });
// TODO: what should we do in this case? Add a config for this?
if (await p.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
return;
}
// TODO: only read if config says so
recordingDisabled = Boolean(!await p.execute(() => config.recordingService?.enabled));
liveStreamingDisabled = Boolean(!await p.execute(() => config.liveStreaming?.enabled))
|| !process.env.YTUBE_TEST_STREAM_KEY;
await p.switchToMainFrame();
});
/**
* Starts recording and asserts that the expected iFrame and JaaS events are received.
* @param command whether to use the "command" or the "function" iFrame API.
*/
async function startRecording(command: boolean) {
await p.getIframeAPI().addEventListener('recordingStatusChanged');
await p.getIframeAPI().addEventListener('recordingLinkAvailable');
if (command) {
await p.getIframeAPI().executeCommand('startRecording', {
mode: 'file'
});
} else {
await p.getIframeAPI().startRecording({
mode: 'file'
});
}
const jaasEvent: {
customerId: string;
eventType: string;
} = await webhooksProxy.waitForEvent('RECORDING_STARTED');
expect('RECORDING_STARTED').toBe(jaasEvent.eventType);
expect(jaasEvent.customerId).toBe(customerId);
webhooksProxy.clearCache();
const iFrameEvent = (await p.getIframeAPI().getEventResult('recordingStatusChanged'));
expect(iFrameEvent.mode).toBe('file');
expect(iFrameEvent.on).toBe(true);
const linkEvent = (await p.getIframeAPI().getEventResult('recordingLinkAvailable'));
expect(linkEvent.link.startsWith('https://')).toBe(true);
expect(linkEvent.link.includes(tenant)).toBe(true);
expect(linkEvent.ttl > 0).toBe(true);
}
/**
* Stops recording and asserts that the expected iFrame and JaaS events are received.
* @param command whether to use the "command" or the "function" iFrame API.
*/
async function stopRecording(command: boolean) {
if (command) {
await p.getIframeAPI().executeCommand('stopRecording', 'file');
} else {
await p.getIframeAPI().stopRecording('file');
}
const jaasEndedEvent: {
customerId: string;
eventType: string;
} = await webhooksProxy.waitForEvent('RECORDING_ENDED');
expect('RECORDING_ENDED').toBe(jaasEndedEvent.eventType);
expect(jaasEndedEvent.customerId).toBe(customerId);
const jaasUploadedEvent: {
customerId: string;
data: {
initiatorId: string;
participants: Array<string>;
};
eventType: string;
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED');
const jwtPayload = p.getToken()?.payload;
expect(jaasUploadedEvent.data.initiatorId).toBe(jwtPayload?.context?.user?.id);
expect(jaasUploadedEvent.data.participants.some(
// @ts-ignore
e => e.id === jwtPayload?.context?.user?.id)).toBe(true);
webhooksProxy.clearCache();
const iFrameEvent = (await p.getIframeAPI().getEventResult('recordingStatusChanged'));
expect(iFrameEvent.mode).toBe('file');
expect(iFrameEvent.on).toBe(false);
await p.getIframeAPI().clearEventResults('recordingStatusChanged');
}
it('start/stop recording using the iFrame command', async () => {
if (recordingDisabled) {
return;
}
await startRecording(true);
await stopRecording(true);
// to avoid rate limits
await p.driver.pause(30000);
});
it('start/stop recording using the iFrame function', async () => {
if (recordingDisabled) {
return;
}
await startRecording(false);
await stopRecording(false);
// to avoid rate limits
await p.driver.pause(30000);
});
it('start/stop live-streaming using the iFrame command', async () => {
if (liveStreamingDisabled) {
return;
}
await p.getIframeAPI().addEventListener('recordingStatusChanged');
await p.getIframeAPI().executeCommand('startRecording', {
youtubeBroadcastID: process.env.YTUBE_TEST_BROADCAST_ID,
mode: 'stream',
youtubeStreamKey: process.env.YTUBE_TEST_STREAM_KEY
});
const jaasEvent: {
customerId: string;
eventType: string;
} = await webhooksProxy.waitForEvent('LIVE_STREAM_STARTED');
expect('LIVE_STREAM_STARTED').toBe(jaasEvent.eventType);
expect(jaasEvent.customerId).toBe(customerId);
const iFrameEvent = (await p.getIframeAPI().getEventResult('recordingStatusChanged'));
expect(iFrameEvent.mode).toBe('stream');
expect(iFrameEvent.on).toBe(true);
if (process.env.YTUBE_TEST_BROADCAST_ID) {
const liveStreamUrl = await p.getIframeAPI().getLivestreamUrl();
expect(liveStreamUrl.livestreamUrl).toBeDefined();
}
await p.getIframeAPI().executeCommand('stopRecording', 'stream');
const jaasEndedEvent: {
customerId: string;
eventType: string;
} = await webhooksProxy.waitForEvent('LIVE_STREAM_ENDED');
expect(jaasEndedEvent.eventType).toBe('LIVE_STREAM_ENDED');
expect(jaasEndedEvent.customerId).toBe(customerId);
const iFrameEndedEvent = (await p.getIframeAPI().getEventResult('recordingStatusChanged'));
expect(iFrameEndedEvent.mode).toBe('stream');
expect(iFrameEndedEvent.on).toBe(false);
});
});

View File

@@ -15,10 +15,9 @@ describe('Visitors triggered by reaching participantsSoftLimit', () => {
};
/// 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 })
);
const m = await joinMuc({
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
});
expect(await m.isInMuc()).toBe(true);
expect(await m.isModerator()).toBe(true);
@@ -26,10 +25,10 @@ describe('Visitors triggered by reaching participantsSoftLimit', () => {
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' })
);
const p = await joinMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Parti Cipant' })
});
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
@@ -37,10 +36,10 @@ describe('Visitors triggered by reaching participantsSoftLimit', () => {
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' })
);
const v = await joinMuc({
name: 'p3',
token: t({ room: ctx.roomName, displayName: 'Visi Tor' })
});
expect(await v.isInMuc()).toBe(true);
expect(await v.isModerator()).toBe(false);

View File

@@ -23,21 +23,20 @@ describe('Visitor receiving video from a single remote participant', () => {
enabled: false
}
};
const sender = await joinMuc(
'p1',
t({ room: ctx.roomName, displayName: 'Sender', moderator: true }), {
configOverwrite
}
);
const sender = await joinMuc({
token: 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
}
);
const visitor = await joinMuc({
name: instanceId,
token: t({ room: ctx.roomName, displayName: 'Visitor', visitor: true })
}, {
configOverwrite
});
await visitor.waitForIceConnected();

View File

@@ -13,10 +13,9 @@ describe('Visitors triggered by visitor tokens', () => {
visitorsEnabled: true
};
const m = await joinMuc(
'p1',
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
);
const m = await joinMuc({
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
});
expect(await m.isInMuc()).toBe(true);
expect(await m.isModerator()).toBe(true);
@@ -24,10 +23,10 @@ describe('Visitors triggered by visitor tokens', () => {
console.log('Moderator joined');
// Joining with a participant token before any visitors
const p = await joinMuc(
'p2',
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
);
const p = await joinMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Parti Cipant' })
});
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
@@ -35,10 +34,10 @@ describe('Visitors triggered by visitor tokens', () => {
console.log('Participant joined');
// Joining with a visitor token
const v = await joinMuc(
'p3',
t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
);
const v = await joinMuc({
name: 'p3',
token: t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
});
expect(await v.isInMuc()).toBe(true);
expect(await v.isModerator()).toBe(false);
@@ -46,9 +45,10 @@ describe('Visitors triggered by visitor tokens', () => {
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' }));
const v2 = await joinMuc({
name: 'p2',
token: t({ room: ctx.roomName, displayName: 'Visi Tor 2' })
});
expect(await v2.isInMuc()).toBe(true);
expect(await v2.isModerator()).toBe(false);