Compare commits

...

8 Commits

Author SHA1 Message Date
damencho
c6e1a12d52 feat(tests): Return early if jaas tests not configured. 2025-08-22 12:02:19 -05:00
damencho
5a00fde865 feat(tests): Make sure we add a single listener for iframeAPI events. 2025-08-22 12:02:19 -05:00
damencho
e14ffe55da feat(tests): Clear previous videoConferenceLeft events. 2025-08-22 12:02:19 -05:00
damencho
dc1f20e059 fix(localrecording): Local recording is not supported in embedded mode.
It is not available due to cross-origin or not able to start setCaptureHandleConfig in iframe.
error 1: Failed to execute 'showSaveFilePicker' on 'Window': Cross origin sub frames aren't allowed to show a file picker.
error 2: Failed to execute 'setCaptureHandleConfig' on 'MediaDevices': Can only be called from the top-level document.
2025-08-22 06:51:12 -05:00
bgrozev
61ee9af304 test: Add a test for visitors with single sender (PLI). (#16364) 2025-08-21 16:31:03 -05:00
bgrozev
d75de3642e Fix jaas tests (#16360)
* fix: Fix jaas joinMuc(), it remove now redundant calls to hangup().

* fix: Fix jaas passcode tests.

* ref: make joinParticipant private again.
2025-08-20 14:46:52 -05:00
Calinteodor
1ae1729545 chore(android): add top and bottom margin insets for API 35 (#16359)
* Once we started targeting SDK 35 on a device running Android 15 or higher, by default, we display edge-to-edge.
  We can handle overlaps by using insets.
2025-08-20 17:22:33 +03:00
Saúl Ibarra Corretgé
8cea505417 fix(dynamic-branding) cleanup custom icon SVGs 2025-08-20 15:49:12 +02:00
20 changed files with 267 additions and 73 deletions

View File

@@ -25,9 +25,15 @@ import android.content.IntentFilter;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.facebook.react.modules.core.PermissionListener;
@@ -87,6 +93,28 @@ public class JitsiMeetActivity extends AppCompatActivity
launch(context, options);
}
public static void addTopBottomInsets(@NonNull Window w, @NonNull View v) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) return;
View decorView = w.getDecorView();
decorView.post(() -> {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
if (insets != null) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
params.topMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
params.bottomMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
v.setLayoutParams(params);
decorView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
view.setBackgroundColor(JitsiMeetView.BACKGROUND_COLOR);
return windowInsets;
});
}
});
}
// Overrides
//
@@ -107,6 +135,7 @@ public class JitsiMeetActivity extends AppCompatActivity
JitsiMeetActivityDelegate.onHostResume(this);
setContentView(R.layout.activity_jitsi_meet);
addTopBottomInsets(getWindow(),findViewById(android.R.id.content));
this.jitsiView = findViewById(R.id.jitsiView);
registerForBroadcastMessages();

View File

@@ -36,7 +36,7 @@ public class JitsiMeetView extends FrameLayout {
/**
* Background color. Should match the background color set in JS.
*/
private static final int BACKGROUND_COLOR = 0xFF040404;
public static final int BACKGROUND_COLOR = 0xFF040404;
/**
* React Native root view.

31
package-lock.json generated
View File

@@ -51,6 +51,7 @@
"clipboard-copy": "4.0.1",
"clsx": "1.1.1",
"dayjs": "1.11.13",
"dompurify": "3.2.6",
"dropbox": "10.7.0",
"focus-visible": "5.1.0",
"glob": "11.0.3",
@@ -7957,6 +7958,13 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unorm": {
"version": "1.3.28",
"resolved": "https://registry.npmjs.org/@types/unorm/-/unorm-1.3.28.tgz",
@@ -12437,6 +12445,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
@@ -32389,6 +32406,12 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
},
"@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"optional": true
},
"@types/unorm": {
"version": "1.3.28",
"resolved": "https://registry.npmjs.org/@types/unorm/-/unorm-1.3.28.tgz",
@@ -35531,6 +35554,14 @@
"domelementtype": "^2.2.0"
}
},
"dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"requires": {
"@types/trusted-types": "^2.0.7"
}
},
"domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",

View File

@@ -57,6 +57,7 @@
"clipboard-copy": "4.0.1",
"clsx": "1.1.1",
"dayjs": "1.11.13",
"dompurify": "3.2.6",
"dropbox": "10.7.0",
"focus-visible": "5.1.0",
"glob": "11.0.3",

View File

@@ -2,6 +2,7 @@ import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import { cleanSvg } from './functions';
import logger from './logger';
/**
@@ -82,7 +83,7 @@ export const fetchCustomIcons = async (customIcons: Record<string, string>) => {
if (response.ok) {
const svgXml = await response.text();
localCustomIcons[key] = svgXml;
localCustomIcons[key] = cleanSvg(svgXml);
} else {
logger.error(`Failed to fetch ${url}. Status: ${response.status}`);
}

View File

@@ -0,0 +1,9 @@
/**
* Sanitizes the given SVG by removing dangerous elements.
*
* @param {string} svg - The SVG string to clean.
* @returns {string} The sanitized SVG string.
*/
export function cleanSvg(svg: string): string {
return svg;
}

View File

@@ -1,11 +1,22 @@
import { Theme } from '@mui/material';
import { adaptV4Theme, createTheme } from '@mui/material/styles';
import DOMPurify from 'dompurify';
import { breakpoints, colorMap, font, shape, spacing, typography } from '../base/ui/Tokens';
import { createColorTokens } from '../base/ui/utils';
const DEFAULT_FONT_SIZE = 16;
/**
* Sanitizes the given SVG by removing dangerous elements.
*
* @param {string} svg - The SVG string to clean.
* @returns {string} The sanitized SVG string.
*/
export function cleanSvg(svg: string): string {
return DOMPurify.sanitize(svg);
}
/**
* Converts unitless fontSize and lineHeight values in a typography style object to rem units.
* Backward compatibility: This conversion supports custom themes that may still override

View File

@@ -7,6 +7,7 @@ import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getSoundFileSrc } from '../base/media/functions';
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
import { registerSound, unregisterSound } from '../base/sounds/actions';
import { isEmbedded } from '../base/util/embedUtils';
import { isSpotTV } from '../base/util/spot';
import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
@@ -151,7 +152,7 @@ export function getSessionStatusToShow(state: IReduxState, mode: string): string
* @returns {boolean} - Whether local recording is supported or not.
*/
export function supportsLocalRecording() {
return LocalRecordingManager.isSupported();
return LocalRecordingManager.isSupported() && !isEmbedded();
}
/**

View File

@@ -188,7 +188,7 @@ export class Participant {
* @param {IParticipantJoinOptions} options - Options for joining.
* @returns {Promise<void>}
*/
async joinConference(options: IParticipantJoinOptions): Promise<void> {
async joinConference(options: IParticipantJoinOptions): Promise<Participant> {
const config = {
room: options.roomName,
configOverwrite: {
@@ -254,6 +254,8 @@ export class Participant {
}
await this.postLoadProcess();
return this;
}
/**
@@ -466,7 +468,7 @@ export class Participant {
}
/**
* Waits for remote streams.
* Waits until there are at least [number] participants that have at least one track.
*
* @param {number} number - The number of remote streams to wait for.
* @returns {Promise<boolean>}
@@ -872,26 +874,46 @@ export class Participant {
}
}
/**
* Checks if video is currently received for the given remote endpoint ID (there is a track, it's not muted,
* and it's streaming status according to the connection-indicator is active).
*/
async isRemoteVideoReceived(endpointId: string): Promise<boolean> {
return this.execute(e => JitsiMeetJS.app.testing.isRemoteVideoReceived(e), endpointId);
}
/**
* Checks if the remove video is displayed for the given remote endpoint ID.
* @param endpointId
*/
async isRemoteVideoDisplayed(endpointId: string): Promise<boolean> {
return this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting();
}
/**
* Check if remote video for a specific remote endpoint is both received and displayed.
* @param endpointId
*/
async isRemoteVideoReceivedAndDisplayed(endpointId: string): Promise<boolean> {
return await this.isRemoteVideoReceived(endpointId) && await this.isRemoteVideoDisplayed(endpointId);
}
/**
* Waits for remote video state - receiving and displayed.
* @param endpointId
* @param reverse
* @param reverse if true, waits for the remote video to NOT be received AND NOT displayed.
*/
async waitForRemoteVideo(endpointId: string, reverse = false) {
if (reverse) {
await this.driver.waitUntil(async () =>
!await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && !await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
!await this.isRemoteVideoReceived(endpointId) && !await this.isRemoteVideoDisplayed(endpointId), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.name}`
});
} else {
await this.driver.waitUntil(async () =>
await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
await this.isRemoteVideoReceivedAndDisplayed(endpointId), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
});

View File

@@ -165,14 +165,16 @@ export async function ensureTwoParticipants(options?: IJoinOptions): Promise<voi
}
/**
* Creates a participant instance or prepares one for re-joining.
* Creates a new participant instance, or returns an existing one if it is already joined.
* @param participantOptions - The participant options, with required name set.
* @param {boolean} options - Join options.
* @param reuse whether to reuse an existing participant instance if one is available.
* @returns {Promise<Participant>} - The participant instance.
*/
export async function joinParticipant( // eslint-disable-line max-params
async function joinParticipant( // eslint-disable-line max-params
participantOptions: IParticipantOptions,
options?: IJoinOptions): Promise<Participant> {
options?: IJoinOptions
): Promise<Participant> {
participantOptions.iFrameApi = ctx.testProperties.useIFrameApi;
@@ -195,8 +197,6 @@ export async function joinParticipant( // eslint-disable-line max-params
// Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
await p.driver.url('/base.html');
// we want the participant instance re-recreated so we clear any kept state, like endpoint ID
}
const newParticipant = new Participant(participantOptions);
@@ -211,13 +211,12 @@ export async function joinParticipant( // eslint-disable-line max-params
&& config.iframe.usesJaas && config.iframe.tenant) {
forceTenant = config.iframe.tenant;
}
await newParticipant.joinConference({
return await newParticipant.joinConference({
...options,
forceTenant,
roomName: options?.roomName || ctx.roomName,
});
return newParticipant;
}
/**

View File

@@ -26,7 +26,11 @@ export type IContext = {
**/
testProperties: ITestProperties;
times: any;
/** A WebhooksProxy instance generated by the framework and available for tests to use, if configured. */
/**
* A WebhooksProxy instance generated by the framework and available for tests to use, if configured.
* Note that this is only configured for roomName, if a test wishes to use a different room name it can set up
* a WebhooksProxy instance itself.
*/
webhooksProxy: WebhookProxy;
};

View File

@@ -41,6 +41,10 @@ export default class IframeAPI extends BasePageObject {
addEventListener(eventName: string) {
return this.participant.execute(
(event, prefix) => {
// we want to add it once as we use static .test[event] to store the last event
if (window.jitsiAPI.listenerCount(event) > 0) {
return;
}
console.log(`${new Date().toISOString()} ${prefix}iframeAPI - Adding listener for event: ${event}`);
window.jitsiAPI.addListener(event, evt => {
console.log(

View File

@@ -1,7 +1,7 @@
import type { Participant } from '../../helpers/Participant';
import { Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import { joinParticipant } from '../../helpers/participants';
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
import { IParticipantJoinOptions } from '../../helpers/types';
export function generateJaasToken(options: ITokenOptions): IToken {
if (!config.jaas.enabled) {
@@ -17,27 +17,46 @@ export function generateJaasToken(options: ITokenOptions): IToken {
}
/**
* Creates a new Participant and joins the MUC with the given name. The jaas-specific properties must be set as
* environment variables (see env.example and TestsConfig.ts).
* 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 roomName the name of the room to join, if any. If not provided, the ctx generated one will be used.
* @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, roomName?: string):
Promise<Participant> {
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.');
}
return await joinParticipant({
// @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
roomName: joinOptions?.roomName || ctx.roomName,
});
}

View File

@@ -246,6 +246,8 @@ describe('Participants presence', () => {
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);
@@ -382,6 +384,7 @@ describe('Participants presence', () => {
await p1.switchToAPI();
await p2.switchToAPI();
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
await p2.getIframeAPI().addEventListener('readyToClose');
@@ -411,6 +414,7 @@ describe('Participants presence', () => {
await p1.switchToAPI();
await p1.getIframeAPI().clearEventResults('videoConferenceLeft');
await p1.getIframeAPI().addEventListener('videoConferenceLeft');
await p1.getIframeAPI().addEventListener('readyToClose');

View File

@@ -13,8 +13,6 @@ describe('XMPP login and MUC join test', () => {
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
await p.hangup();
});
it('with a valid token (specific room)', async () => {
@@ -23,8 +21,6 @@ describe('XMPP login and MUC join test', () => {
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
await p.hangup();
});
it('with a token with bad signature', async () => {
@@ -41,8 +37,6 @@ describe('XMPP login and MUC join test', () => {
|| await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
expect(errorText).toContain('not allowed to join');
await p.hangup();
});
it('with an expired token', async () => {
@@ -54,8 +48,6 @@ describe('XMPP login and MUC join test', () => {
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
expect(errorText).toContain('Token is expired');
await p.hangup();
});
it('with a token using the wrong key ID', async () => {
@@ -67,8 +59,6 @@ describe('XMPP login and MUC join test', () => {
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
expect(errorText).toContain('not allowed to join');
await p.hangup();
});
it('with a token for a different room', async () => {
@@ -80,8 +70,6 @@ describe('XMPP login and MUC join test', () => {
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
expect(errorText).toContain('not allowed to join');
await p.hangup();
});
it('with a moderator token', async () => {
@@ -90,8 +78,6 @@ describe('XMPP login and MUC join test', () => {
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(true);
await p.hangup();
});
// This is dependent on jaas account configuration. All tests under jaas/ expect that "unauthenticated access" is
@@ -105,8 +91,6 @@ describe('XMPP login and MUC join test', () => {
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
expect(errorText).toContain('not allowed to join');
await p.hangup();
});
// it('without sending a conference-request', async () => {

View File

@@ -4,7 +4,8 @@ import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
const passcode = '1234';
@@ -16,22 +17,11 @@ describe('Setting passcode through settings provisioning', () => {
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('p1', t({ room: ctx.roomName, moderator: true }));
await joinWithPassword('p1', t({ room: ctx.roomName, visitor: true }));
});
it('With an invalid passcode', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
passcode: 'passcode-must-be-digits-only'
};
const roomName = ctx.roomName + '-2';
const p = await joinMuc('p1', t({ room: roomName }), roomName);
// Setting the passcode should fail, resulting in the room being accessible without a password
await p.waitToJoinMUC();
expect(await p.isInMuc()).toBe(true);
expect(await p.getPasswordDialog().isOpen()).toBe(false);
await joinWithPassword('p2', t({ room: ctx.roomName, moderator: true }));
await joinWithPassword('p2', t({ room: ctx.roomName, visitor: true }));
});
});
@@ -41,7 +31,7 @@ describe('Setting passcode through settings provisioning', () => {
*/
async function joinWithPassword(instanceId: string, token: IToken) {
// @ts-ignore
const p = await joinMuc(instanceId, token);
const p = await joinMuc(instanceId, token, ctx.roomName);
await p.waitForMucJoinedOrError();
expect(await p.isInMuc()).toBe(false);
@@ -57,7 +47,5 @@ async function joinWithPassword(instanceId: string, token: IToken) {
expect(await p.isInMuc()).toBe(true);
expect(await p.getPasswordDialog().isOpen()).toBe(false);
await p.hangup();
}

View 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);
});
});

View 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');
});
});

View File

@@ -45,8 +45,6 @@ describe('Visitors triggered by visitor tokens', () => {
expect(await v.isVisitor()).toBe(true);
console.log('Visitor joined');
await p.hangup();
// Joining with a participant token after visitors...:mindblown:
const v2 = await joinMuc(
'p2',

View File

@@ -211,6 +211,13 @@ export const config: WebdriverIO.MultiremoteConfig = {
} as IContext;
globalAny.ctx.testProperties = testProperties;
if (testProperties.useJaas && !testsConfig.jaas.enabled) {
console.warn(`JaaS is not configured, skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = true;
return;
}
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
const bInstance = multiremotebrowser.getInstance(instance);
@@ -233,6 +240,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
}));
globalAny.ctx.roomName = generateRoomName(testName);
console.log(`Using room name: ${globalAny.ctx.roomName}`);
// If we are running the iFrameApi tests, we need to mark it as such and if needed to create the proxy
// and connect to it.
@@ -251,11 +259,6 @@ export const config: WebdriverIO.MultiremoteConfig = {
console.warn(`WebhookProxy is not available, skipping ${testName}`);
globalAny.ctx.skipSuiteTests = true;
}
if (testProperties.useJaas && !testsConfig.jaas.enabled) {
console.warn(`JaaS is not configured, skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = true;
}
},
after() {